Prune merged local branches — list local branches already merged on remote
(the : gone] upstream set), excluding the current branch and the default
(main). Delete a listed branch only after an explicit per-branch confirmation;
declining leaves it in place.
A : gone] branch that was squash-merged will typically refuse plain
git branch -d — its upstream is already pruned, so -d has nothing to test
reachability against. Before reporting that as a stop, check for a merged PR
record: gh pr list --head <branch> --state merged --json headRefOid. When a
MERGED PR's headRefOid equals that branch's local tip, containment is
proven and git branch -D is permitted — with the proof stated and the
branch named in the confirmation — mirroring the post-merge-cleanup Phase 5
contract. Without that proof, a refusal stays a report-and-stop; never guess.
Archival sweep — proposed per item, never batched (B-24). Long-done tasks
accumulate on the board; they are already excluded from the in-flight count.
List dated done items strictly more than 14 calendar days old
(ARCHIVE_CUTOFF_DAYS), then ask about each one
separately, noting which ones the user says yes to.
An item marked [x] with no (done YYYY-MM-DD) stamp cannot be aged, so
it is never in the proposed set. Offer it only if the user asks, and only with
--allow-undated — both $ca-task done and the board classifier require
the stamp, so an unstamped entry is legacy or override-era and its real age is
unknown.
Declining is always available and costs nothing: an unarchived task stays
exactly where it is. Never archive without a yes.
If at least one item was confirmed: an archive is calendar-driven hygiene,
not a consequence of any in-flight work, so it never rides another commit.
Before creating the archive branch, require a clean working tree and index
(git status --porcelain emits nothing); otherwise STOP before mutating the
board. Then switch to the configured default branch, fetch it and fast-forward
it with step 1's --ff-only pull, and STOP on checkout, fetch, divergence, or
pull failure. Verify that HEAD is the fetched default tip. Only then create and
switch to a fresh dedicated branch with non-overwriting git switch -c, e.g.
chore/board-archive-<YYYY-MM-DD>-<HHMMSSZ>. Branch creation MUST fail closed
if the branch name already exists; never reuse or reset an earlier sweep's
branch. Verify the new branch is current and still points at the fetched
default tip before the first archive helper call.
This ordered isolation is what makes the sweep committable at all: writing
straight to disk on whatever branch happens to be current would either land
on main (refused outright) or mix into an unrelated feature branch's staged
work (a type split under commit-gate Phase 3) — see #573. One fresh branch per
sweep, never reused across sessions.
On that branch, archive each confirmed item, still one at a time:
"$PY" "${PLUGIN_ROOT}/hooks/taskwrite.py" archive <id>.
One confirmation per item, one helper call per item — the two map 1:1 on
purpose. A batched "archive all 12?" turns twelve decisions into one, and the
helper's own per-item ordering (append to done-tasks.md first, then remove
from open-tasks.md) is what makes an interrupted sweep recoverable; a batch
loop that answered once would throw that away.
Once every confirmed item is archived, stage .codearbiter/open-tasks.md and
.codearbiter/done-tasks.md — the very first archive ever run creates
done-tasks.md; that new file is expected here, not scope creep — and hand
off to $ca-chore docs, which exits through commit-gate (classification
docs) and finishing-a-development-branch as usual. .codearbiter/ prose
is already a listed docs chore type.