Make worktree cleanup audit merged-PR aware for issue worktrees #858

Closed
opened 2026-07-23 20:01:40 -05:00 by jcwalker3 · 0 comments
Owner

Problem

gitea_audit_worktree_cleanup has no merged-PR linkage. For workflow_type=issue_work worktrees it therefore reports pr_number=null and classifies every one of them active_issue_work with removable=false, permanently — even when the owning PR is merged, the head is already contained in master, and nothing protects the worktree.

The PR-aware assessor gitea_reconcile_merged_cleanups looks at the same on-disk state and disagrees.

Observed disagreement (master 9301739910df)

For registered worktree branches/issue-637-timeline-model (branch feat/issue-637-timeline-model, head b9ba43a5bf292690eec0b24b19a1f489930986e7, owning PR #849 merged as 9301739910df7b51ca3abcdbf46cce25cfb026e5, issue #637 closed, no lease, clean):

  • gitea_reconcile_merged_cleanups links the worktree to merged PR #849 and reports safe_to_remove_worktree=true, safe_to_delete_remote=true, block_reasons=[].
  • gitea_audit_worktree_cleanup reports pr_number=null, classification=active_issue_work, removable=false, with dirty=false, has_open_pr=false, has_active_lease=false, has_active_issue_lock=false, is_protected=false.

Repository-wide in the same audit run: 42 worktrees of workflow_type=issue_work, removable=true on 0 of them, pr_number populated on 0 of them.

Root cause

Two independent gaps in worktree_cleanup_audit.py and its MCP wiring:

  1. No PR linkage at all. audit_branches_directory builds metadata with build_worktree_metadata(path=..., branch=..., head_sha=...) and never supplies pr_number, so pr_number is structurally always null. gitea_audit_worktree_cleanup fetches only pulls?state=open; merged PRs are never read, so no owning-PR evidence exists.

  2. clean_stale_removable is unreachable for issue_work. In classify_worktree, issue_work/conflict_fix reach clean_stale_removable only when ttl_expired is true. audit_branches_directory derives that from metadata["last_used_at"], which build_worktree_metadata never populates, and is_ttl_expired fail-safes to False on an unknown timestamp. So ttl_expired is always False and the final return CLASS_ACTIVE_ISSUE_WORK is the only reachable outcome for a clean issue_work worktree.

A merged issue_work worktree is therefore reported as active work forever, and the audit can never agree with the PR-scoped reconciler.

Adjacent safety gap this exposes

audit_branches_directory accepts leased_branches, but gitea_audit_worktree_cleanup never passes it, so has_active_lease is false for every worktree in a live run (0 of 90 observed). This is currently inert only because issue_work can never become removable. Making removability reachable without first wiring real lease evidence would convert an inert gap into an active hole, so lease and issue-lock protection must be supplied from authoritative state as part of this change.

Required behavior

  1. An issue_work worktree can be linked deterministically to its owning PR when authoritative repository state permits, and pr_number is populated from that linkage.
  2. A clean worktree may become removable only when all of the following hold: the owning PR is conclusively identified; that PR is merged; the worktree branch and the expected PR head branch agree; the worktree head is contained in authoritative master; no open or competing PR requires the branch; no active lease or issue lock protects the worktree; no live process or session uses it; the worktree is clean; no unmerged commits exist; and it is not the stable control checkout.
  3. Ownership, PR linkage, merge state, containment, cleanliness, and protection state that cannot be proven fail closed.
  4. Existing blocking classifications are preserved for: active open PR, active lease or lock, dirty worktree, unmerged head, ambiguous ownership, protected/control checkout, live process, and unsafe or incomplete repository state.
  5. active_issue_work is not weakened globally; it is replaced only when authoritative merged-PR evidence satisfies the complete policy.
  6. The audit and gitea_reconcile_merged_cleanups stay semantically aligned while keeping their distinct responsibilities.
  7. The assessor performs no deletion and gains no cleanup mutation.

Acceptance criteria

  • A clean merged issue_work worktree is linked to its owning PR and can become removable.
  • pr_number is populated from authoritative linkage rather than left null.
  • A worktree whose branch has an open PR remains non-removable.
  • A dirty worktree, including untracked-only changes, remains non-removable.
  • An active lease remains protective, from authoritative lease state.
  • An active issue lock remains protective.
  • An unmerged head, or a head not contained in master, remains non-removable.
  • Ambiguous or competing PR linkage fails closed.
  • The control checkout is never removable.
  • For equivalent safe input the audit result agrees with the PR-scoped reconciler.
  • The assessor itself performs no deletion.
  • Classifications for worktree kinds other than issue_work are unchanged.
  • Tests use fabricated temporary repositories and records only.

Explicit non-authorization

This task authorizes no real worktree or branch cleanup. Implementation and tests must not remove any real worktree or delete any real remote branch. In particular the branches/issue-637-timeline-model worktree and the feat/issue-637-timeline-model remote branch are retained as the reproduction fixture and must be left untouched; PR #849 and issue #637 must not be modified.

Scope boundary against issue #855

Issue #855 is a reconciler-executor issue: it asks for a PR-scoped target for gitea_reconcile_merged_cleanups (whose execute path is batch-only) and for expired reviewer leases to stop protecting an already-merged branch. It does not mention gitea_audit_worktree_cleanup, PR linkage in the audit, or the permanent active_issue_work classification, and none of its acceptance criteria cover them.

This issue is assessor-side only and additive to #855: it makes the audit's classification merged-PR aware. It does not add a PR-scoped executor, does not change lease reclaim policy, and does not alter the batch reconciliation path. #855 is cross-referenced as context only; the two scopes stay separate.

Non-goals

  • Adding any cleanup or deletion mutation to gitea_audit_worktree_cleanup.
  • Adding a PR-scoped executor or changing expired-lease reclaim (both belong to #855).
  • Cleaning PR #849 or PR #818 residue.
  • Weakening ownership guards anywhere.

References

  • Code paths: worktree_cleanup_audit.py (classify_worktree, audit_branches_directory, build_worktree_metadata), gitea_audit_worktree_cleanup in gitea_mcp_server.py.
  • Alignment target: merged_cleanup_reconcile.py (build_pr_cleanup_entry, assess_local_worktree_cleanup, is_head_ancestor_of_ref).
  • Observed at master 9301739910df.
## Problem `gitea_audit_worktree_cleanup` has no merged-PR linkage. For `workflow_type=issue_work` worktrees it therefore reports `pr_number=null` and classifies every one of them `active_issue_work` with `removable=false`, permanently — even when the owning PR is merged, the head is already contained in master, and nothing protects the worktree. The PR-aware assessor `gitea_reconcile_merged_cleanups` looks at the same on-disk state and disagrees. ## Observed disagreement (master `9301739910df`) For registered worktree `branches/issue-637-timeline-model` (branch `feat/issue-637-timeline-model`, head `b9ba43a5bf292690eec0b24b19a1f489930986e7`, owning PR #849 merged as `9301739910df7b51ca3abcdbf46cce25cfb026e5`, issue #637 closed, no lease, clean): - `gitea_reconcile_merged_cleanups` links the worktree to merged PR #849 and reports `safe_to_remove_worktree=true`, `safe_to_delete_remote=true`, `block_reasons=[]`. - `gitea_audit_worktree_cleanup` reports `pr_number=null`, `classification=active_issue_work`, `removable=false`, with `dirty=false`, `has_open_pr=false`, `has_active_lease=false`, `has_active_issue_lock=false`, `is_protected=false`. Repository-wide in the same audit run: 42 worktrees of `workflow_type=issue_work`, `removable=true` on 0 of them, `pr_number` populated on 0 of them. ## Root cause Two independent gaps in `worktree_cleanup_audit.py` and its MCP wiring: 1. **No PR linkage at all.** `audit_branches_directory` builds metadata with `build_worktree_metadata(path=..., branch=..., head_sha=...)` and never supplies `pr_number`, so `pr_number` is structurally always `null`. `gitea_audit_worktree_cleanup` fetches only `pulls?state=open`; merged PRs are never read, so no owning-PR evidence exists. 2. **`clean_stale_removable` is unreachable for `issue_work`.** In `classify_worktree`, `issue_work`/`conflict_fix` reach `clean_stale_removable` only when `ttl_expired` is true. `audit_branches_directory` derives that from `metadata["last_used_at"]`, which `build_worktree_metadata` never populates, and `is_ttl_expired` fail-safes to `False` on an unknown timestamp. So `ttl_expired` is always `False` and the final `return CLASS_ACTIVE_ISSUE_WORK` is the only reachable outcome for a clean `issue_work` worktree. A merged `issue_work` worktree is therefore reported as active work forever, and the audit can never agree with the PR-scoped reconciler. ## Adjacent safety gap this exposes `audit_branches_directory` accepts `leased_branches`, but `gitea_audit_worktree_cleanup` never passes it, so `has_active_lease` is `false` for every worktree in a live run (0 of 90 observed). This is currently inert only because `issue_work` can never become removable. Making removability reachable without first wiring real lease evidence would convert an inert gap into an active hole, so lease and issue-lock protection must be supplied from authoritative state as part of this change. ## Required behavior 1. An `issue_work` worktree can be linked deterministically to its owning PR when authoritative repository state permits, and `pr_number` is populated from that linkage. 2. A clean worktree may become removable only when **all** of the following hold: the owning PR is conclusively identified; that PR is merged; the worktree branch and the expected PR head branch agree; the worktree head is contained in authoritative master; no open or competing PR requires the branch; no active lease or issue lock protects the worktree; no live process or session uses it; the worktree is clean; no unmerged commits exist; and it is not the stable control checkout. 3. Ownership, PR linkage, merge state, containment, cleanliness, and protection state that cannot be proven fail closed. 4. Existing blocking classifications are preserved for: active open PR, active lease or lock, dirty worktree, unmerged head, ambiguous ownership, protected/control checkout, live process, and unsafe or incomplete repository state. 5. `active_issue_work` is not weakened globally; it is replaced only when authoritative merged-PR evidence satisfies the complete policy. 6. The audit and `gitea_reconcile_merged_cleanups` stay semantically aligned while keeping their distinct responsibilities. 7. The assessor performs no deletion and gains no cleanup mutation. ## Acceptance criteria - [ ] A clean merged `issue_work` worktree is linked to its owning PR and can become removable. - [ ] `pr_number` is populated from authoritative linkage rather than left null. - [ ] A worktree whose branch has an open PR remains non-removable. - [ ] A dirty worktree, including untracked-only changes, remains non-removable. - [ ] An active lease remains protective, from authoritative lease state. - [ ] An active issue lock remains protective. - [ ] An unmerged head, or a head not contained in master, remains non-removable. - [ ] Ambiguous or competing PR linkage fails closed. - [ ] The control checkout is never removable. - [ ] For equivalent safe input the audit result agrees with the PR-scoped reconciler. - [ ] The assessor itself performs no deletion. - [ ] Classifications for worktree kinds other than `issue_work` are unchanged. - [ ] Tests use fabricated temporary repositories and records only. ## Explicit non-authorization This task authorizes **no** real worktree or branch cleanup. Implementation and tests must not remove any real worktree or delete any real remote branch. In particular the `branches/issue-637-timeline-model` worktree and the `feat/issue-637-timeline-model` remote branch are retained as the reproduction fixture and must be left untouched; PR #849 and issue #637 must not be modified. ## Scope boundary against issue #855 Issue #855 is a reconciler-executor issue: it asks for a PR-scoped target for `gitea_reconcile_merged_cleanups` (whose execute path is batch-only) and for expired reviewer leases to stop protecting an already-merged branch. It does not mention `gitea_audit_worktree_cleanup`, PR linkage in the audit, or the permanent `active_issue_work` classification, and none of its acceptance criteria cover them. This issue is assessor-side only and additive to #855: it makes the audit's classification merged-PR aware. It does not add a PR-scoped executor, does not change lease reclaim policy, and does not alter the batch reconciliation path. #855 is cross-referenced as context only; the two scopes stay separate. ## Non-goals - Adding any cleanup or deletion mutation to `gitea_audit_worktree_cleanup`. - Adding a PR-scoped executor or changing expired-lease reclaim (both belong to #855). - Cleaning PR #849 or PR #818 residue. - Weakening ownership guards anywhere. ## References - Code paths: `worktree_cleanup_audit.py` (`classify_worktree`, `audit_branches_directory`, `build_worktree_metadata`), `gitea_audit_worktree_cleanup` in `gitea_mcp_server.py`. - Alignment target: `merged_cleanup_reconcile.py` (`build_pr_cleanup_entry`, `assess_local_worktree_cleanup`, `is_head_ancestor_of_ref`). - Observed at master `9301739910df`.
jcwalker3 added the status:readyworkflow-hardeningtype:bug labels 2026-07-23 20:01:40 -05:00
jcwalker3 added status:pr-open and removed status:ready labels 2026-07-23 20:21:53 -05:00
sysadmin removed the status:pr-open label 2026-07-23 22:02:29 -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#858