Cleanup Merged Worktrees
Drive the end-to-end cleanup of stale git worktrees and branches after their work has
merged into main. The deterministic classification, consolidation staging, and
deletion mechanics live in scripts/bash/cleanup-worktrees.sh (wrapping
scripts/bash/cleanup_worktrees_lib.sh and
scripts/bash/cleanup_worktrees_actions_lib.sh). This skill owns the editorial and
orchestration layer: deciding whether flagged unique content is genuinely
documentation/memory material, driving consolidation onto a single
documentationandmemories branch, delegating PR creation to Agent(pr-author), and
running the destructive apply pass only after the consolidation PR has merged.
The script is deterministic and owns the safe/unsafe decision; the LLM/editorial judgment (which flagged commits are documentation/memory content) is this skill's job and is out of the script's scope.
When to Use This Skill
- After an epic or feature's PRs have all merged and two to five stale
drm-copilot-wt-*branches or worktrees remain. - When you need a trustworthy, machine-parseable report of which branches/worktrees are
safe to delete (
MERGED_CLEAN,MERGED_CONTENT_NEUTRAL,MERGED_EQUIVALENT) versus which carry unmerged or unique work (NOT_MERGED,HAS_UNIQUE_RESIDUALS). - When stranded documentation/agent-memory commits were appended to a worktree branch after its feature content already merged and must be preserved before deletion.
- When a worktree is reported
BLOCKED-DIRTY, or its branch is classifiedNOT_MERGEDorHAS_UNIQUE_RESIDUALS, and the uncommitted or unmerged content it holds must be triaged into disposable versus must-preserve before the worktree can ever be deleted. - Do not use this skill to manage remote branches; its scope is local branches and local worktree registrations only, except for the explicitly confirmed origin-branch offer in the Dirty Worktree Triage Procedure's final step.
Report Line Contract
The script emits pipe-delimited, LC_ALL=C-ordered records, one per line:
BRANCH|<name>|<state>—stateinNOT_MERGED | MERGED_CLEAN | MERGED_CONTENT_NEUTRAL | MERGED_EQUIVALENT | HAS_UNIQUE_RESIDUALS | PROTECTED_CURRENT.COMMIT|<branch>|<sha>|<state>|<paths-csv>|<author>|<author-date>— per-commit state inEQUIVALENT | CONTENT_ON_MAIN | EMPTY | UNIQUE | CONFLICT. AUNIQUECOMMIT record is a cherry-pick candidate for editorial triage.WORKTREE|<path>|<branch-or-DETACHED>|<flags>— worktree registrations.WORKTREE|<path>|DETACHED|<state>|<flags>— a detached-HEAD worktree registration, classified on its own HEAD SHA. This five-field record replaces the four-field record above for a detached registration; it is not emitted in addition to it.stateis one ofMERGED_CLEAN | MERGED_CONTENT_NEUTRAL | MERGED_EQUIVALENT | NOT_MERGED | HAS_UNIQUE_RESIDUALS | PROTECTED_CURRENT | ANCESTRY_ERROR, whereANCESTRY_ERRORis the fail-closed verdict for a hard git failure at any rung of the classification. The fifth field carries the porcelain flag set unchanged, so thelockedandprunablemarkers are preserved in the record.WARN|main-divergence|<local-sha>|<origin-sha>— localmaindiffers fromorigin/main(advisory; classification still runs).DIRTY|<worktree-path>|<status-porcelain-line>— a dirty worktree that blocked removal. This record is apply mode only, and its three-field shape is unchanged by the dirt classifier: the classifier adds the two records below rather than altering or replacing this one.DIRTFILE|<worktree-path>|<verdict>|<detail>|<xy>|<file-path>— one record pergit status --porcelainentry in a dirty worktree, emitted by report mode in porcelain order immediately after that worktree'sWORKTREE|record.verdictis exactly one ofDISPOSABLE_BUILD_ARTIFACT | DISPOSABLE_SESSION_ARTIFACT | CONTENT_ON_MAIN | CONTENT_IN_HISTORY | STAGED_TREE_IS_COMMIT | UNIQUE, and no other verdict token is ever produced.detailcarries the commit SHA that justifies the verdict forSTAGED_TREE_IS_COMMITandCONTENT_IN_HISTORY, and is empty for the other four verdicts.xyis the porcelain two-character status field, preserved verbatim including its space. The file path is the last field, so a path containing a pipe character cannot shift any later field. A classification read that fails maps its entry toUNIQUE: that is the fail-closed direction, because an entry the tool could not classify is treated as content that must be preserved.CONTENT_ON_MAINis emitted for a tracked entry only whenmaincontains the path, so an entry whose content exists only as a staged blob is reportedUNIQUErather than as content that is already onmain. An entry whose porcelain status shows content in both the index and the working tree is likewise reportedUNIQUE, because the rungs that could otherwise resolve it compare working-tree content only and would leave the differing staged blob unaccounted for.DISPOSABLE_SESSION_ARTIFACTmatches three fixed repository paths underartifacts/, is repository-agnostic, and cannot fire in a checkout that gitignoresartifacts/— which drm-copilot does at.gitignore:6— because the status read never carries--ignored; the verdict is retained for consumer checkouts where those paths are not ignored, and it must not be made reachable by adding--ignored.DIRTSUM|<worktree-path>|<aggregate>|<detail>— exactly one record per dirty worktree, emitted by report mode immediately after that worktree'sDIRTFILE|records.aggregateisALL_DISPOSABLEif and only if the worktree has at least one status entry and none of them isUNIQUE, andHAS_UNIQUEotherwise.detailcarries the first non-empty per-entry detail. A worktree with zero status entries emits neither this record nor anyDIRTFILE|record. Both records are read-only: anALL_DISPOSABLEaggregate unlocks nothing on its own, and clearing happens only when the operator supplies--clear-disposablein apply mode.Report-mode exit status. When a candidate worktree's
git status --porcelainread fails, report mode emits noDIRTFILE|and noDIRTSUM|record for that worktree and returns git's non-zero exit code. A checkout containing such a worktree therefore exits non-zero from report mode where the same checkout previously exited 0 and produced a complete report. This is deliberate. A worktree whose status read failed produces no dirt records at all, so a report that also exited 0 would be indistinguishable from a report about a clean worktree, and an operator would make a deletion decision on silently incomplete data. The non-zero exit is the only channel that carries the incompleteness to a wrapping script. See the analogous apply-mode note in the End-to-End Workflow, where a blocked detached removal sets a non-zero exit status for the same reason.ACTION|<verb>|<target>|<result>— apply-mode action results.PRESERVE|<worktree-path>|<source-path>|<verdict>— a manifestpreserved_files[]finding staged onto the consolidation branch;verdictinDEAD_ONE_OFF | ALREADY_SOLVED_ELSEWHERE | STALE_OR_CONTRADICTED | GENUINELY_NEW | STILL_RELEVANT. The per-file outcome is reported by the companionACTION|preserve-stage|...record.ORPHAN_DIR|<path>|<size>— a directory under a worktree-tracking root that carries no.gitpointer file and nogit worktree listentry.<size>is best-effort and may be the literalunknown. The record is advisory: it reports the directory, and nothing in apply mode acts on it. For the disposition, see the Dirty Worktree Triage Procedure's step 7, which governs how an orphaned directory is handled.STALE_REF|<refname>— arefs/remotes/<name>/*ref whose<name>is not a configured remote, named in full ref form. Advisory only; no ref is ever pruned by this tool.CHILD_OF|<branch>|<ancestor>—branchis a git ancestor ofancestor, and both resolved exactlyNOT_MERGEDthrough their own full ladders. It is emitted alongside, never instead of, the branch's ownBRANCH|<branch>|NOT_MERGEDline, and names the containment relationship so an operator can see that the branch's work is not lost when the named ancestor is retained.WARN|registration-lost|<path>— a worktree directory whose.gitpointer file names a gitdir target that no longer exists. Git commands run inside such a directory fail in confusing ways. Advisory only.
End-to-End Workflow
Detect and report (dry run). Run
bash scripts/bash/cleanup-worktrees.sh(report mode is the default and mutates nothing). It verifies localmainagainstorigin/main(emittingWARN|main-divergenceon drift), enumerates branches and worktrees, and prints oneBRANCH|line per branch plusCOMMIT|...|UNIQUE|...records for each unique residual commit.Editorial triage of the cherry-pick candidates. Review each
COMMIT|...|UNIQUE|...record — this is the LLM-judgment boundary. Confirm editorially that the unique commits are genuinely documentation/agent-memory content (for example paths underdocs/**,.claude/agent-memory/**, or**/*.md). The script only reports the deterministic facts (SHA, paths, author, date); deciding what counts as documentation is this skill's responsibility.Consolidate onto
documentationandmemories. When the candidate list is non-empty, the script creates thedocumentationandmemoriesbranch offmainin a dedicated worktree (never the caller's worktree) and cherry-picks the flagged commits oldest-first per source branch, branches inLC_ALL=Corder, with-xprovenance. A pre-existingdocumentationandmemoriesbranch stops the run with a report — never reuse it silently. Conflicts are aborted and surfaced asCONFLICTfor editorial resolution, never auto-resolved.Push and hand off PR creation to
Agent(pr-author). Push the consolidation branch (git push). Refresh the PR-context bundle withmcp__drm-copilot__collect_pr_contextusing base branchmain(producingartifacts/pr_context.summary.txtandartifacts/pr_context.appendix.txt). Validate the orchestrator-state checkpoint (artifacts/orchestration/orchestrator-state.json) with--require-pr-creation-readyand record thepr_author_preflightresult; delegation is prohibited when that validation fails. Then delegate PR creation toAgent(pr-author)per.claude/skills/pr-author/SKILL.md, which owns the body-file and receipt contract; defer to that skill for it rather than restating any value from it here. For<N>, use the GitHub issue number the run executes under when the run has one. When the run has none,<N>is an arbitrary run-scoped identifier chosen by the pr-author agent. It is not a pull-request number, and the only requirement on it is that the body-file path, the receipt'snumberfield, and the body bytes all agree. This skill never authors or creates the PR itself.Wait for merge and verify git-natively. The consolidation PR's merge is human-performed: the operator merges it outside the agent session; this skill never issues the merge itself. This is why:
gh pr mergeis absent from this skill'sallowed-tools; the project permission allow-list (permissions.allowin.claude/settings.json) carries noghentry; and.claude/hooks/enforce-epic-merge-gate.ps1would deny the command withEPIC_MERGE_GATE_BLOCKEDif it were attempted. At this boundary the agent reports the consolidation pull request's URL or number to the operator and stops. The ruleset onmainsetsstrict_required_status_checks_policy, so the branch must be up to date withmainbefore the merge becomes available to the operator; the wait for the merge is unbounded within a session. After the consolidation PR merges, verify it withgit fetchfollowed bygit merge-base --is-ancestor documentationandmemories main. Exit 0 confirms every consolidated commit is now reachable frommain; that is the only state that unlocks deletion of branches whose unique content was consolidated.Run the apply-mode deletion. Run
bash scripts/bash/cleanup-worktrees.sh --apply. It re-verifies each candidate's ancestry/equivalence in-process, removes worktrees (without force; a dirty worktree is reported viaDIRTY|lines and skipped), then deletes branches withgit branch -D. The now-mergeddocumentationandmemoriesbranch and its worktree becomeMERGED_CLEANinstances and are cleaned up by the same mechanics. Any worktree left standing afterward — reportedBLOCKED-DIRTY, or whose branch classifiedNOT_MERGEDorHAS_UNIQUE_RESIDUALS— is not abandoned; it moves to the Dirty Worktree Triage Procedure below.A consolidation branch whose tip equals
main, which is the state ofdocumentationandmemoriesbetween its creation offmainand its first commit, is reportedNOT_ANCESTORby a tip-equality pre-check that runs before any network fetch and is therefore not delete-eligible; the post-merge cleanup described above is unaffected, because a merged consolidation branch's tip differs frommain, and an empty or unresolvable tip on either side is reportedANCESTRY_ERRORrather than treated as equality.Detached-HEAD worktrees are handled on the same terms. Apply mode classifies each on its own HEAD SHA, emits the five-field
WORKTREE|<path>|DETACHED|<state>|<flags>record from that verdict, and acts only on the delete-eligible statesMERGED_CLEAN,MERGED_CONTENT_NEUTRAL, andMERGED_EQUIVALENT. A delete-eligible detached worktree is re-verified in the same process immediately before the destructive action and is then removed without force. A locked one is skipped with the result tokenACTION|worktree-remove|<path>|BLOCKED-LOCKED; the locked test reads the porcelain flags already in hand and precedes every git invocation, so nogit worktree removecommand is issued for it. A prunable one is report-only: it produces its registration record and no further line, with no removal and no prune. A dirty one reaches the same non-forced removal path as a branch-backed worktree and is reportedBLOCKED-DIRTYwith its accompanyingDIRTY|lines. In apply mode a blocked detached removal —BLOCKED-DIRTY,BLOCKED-LOCKED, orBLOCKED-REVERIFY— sets a non-zero exit status, so a checkout holding dirty or locked detached worktrees exits non-zero from--applywhere the same checkout previously exited 0.
Nothing to Consolidate (Short Path)
When report mode classifies every candidate as MERGED_CLEAN or MERGED_EQUIVALENT
with an empty cherry-pick-candidate list, skip steps 3-5 entirely: proceed directly from
the report to bash scripts/bash/cleanup-worktrees.sh --apply. Cleanup completes in a
single session with no PR.
Dirty Worktree Triage Procedure
Trigger. A worktree reported ACTION|worktree-remove|<path>|BLOCKED-DIRTY (with
accompanying DIRTY|<path>|<status-porcelain-line> records), or a branch classified
NOT_MERGED or HAS_UNIQUE_RESIDUALS, carries uncommitted or unmerged content the
script correctly refuses to discard. That refusal is correct and this procedure never
overrides it — a dirty worktree is never force-removed. This procedure is the systematic
follow-up: deciding, per worktree, whether that content is disposable or must be
preserved before the worktree can ever be deleted. Report mode now precedes this
procedure with a machine-readable first pass: one DIRTFILE| record per status entry
giving that entry's verdict, and one DIRTSUM| record per worktree giving the aggregate
ALL_DISPOSABLE or HAS_UNIQUE. Steps 1-9 below apply to the worktrees carrying at
least one UNIQUE verdict, which are exactly the ones the classifier could not establish
as disposable and which therefore still require human editorial judgement.
Steps 1-7 are read-only investigation. Run them per worktree, or fan out one
Agent(general-purpose) investigation per worktree (or small batch) concurrently per
step 8, each returning a SAFE_TO_DELETE / PRESERVE verdict with justification citing
specific files or commit SHAs, before step 9 acts on any finding.
Re-verify current state before analyzing. Worktrees can be actively in use by another concurrent session. Re-run
git status --porcelainin the worktree and re-check the branch's merge status fresh — do not reuse the original scan's snapshot. If the worktree's.git/index/HEAD mtimes show activity in the last few minutes, treat it as possibly live and pause rather than analyze it as abandoned.Check committed-but-unmerged commits, not only the working tree. Run
git log main..<branch> --oneline. Some worktrees carry real commits that never merged, separate from uncommitted working-tree changes. Both need the classification in step 5.Check for equivalent content already on
main, by topic, not only by path. For every dirty, untracked, or unmerged file, checkgit show main:<path>at the same path, and also grep broadly across the relevant shared namespace (for example.claude/agent-memory/**for lesson files,docs/features/**for feature docs) since the same fact is often re-recorded under a different filename onmain.For feature-folder doc snapshots (
issue.md,plan.md,spec.md,research/*), check whether the feature is fully closed onmain— acceptance criteria all checked, code-review/feature-audit/policy-audit artifacts present, an evidence trail present. An earlier draft of an already-closed feature is almost always fully superseded; diff it against the closed feature's final artifacts to confirm rather than assume.Classify any content that is not obviously superseded into exactly one of:
DEAD_ONE_OFF— real, but tied to an already-executed, closed plan with no reuse elsewhere (check whether the same pattern appears in shared.claude/skills/**templates or in other feature plans). Low value; safe to discard even though it is not technically duplicated.ALREADY_SOLVED_ELSEWHERE— the underlying problem it documents is fixed a different way onmain(checkmain's current code/config/script, not only its memory files — a memory file can describe a bug that no longer exists).STALE_OR_CONTRADICTED—main's current version of the same lesson has since been corrected to state something different or opposite. This is not merely redundant; it is actively wrong, and discarding is the right call.GENUINELY_NEW/STILL_RELEVANT— not found anywhere else, or it corrects somethingmaincurrently gets wrong, or it documents unresolved scope on a still-open issue (verify open/closed withgh issue view <n>; never assume). Must be preserved before the worktree is deleted.
Handle non-memory dirty content on its own terms. Some worktrees carry stale build artifacts (a modified
.csproj/packages.config/app.configfrom a build run in that worktree) rather than documentation. Diff a representative sample againstmain(git diff main -- <path>) to characterize the change before deciding it is disposable. The classifier scopes this class narrowly: it labels a project fileDISPOSABLE_BUILD_ARTIFACTonly when every changed line in that entry's diff is an analyzerHintPathrewrite. A project file whose diff touches anything else — aCompile Includeregistration, a package reference, a target or property change — is reportedUNIQUEand is not disposable, so this step's manual characterization is still required for it.Recognize orphaned non-worktree directories. A path can still exist on disk under a worktree-tracking folder after
git worktree removepartially ran or failed, with no.gitfile inside and no entry ingit worktree list. These are no longer worktrees — flag them for plain filesystem removal, notgit worktree remove, which will misfire or no-op on them. Filesystem removal of an orphaned directory is a destructive action outside this skill's pre-approved tool surface; it requires explicit user confirmation each time, the same as any other irreversible delete.Parallelize the triage. Steps 1-7 are pure read-only investigation. Fan out one
Agent(general-purpose)investigation per worktree (or a small batch) concurrently, each following steps 1-7 and returning a structuredSAFE_TO_DELETE/PRESERVEverdict with justification. This scales far better than triaging serially.Route
PRESERVEfindings through the existing consolidation flow (thedocumentationandmemoriesbranch/PR mechanism in steps 3-4 of the End-to-End Workflow above) before that worktree's dirty content is discarded. If a finding describes unresolved product scope rather than a process lesson, promote it to a real follow-up issue instead of folding it into the docs/memory PR: file it withmcp__drm-copilot__new_potential_bug_entryand promote withmcp__drm-copilot__potential_to_issueper.claude/skills/feature-promotion-lifecycle/SKILL.md. For aSAFE_TO_DELETEverdict, discard the content as a distinct, individually confirmed manual action — clear the dirty working tree, delete a disposableNOT_MERGED/HAS_UNIQUE_RESIDUALSbranch directly, or remove the worktree itself through a manifest-authorized removal. A manifest-authorized removal is a singlegit worktree remove <path>covering one worktree, issued as its own Bash tool call, one call per worktree, and it is authorized only when the Sanctioned Removal Manifest below carries a record for that exact path whoseremoval_dispositionisSAFE_TO_DELETEand whosebranch_stateisNOT_MERGEDorHAS_UNIQUE_RESIDUALS. Never pass a force flag to that command: a dirty worktree blocks deletion and is reported for manual handling, and it is never force-removed. This is never automated: the script's classification ladder and apply-mode allowlist are never changed to accept these states, so a--applyrun never deletes them on its own, before or after triage. If discarding the working-tree content changes the branch's classification (for example to content-neutral againstmain), a follow-up report/apply pass then picks it up through the normal deterministic path.Distinguish two different discards. The sentence above governs the editorial discard of
UNIQUEcontent — content a human judged safe to lose — and that discard stays manual and individually confirmed. Separately, dirt the classifier established as disposable, meaning a worktree whoseDIRTSUM|aggregate isALL_DISPOSABLE, can be cleared by the tool itself under the opt-in--clear-disposableflag in apply mode. That path never touches aUNIQUEverdict: oneUNIQUEentry refuses the clear for the whole worktree, and a classification read that errors produces a fail-closedUNIQUEwith the same effect. It is also not force-removal — it clears the working tree and retries the same unforcedgit worktree removeafter a fresh in-process re-verification. The classification ladder and the apply-mode allowlist remain unchanged by it.After local branch deletion, check origin too. This skill is local-only by design (see "When to Use This Skill"), which leaves stale branches on the remote for anything already merged. After
--applyfinishes, diff the deleted-local-branch list againstgit branch -r(post-prune) to find remote branches whose local counterpart is gone, and offer to delete the remainder on origin. Because this mutates shared, visible remote state, each deletion requires explicit user confirmation — never delete an origin branch as an automatic consequence of local cleanup, and never rely on this skill's generalBash(git push *)allowance to perform it silently.
Sanctioned Removal Manifest
Write the manifest before step 9 of the Dirty Worktree Triage Procedure acts on any
SAFE_TO_DELETE verdict. The run records its triage verdicts in
artifacts/orchestration/cleanup-worktrees-manifest.json. The two PreToolUse gates
.claude/hooks/enforce-epic-worktree-removal-gate.ps1 and
.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 read that document and admit a
git worktree remove <path> command only when a record in it covers that exact path. A removal
issued with no covering record is denied with each gate's existing, unchanged reason code.
The document is UTF-8 JSON with snake_case keys. It carries six top-level fields. Its removals
and preserved_files entries are sibling arrays and are never nested: removal records are keyed by
worktree path, preserve records by the pair of worktree path and file path, and the gates must
never traverse preserve data.
Top-level fields
tool— string, required, exactlycleanup-merged-worktrees. The self-identifying discriminator. Absent, non-string, or any other value and no record authorizes anything.schema_version— integer, required, exactly1. Absent, non-integer, or any other value fails closed; a forward version is not accepted by silence.generated_at— string, required, an ISO-8601 UTC timestamp recording when the run wrote the document. Absent, unparseable, in the future, or older than the 24-hour freshness bound fails closed.run_id— string, required, non-empty. An opaque per-run identifier for audit correlation.removals— array, required, of removal records. It may be empty; absent, non-array, or empty authorizes no removal.preserved_files— array, required, of preserve records. It may be empty. Neither gate ever reads it, and its absence or malformation must not affect any gate decision.
removals[] record fields
worktree_path— string, required, non-empty. The absolute path of the worktree to remove. A record with it absent or empty is skipped.branch— string ornull, key required. The worktree's branch, ornullfor a detached worktree. An absent key means the record does not authorize;nullis a valid, meaningful value.branch_state— string, required. The classification the report emitted for that branch: one ofMERGED_CLEAN,MERGED_CONTENT_NEUTRAL,MERGED_EQUIVALENT,NOT_MERGED,HAS_UNIQUE_RESIDUALS, orPROTECTED_CURRENT. OnlyNOT_MERGEDandHAS_UNIQUE_RESIDUALSauthorize a removal; every other member, and any value outside the vocabulary, does not.removal_disposition— string, required. The triage disposition that authorizes removal. The allowed set is exactly the single memberSAFE_TO_DELETE. This key is deliberately distinct from themerge_statuskey the orchestration checkpoints carry.verdict— string, required. The step-5 content classification that produced the disposition: one ofDEAD_ONE_OFF,ALREADY_SOLVED_ELSEWHERE,STALE_OR_CONTRADICTED,GENUINELY_NEW, orSTILL_RELEVANT. Absent, out of vocabulary, or preserve-implying and the record does not authorize.evidence— string, required, non-empty. The justification step 9 requires, citing specific files or commit SHAs. Absent, empty, or whitespace-only and the record does not authorize.
preserved_files[] record fields
These records serve the consolidation consumer and are never read by either gate.
worktree_path— string, required, non-empty. The worktree holding the file, and where the consumer reads that worktree'sMEMORY.md.source_path— string, required, repo-relative within that worktree. The file to preserve.change_class— string, required, eitheruntrackedormodified, recording whether staging adds a new file or carries a working-tree modification.disposition— string, required, exactlyPRESERVE.verdict— string, required, a member of the step-5 verdict vocabulary listed above.target_path— string, required, repo-relative on the consolidation branch. Required because the same relative path may already exist onmainwith different content and a lesson file may be re-namespaced; the consumer must not guess it.memory_index_line— string ornull, key required. The source worktree'sMEMORY.mdindex line, ornullwhen the file is not a memory entry.line_ending— string, required, one ofcrlf,lf, orabsent. Advisory only: it records the target file's existing convention, and the consumer re-derives and compares rather than trusting it.host_token_scan— object, required, carrying aresultmember and apattern_set_idmember. It records the scan outcome and the identifier of the pattern set used, never the patterns themselves.evidence— string, required, non-empty.
Example
{
"tool": "cleanup-merged-worktrees",
"schema_version": 1,
"generated_at": "2026-09-07T03:40:00Z",
"run_id": "cleanup-2026-09-07T03-40-00Z-a47a5e33",
"removals": [
{
"worktree_path": "C:/Users/DanMoisan/repos/drm-copilot/.claude/worktrees/agent-0f1c2d",
"branch": "drm-copilot-wt-2026-08-14T09-02",
"branch_state": "HAS_UNIQUE_RESIDUALS",
"removal_disposition": "SAFE_TO_DELETE",
"verdict": "ALREADY_SOLVED_ELSEWHERE",
"evidence": "Unique residual commit 3f9a1c2 records the cleanup-worktrees ancestry error; main already fixes it at scripts/bash/cleanup_worktrees_lib.sh:214-231 under issue #612."
}
],
"preserved_files": [
{
"worktree_path": "C:/Users/DanMoisan/repos/drm-copilot/.claude/worktrees/agent-91ee43",
"source_path": ".claude/agent-memory/general-purpose/hook-payload-anomaly.md",
"change_class": "untracked",
"disposition": "PRESERVE",
"verdict": "GENUINELY_NEW",
"target_path": ".claude/agent-memory/general-purpose/hook-payload-anomaly-envelope.md",
"memory_index_line": "- [Hook payload anomaly envelope](hook-payload-anomaly-envelope.md) - the deny path a malformed envelope takes",
"line_ending": "crlf",
"host_token_scan": {
"result": "clean",
"pattern_set_id": "child-f-host-tokens-v1"
},
"evidence": "No equivalent file on main under .claude/agent-memory/**; grep for 'payload anomaly' returns only this worktree."
}
]
}
Accepted residual
Both gates read the command text of the Bash tool call they are given. A removal routed
indirectly — for example by writing the command into a file and invoking that file with bash <file> — presents command text the gates do not match, so the manifest requirement does not reach
it. This design does not close that indirection, and the requirement is recorded here as an
accepted residual rather than left implicit.
The manifest requirement is a policy-level integrity check, on the same terms
.claude/hooks/enforce-pr-author-skill.ps1 records for its own receipt mechanism: it prevents
accidental bypass and requires a deliberate, documented act to circumvent. It is not a
cryptographic or security boundary, and it must not be described as tamper-proof. Routing a
removal through such an indirection in order to avoid the manifest requirement is a deliberate act
and is prohibited by this skill, which is the term the residual rests on.
Prohibited Shortcuts
- Never invoke
gh pr createorgh pr edit --body*from this skill or the scripts. PR authoring isAgent(pr-author)'s exclusive responsibility and is enforced by theenforce-pr-author-skill.ps1PreToolUse hook. - Never pass a force flag to
git worktree remove. A dirty worktree blocks deletion and is reported for manual handling; it is never force-removed. The--clear-disposableflag is not an exception to this rule and is not force-removal: it clears the working tree first and then retries the same unforcedgit worktree remove, so the removal it retries is the identical unforced call that was blocked. It runs only when every per-file verdict for that worktree is non-UNIQUE, only in apply mode, only when the operator explicitly requests it, and only after a fresh in-process re-verification that the branch is still delete-eligible. A singleUNIQUEverdict refuses the clear for the whole worktree, including the fail-closedUNIQUEassigned when a classification read errors. - Never execute
git worktree prune. Prunable registrations are report-only. - Never act on
NOT_MERGED,HAS_UNIQUE_RESIDUALS, orPROTECTED_CURRENTcandidates through the script or its apply-mode allowlist;--applynever mutates them, and the caller's worktree and branch, and the main worktree, are never mutated under any disposition. The Dirty Worktree Triage Procedure'sSAFE_TO_DELETEverdict authorizes only a distinct, individually confirmed manual action outside that automated path forNOT_MERGED/HAS_UNIQUE_RESIDUALS— never a change to the classification ladder itself, and never forPROTECTED_CURRENT. - Never use commit-message text matching as a classification input, and never auto-resolve cherry-pick conflicts.
- Never delete an origin branch, or run plain filesystem removal on an orphaned worktree-tracking directory, without explicit per-item user confirmation — both are outside this skill's pre-approved tool surface regardless of how the triage verdict came out.
- Never issue the consolidation merge command, and never write or edit an orchestration
checkpoint in order to satisfy
.claude/hooks/enforce-epic-merge-gate.ps1. This forecloses the specific evasion of writing anartifacts/orchestration/orchestrator-state.jsonwhoseepic_modeandstep9_statusfields the gate's child-feature accept shape would honour for any pull-request number. - Never widen the definition of disposable dirt. The session-artifact list is a fixed
in-script array of exactly three paths with no configuration override, no environment
variable, and no per-repository extension point; adding a path to it is a change to the
classifier, not a setting. The build-artifact rule requires both conditions together —
the
*.csprojpath pattern and the content confinement that every changed line in the entry's diff is an analyzerHintPathrewrite — and a project file whose diff touches anything else isUNIQUE. Ignored files are never cleared, because they are never classified:git status --porcelainis read without--ignored, so an ignored file never becomes an entry, andgit cleanis never given-x,-X, or-ff.
Cross-References
.claude/skills/pr-author/SKILL.md— the PR body/receipt contract and the delegation target for step 4..claude/skills/pr-context-artifacts/SKILL.md— how the PR-context bundle is collected and the base-branch resolution rules..claude/rules/shell.md— the bash toolchain (shfmt/shellcheck/bats/kcov), the 500-line cap, the no-temp-files test policy, and theCLEANUP_WT_GIT_BINseam convention..claude/skills/feature-promotion-lifecycle/SKILL.md— the potential-entry-to-issue promotion path used by the Dirty Worktree Triage Procedure's step 9 forPRESERVEfindings that describe unresolved product scope..claude/hooks/enforce-epic-merge-gate.ps1— the PreToolUse gate on the consolidation merge command, backed by orchestration checkpoints. A cleanup run satisfies none of its three checkpoint shapes: it is neither a per-feature orchestration, nor an epic integration, nor a parallel run, so it writes none of the three checkpoints the gate reads.