Terminal retirement of author issue leases (Issue #790 Slice B) #792

Open
opened 2026-07-22 03:27:19 -05:00 by jcwalker3 · 0 comments
Owner

Scope transfer

Slice B of Issue #790, transferred to its own issue so that closing #790 through the Slice A pull request does not imply this work is done. Issue #790 tracks and is completed by Slice A only. The acceptance criteria below are carried verbatim from the controller reassessment recorded in Issue #790 comment 13958.

Slice A delivered the shared lease policy, the load-bearing task heartbeat, per-task session identity, and legacy-lock compatibility. This issue delivers terminal retirement on top of it.

Problem

Work that has reached its terminal state still holds its author claim. When a pull request has completed integration and its issue is closed, the durable author lock keeps protecting the branch and worktree until the lease lapses on its own. Reconciliation is blocked for no safety benefit: the artifacts are already proven safe.

Two production instances are on record. Issue #787 / PR #789 stalled on a four-hour lease whose owner processes were dead. Issue #760 / PR #791 stalled for more than five hours after its merge commit 620ed6e9a9550b8da2ceb82d9ab8744e8920490f landed, with the lease still unexpired and its recorded pid alive as a long-running MCP daemon.

Slice A shortens the window for an abandoned lease to at most one ten-minute grace, but it does not make terminal work retire promptly, and it deliberately did not: retirement is this issue.

The correction, and the trap to avoid

The original #790 text conjoined "owner process is dead or its heartbeat exceeded the allowed grace period" with the terminality and artifact proofs. That is wrong, and the controller struck it. Terminal work is terminal regardless of whether a session is still heartbeating on it: a session heartbeating on integrated work is a leaked session, not protected work. Implemented as originally written, the PR #791 cleanup would still have stalled.

Owner heartbeat freshness and recorded pid liveness are recorded as audit evidence and are never retirement preconditions.

Two distinct steps with two distinct gates

Step 1 — record terminal status. Once every server-side proof below passes, the reconciler may atomically record terminal status immediately, through the lock_generation compare-and-swap. There is no waiting period on this step.

  1. The owning pull request is in its closed post-merge state at the exact recorded head: a merge commit is present, and the pull-request head matches the branch head recorded on the durable lock.
  2. The linked issue is closed or otherwise canonically terminal.
  3. The branch head is contained in the target branch, proven by git merge-base --is-ancestor.
  4. The registered worktree exists, is clean by parse_dirty_tracked_files, and sits on the locked branch.
  5. No unpublished work: the local head matches the remote branch head.
  6. No newer lock_generation, no competing live lock, no competing branch carrying the issue marker, and no other owning pull request.

Step 2 — destructive branch and worktree cleanup. Permitted only after the configurable two-minute race-drain interval has elapsed, and only after re-reading and re-proving all of: lock_generation, terminal state, worktree cleanliness, branch containment in the target branch, and absence of competing ownership. Any drift observed on the re-read fails closed and cancels the cleanup.

Acceptance criteria

AC-B1. Terminal retirement has no owner-liveness precondition of any kind. When the terminality, artifact-safety, and generation proofs pass, retirement proceeds whether or not the recorded pid is alive and whether or not the heartbeat is fresh. Both are recorded as audit evidence. This is #790 AC-N3.

AC-B2. A pure terminal-state assessor gathers evidence and returns a disposition without mutation or caller-trusted input, mirroring the issue_lock_renewal and issue_lock_recovery pattern. It reuses the Issue #760 evidence gatherer rather than duplicating it.

AC-B3. A sanctioned reconciler-only operation records terminal status atomically through the lock_generation compare-and-swap. Author, reviewer, and merger roles fail closed.

AC-B4. The recorded terminal status is consumed by the existing branch_cleanup_guard._TERMINAL_OWNERSHIP_STATUSES path without modifying that module's classification logic. Slice A already confirmed that set contains released, terminal, and closed, and that assess_ownership_record_activity returns a non-blocking verdict for them, so this issue supplies a status producer rather than a new guard.

AC-B5. The race-drain interval comes from lease_policy.terminal_race_drain_minutes, which Slice A already declares at two minutes. No duration is hardcoded at a call site.

AC-B6. Destructive branch or worktree cleanup is refused until the race-drain interval has elapsed and every proof in step 1 has been independently re-established on a fresh read. Drift fails closed and names the element that changed.

AC-B7. Retirement is never caller-declarable. No parameter, boolean, or flag lets a caller assert terminality or eligibility; every proof is computed server-side from durable state plus live git and Gitea observation.

AC-B8. A durable retirement record captures the prior status, the retiring identity and profile, the prior and replacement generations, the terminal reason, the full proof set, and the pid and heartbeat evidence that did not gate the decision.

AC-B9. Native MCP integration tests cover every downstream gate the retirement path touches, driving the real tools against a real repository and a real durable lock. Assessor-only coverage is explicitly insufficient: it did not catch either of the two sanction discard points found in review #499 on PR #791. This is #790 AC-N6 applied to this slice.

AC-B10. Existing behavior is preserved: Issue #753 dead-session recovery, Issue #755 owning-pull-request evidence, Issue #772 generation compare-and-swap, Issue #747 reviewer sliding TTL, and the Slice A heartbeat lifecycle and legacy-lock protection.

Required tests

  1. Closed issue plus completed integration plus clean and contained artifacts permits retirement while the owner is still heartbeating.
  2. The same, with a dead owner pid — identical outcome, proving liveness is not a precondition in either direction.
  3. Dirty worktree blocks retirement.
  4. Unpublished commits block retirement.
  5. Head mismatch between the lock, the branch, and the pull request blocks retirement.
  6. Nonterminal issue blocks retirement.
  7. A newer lock generation blocks retirement.
  8. A competing live lock, competing branch marker, or other owning pull request blocks retirement.
  9. Author, reviewer, and merger roles are refused; only the reconciler proceeds.
  10. Destructive cleanup before the race-drain interval elapses is refused.
  11. Drift observed on the post-drain re-read cancels cleanup and names the changed element.
  12. A legacy lock qualifying for terminal retirement leaves the preserved-expiry state by this route.
  13. Read-after-write verifies the durable retirement record and the resulting ownership classification.
  14. Native MCP integration coverage per AC-B9.

Dependencies

Depends on Issue #790 Slice A landing first, for lease_policy, the lifecycle marker, and the heartbeat semantics this builds on. Also depends on the landed behavior of Issue #772, Issue #755, Issue #760, and Issue #601. Slice C is independent of this issue and may proceed in either order.

Non-goals

Do not implement Slice C. Do not weaken worktree cleanliness, ancestry, publication, or binding checks. Do not permit arbitrary lock deletion. Do not make pid liveness an ownership authority in either direction. Do not delete Issue #787, Issue #760, or PR #791 artifacts as part of this work.

Canonical issue state

STATE: ready-for-allocation
WHO_IS_NEXT: controller
NEXT_ACTION: Allocate after Issue #790 Slice A lands.
NEXT_PROMPT: Implement terminal retirement of author issue leases per AC-B1 through AC-B10, in a separately bounded author task, after Slice A has landed on master.
## Scope transfer Slice B of Issue #790, transferred to its own issue so that closing #790 through the Slice A pull request does not imply this work is done. Issue #790 tracks and is completed by Slice A only. The acceptance criteria below are carried verbatim from the controller reassessment recorded in Issue #790 comment 13958. Slice A delivered the shared lease policy, the load-bearing task heartbeat, per-task session identity, and legacy-lock compatibility. This issue delivers terminal retirement on top of it. ## Problem Work that has reached its terminal state still holds its author claim. When a pull request has completed integration and its issue is closed, the durable author lock keeps protecting the branch and worktree until the lease lapses on its own. Reconciliation is blocked for no safety benefit: the artifacts are already proven safe. Two production instances are on record. Issue #787 / PR #789 stalled on a four-hour lease whose owner processes were dead. Issue #760 / PR #791 stalled for more than five hours after its merge commit `620ed6e9a9550b8da2ceb82d9ab8744e8920490f` landed, with the lease still unexpired and its recorded pid alive as a long-running MCP daemon. Slice A shortens the window for an abandoned lease to at most one ten-minute grace, but it does not make terminal work retire promptly, and it deliberately did not: retirement is this issue. ## The correction, and the trap to avoid The original #790 text conjoined "owner process is dead or its heartbeat exceeded the allowed grace period" with the terminality and artifact proofs. That is wrong, and the controller struck it. Terminal work is terminal regardless of whether a session is still heartbeating on it: a session heartbeating on integrated work is a leaked session, not protected work. Implemented as originally written, the PR #791 cleanup would still have stalled. Owner heartbeat freshness and recorded pid liveness are recorded as audit evidence and are never retirement preconditions. ## Two distinct steps with two distinct gates **Step 1 — record terminal status.** Once every server-side proof below passes, the reconciler may atomically record terminal status immediately, through the `lock_generation` compare-and-swap. There is no waiting period on this step. 1. The owning pull request is in its closed post-merge state at the exact recorded head: a merge commit is present, and the pull-request head matches the branch head recorded on the durable lock. 2. The linked issue is closed or otherwise canonically terminal. 3. The branch head is contained in the target branch, proven by `git merge-base --is-ancestor`. 4. The registered worktree exists, is clean by `parse_dirty_tracked_files`, and sits on the locked branch. 5. No unpublished work: the local head matches the remote branch head. 6. No newer `lock_generation`, no competing live lock, no competing branch carrying the issue marker, and no other owning pull request. **Step 2 — destructive branch and worktree cleanup.** Permitted only after the configurable two-minute race-drain interval has elapsed, and only after re-reading and re-proving all of: `lock_generation`, terminal state, worktree cleanliness, branch containment in the target branch, and absence of competing ownership. Any drift observed on the re-read fails closed and cancels the cleanup. ## Acceptance criteria **AC-B1.** Terminal retirement has no owner-liveness precondition of any kind. When the terminality, artifact-safety, and generation proofs pass, retirement proceeds whether or not the recorded pid is alive and whether or not the heartbeat is fresh. Both are recorded as audit evidence. This is #790 AC-N3. **AC-B2.** A pure terminal-state assessor gathers evidence and returns a disposition without mutation or caller-trusted input, mirroring the `issue_lock_renewal` and `issue_lock_recovery` pattern. It reuses the Issue #760 evidence gatherer rather than duplicating it. **AC-B3.** A sanctioned reconciler-only operation records terminal status atomically through the `lock_generation` compare-and-swap. Author, reviewer, and merger roles fail closed. **AC-B4.** The recorded terminal status is consumed by the existing `branch_cleanup_guard._TERMINAL_OWNERSHIP_STATUSES` path without modifying that module's classification logic. Slice A already confirmed that set contains `released`, `terminal`, and `closed`, and that `assess_ownership_record_activity` returns a non-blocking verdict for them, so this issue supplies a status producer rather than a new guard. **AC-B5.** The race-drain interval comes from `lease_policy.terminal_race_drain_minutes`, which Slice A already declares at two minutes. No duration is hardcoded at a call site. **AC-B6.** Destructive branch or worktree cleanup is refused until the race-drain interval has elapsed and every proof in step 1 has been independently re-established on a fresh read. Drift fails closed and names the element that changed. **AC-B7.** Retirement is never caller-declarable. No parameter, boolean, or flag lets a caller assert terminality or eligibility; every proof is computed server-side from durable state plus live git and Gitea observation. **AC-B8.** A durable retirement record captures the prior status, the retiring identity and profile, the prior and replacement generations, the terminal reason, the full proof set, and the pid and heartbeat evidence that did not gate the decision. **AC-B9.** Native MCP integration tests cover every downstream gate the retirement path touches, driving the real tools against a real repository and a real durable lock. Assessor-only coverage is explicitly insufficient: it did not catch either of the two sanction discard points found in review #499 on PR #791. This is #790 AC-N6 applied to this slice. **AC-B10.** Existing behavior is preserved: Issue #753 dead-session recovery, Issue #755 owning-pull-request evidence, Issue #772 generation compare-and-swap, Issue #747 reviewer sliding TTL, and the Slice A heartbeat lifecycle and legacy-lock protection. ## Required tests 1. Closed issue plus completed integration plus clean and contained artifacts permits retirement while the owner is still heartbeating. 2. The same, with a dead owner pid — identical outcome, proving liveness is not a precondition in either direction. 3. Dirty worktree blocks retirement. 4. Unpublished commits block retirement. 5. Head mismatch between the lock, the branch, and the pull request blocks retirement. 6. Nonterminal issue blocks retirement. 7. A newer lock generation blocks retirement. 8. A competing live lock, competing branch marker, or other owning pull request blocks retirement. 9. Author, reviewer, and merger roles are refused; only the reconciler proceeds. 10. Destructive cleanup before the race-drain interval elapses is refused. 11. Drift observed on the post-drain re-read cancels cleanup and names the changed element. 12. A legacy lock qualifying for terminal retirement leaves the preserved-expiry state by this route. 13. Read-after-write verifies the durable retirement record and the resulting ownership classification. 14. Native MCP integration coverage per AC-B9. ## Dependencies Depends on Issue #790 Slice A landing first, for `lease_policy`, the lifecycle marker, and the heartbeat semantics this builds on. Also depends on the landed behavior of Issue #772, Issue #755, Issue #760, and Issue #601. Slice C is independent of this issue and may proceed in either order. ## Non-goals Do not implement Slice C. Do not weaken worktree cleanliness, ancestry, publication, or binding checks. Do not permit arbitrary lock deletion. Do not make pid liveness an ownership authority in either direction. Do not delete Issue #787, Issue #760, or PR #791 artifacts as part of this work. ## Canonical issue state ```text STATE: ready-for-allocation WHO_IS_NEXT: controller NEXT_ACTION: Allocate after Issue #790 Slice A lands. NEXT_PROMPT: Implement terminal retirement of author issue leases per AC-B1 through AC-B10, in a separately bounded author task, after Slice A has landed on master. ```
jcwalker3 added the bugtype:guardrailstatus:readyworkflow-hardeningmcp-health labels 2026-07-22 03:27:19 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Scaled-Tech-Consulting/Gitea-Tools#792