You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
chore(process): .session/ holds 28 handoff files and none match a live branch — the start-protocol step reads a graveyard #13848
.session/ holds 28 tracked HANDOFF-*.md files. Zero of them correspond to a branch that still exists:
$ for f in .session/HANDOFF-*.md; do
b=$(basename "$f" .md | sed 's/^HANDOFF-//')
git ls-remote --heads origin "$b" | grep -q . && echo "LIVE: $b"
done
handoff files: 28 | for branches that still exist: 0
They span roughly two months (oldest HANDOFF-chore-triage-umbrellas.md, June 11) and are all committed to the base branch.
Why this is a real cost, not clutter
The session-start protocol makes reading them mandatory:
Read predecessor handoffs: check .session/ directory on the base branch for HANDOFF files from unmerged sessions touching your area. If one exists and its branch is unmerged, decide: continue their branch (rebase it onto base first) or start fresh — never duplicate their work blind.
That step currently has a 100% false-positive rate. A new session doing it correctly reads 28 dead files to find zero live ones. The predictable outcome is that sessions stop doing the step — and then the one handoff that does matter gets missed, which is the exact failure the mechanism exists to prevent ("Duplicate work on the same problem").
The protocol has a disposal rule for worktrees ("the next session's start-protocol removes it after the branch merges") but no equivalent for handoff files. Nothing reaps them, so the signal-to-noise ratio only degrades.
Fix
Give handoff files the same lifecycle the protocol already gives worktrees: once a handoff's branch is merged or gone, the handoff has served its purpose and should be removed. Options, cheapest first:
Reap at session start — the existing sweep step already enumerates branches; deleting a handoff whose branch no longer exists is the same check.
Reap at merge — a workflow step on merge to the base branch removes .session/HANDOFF-<branch>.md.
Stop tracking them — if handoffs are only ever read by the next session on the same machine, .gitignore them; but this loses cross-machine handoff, which is the point.
Option 1 or 2. This is disposal of landed work, not deletion of unfinished work: each of these 28 describes a branch whose work is merged, so the file has no remaining reader.
Worth confirming before bulk removal that none of the 28 documents a status: blocked handoff whose work never landed — a blocked handoff for a deleted branch is stranded work, and that one should become an issue rather than be deleted.
Acceptance criteria
A handoff file is removed automatically once its branch is merged or deleted.
The existing 28 are triaged: merged → removed; any status: blocked with unlanded work → filed as an issue first, then removed.
The session-start protocol step is accurate again — files present in .session/ correspond to live, unmerged branches.
Verified by deliberate failure: create a handoff for a branch, delete the branch, confirm the reaper removes the file; create one for a live branch and confirm it survives.
Triage complete: all 32 handoffs record landed work. Reaper opened as #15034 — leaving this open
Worked as part of the stranded-work sweep on umbrella #14078.
The count is 32, not 28
.session/ holds 32 tracked HANDOFF-*.md files plus README.md; four arrived after this issue
was written. Confirmed: zero name a branch that still exists locally or on origin.
The check this issue asked for before any removal
Worth confirming before bulk removal that none of the 28 documents a status: blocked handoff
whose work never landed.
Done, all 32. None is blocked. Thirty-one are complete (some with a scope qualifier); one, HANDOFF-issue-12647.md, is partial.
Four handoffs name no PR, so they were checked against the base branch directly — these were the
candidates for stranded work, and none of them is:
Handoff
What it claimed
Verified in base
issue-10008
pr: DEFERRED — open PR queue at 6 (project limit 5); branch ready + mergeable
landed anyway: ConnectionStatus(str, Enum) is at autobot_shared/status_enums.py:494, tagged (#10008), and services/ai_stack_client.py references it 10 times
issue-14111-stable-shard
pr: (none yet), remaining: decide whether ci.yml adopts this; it is inert until something passes --shard-splits
the decision was taken: .github/workflows/ci.yml runs -p repo_tests.stable_shard --shard-splits 12 --shard-group … at lines 283-284 and 326, and repo_tests/stable_shard.py exists. Not inert
PR #13163 MERGED, issue #12647 CLOSED, and its three sibling slice handoffs (-org, -team, -user) all record complete — -org states explicitly that it is the last de-fork file
Two referenced issues remain open, #13623 and #13685, but both are umbrellas whose child PRs
merged; their handoffs have no remaining reader either.
Nothing in .session/ is stranded work. All 32 are disposable.
Took option 1 (reap at session start). scripts/cleanup-worktrees.sh already runs at session start
and already answers "is this branch gone?" three times; a fourth phase costs one source line and
reuses a sweep operators already trust. Option 2 could not see deleted branches, only merged ones;
option 3 loses cross-machine handoff.
The disposal rule is deliberately narrower than "branch gone":
Handoff
Reaper does
branch still exists (local or origin)
keeps it — it still has a reader
branch gone, status: complete
removes it
branch gone, status: blocked / partial / no parseable status
keeps it and prints STRANDED
STRANDED is the deliberate dead end: work that never landed and whose branch is gone becomes an
issue, not a deletion. A handoff the reaper cannot parse is never a handoff it may delete.
Ships with scripts/lib/session-handoffs_test.sh — 22 hermetic assertions against a throwaway repo
under mktemp -d, including the deliberate-failure case AC4 asks for (handoff for a live branch
survives a sweep; delete the branch; next sweep reaps it) — registered in repo_tests/shell_lib_test.py
so CI runs it. .session/README.md and the CLAUDE_WORKFLOW.md start protocol are updated to match.
Why this is still open
The PR ships the mechanism; it does not hand-delete the 32. That is deliberate — the reaper's first
run against real data is the proof it works, whereas hand-deleting the files and shipping an
untested reaper proves nothing.
Remaining before this closes:
AC2 — the existing handoffs triaged; all landed, none blocked, evidence above
Delivered by PR #15034, merged to Dev_new_gui as 627dca93e. Evidence below is read from the merged base, not from the PR branch.
#
Criterion
Evidence
1
A handoff is removed automatically once its branch is merged or deleted
scripts/lib/session-handoffs.sh defines handoff_disposition (keep-live / keep-unknown / keep-unlanded / reap) and reap_session_handoffs; scripts/cleanup-worktrees.sh:275-283 wires it as Phase 4 of the start-of-session sweep, honouring --dry-run. "Automatic" here means the next session's sweep, which is Option 1 as this issue framed it
2
The existing handoffs triaged: merged → removed; unlanded → issue first, then removed
git ls-tree origin/Dev_new_gui .session/ now returns only README.md — all 32 gone. Landing was proven per file before deletion, not assumed from "branch is gone": 30 via a merged PR from that exact branch, and the two slash-named ones via #10033 and #9932 directly, since --head cannot match a dash-flattened filename
3
The session-start protocol step is accurate again
Follows from 2 — zero stale files remain, so "read predecessor handoffs" no longer has a 100% false-positive rate
4
Verified by deliberate failure
scripts/lib/session-handoffs_test.sh builds a throwaway repo per run and asserts both directions: a live branch keeps its handoff, and deleting that branch then re-sweeping reaps it. passed=34 failed=0. Registered in CI via repo_tests/shell_lib_test.py, which also fails if a new *_test.sh is not added to SHELL_SUITES
The one file the reaper refused to delete is why criterion 2 reads the way it does.HANDOFF-issue-12647.md said status: partial; #12647 is closed; four of its user_management files are still forked across both backends. That is real unfinished work whose only record was a file nothing reads — precisely the failure this issue describes. Filed as #15068 with the drift table and the carried-over traps, and only then removed. "Blocked → issue first, then removed" was followed literally.
Three defects found in review and fixed before merge, each with a contrast mutation proving the new tests are not vacuous:
grep -m1 '^status:' matched the first such line anywhere in the file — and .session/README.md documents the schema as a literal status: complete | blocked | partial line, so a handoff written by pasting the template parsed as complete and was reaped even when it said blocked. Parsing is now scoped to the leading key: value block, skips fenced blocks, and refuses to guess when two status: lines compete. Reverting to grep -m1 fails 4 named assertions.
git show-ref exits 1 for a missing ref and ≥2 for a hard error; collapsing both into "branch gone" let one broken git call reap the whole directory. Now a third outcome, keep-unknown; and git fetch --prune ... || true no longer swallows failure — Phase 4 refuses to run on stale refs. Collapsing the exit code back fails 3 assertions, including unresolvable branch is never reaped flipping to reap.
A filename differing only in case from its branch read as "gone". Fixed with a loose second pass over for-each-ref.
Closing: all four criteria met with code evidence from the merged base.
Activity
mrveiss commentedon Aug 25, 2026
Triage complete: all 32 handoffs record landed work. Reaper opened as #15034 — leaving this open
Worked as part of the stranded-work sweep on umbrella #14078.
The count is 32, not 28
.session/holds 32 trackedHANDOFF-*.mdfiles plusREADME.md; four arrived after this issuewas written. Confirmed: zero name a branch that still exists locally or on
origin.The check this issue asked for before any removal
Done, all 32. None is
blocked. Thirty-one arecomplete(some with a scope qualifier); one,HANDOFF-issue-12647.md, ispartial.Every named PR is MERGED: #9932, #10033, #10072, #10077, #10112, #10125, #10433, #10482, #11068,
#12620, #13163, #13164, #13666, #13693, #13731, #13735, #13752, #13760, #14032, #14338, #14347.
Four handoffs name no PR, so they were checked against the base branch directly — these were the
candidates for stranded work, and none of them is:
issue-10008pr: DEFERRED — open PR queue at 6 (project limit 5); branch ready + mergeableConnectionStatus(str, Enum)is atautobot_shared/status_enums.py:494, tagged(#10008), andservices/ai_stack_client.pyreferences it 10 timesissue-14111-stable-shardpr: (none yet),remaining: decide whether ci.yml adopts this; it is inert until something passes --shard-splits.github/workflows/ci.ymlruns-p repo_tests.stable_shard --shard-splits 12 --shard-group …at lines 283-284 and 326, andrepo_tests/stable_shard.pyexists. Not inertissue-12924,issue-12925pr:fieldissue-12647(partial)-org,-team,-user) all recordcomplete—-orgstates explicitly that it is the last de-fork fileTwo referenced issues remain open, #13623 and #13685, but both are umbrellas whose child PRs
merged; their handoffs have no remaining reader either.
Nothing in
.session/is stranded work. All 32 are disposable.The mechanism — PR #15034
Took option 1 (reap at session start).
scripts/cleanup-worktrees.shalready runs at session startand already answers "is this branch gone?" three times; a fourth phase costs one
sourceline andreuses a sweep operators already trust. Option 2 could not see deleted branches, only merged ones;
option 3 loses cross-machine handoff.
The disposal rule is deliberately narrower than "branch gone":
origin)status: completestatus: blocked/partial/ no parseable statusSTRANDEDSTRANDEDis the deliberate dead end: work that never landed and whose branch is gone becomes anissue, not a deletion. A handoff the reaper cannot parse is never a handoff it may delete.
Ships with
scripts/lib/session-handoffs_test.sh— 22 hermetic assertions against a throwaway repounder
mktemp -d, including the deliberate-failure case AC4 asks for (handoff for a live branchsurvives a sweep; delete the branch; next sweep reaps it) — registered in
repo_tests/shell_lib_test.pyso CI runs it.
.session/README.mdand theCLAUDE_WORKFLOW.mdstart protocol are updated to match.Why this is still open
The PR ships the mechanism; it does not hand-delete the 32. That is deliberate — the reaper's first
run against real data is the proof it works, whereas hand-deleting the files and shipping an
untested reaper proves nothing.
Remaining before this closes:
.session/down toREADME.mdplus live-branch handoffsrepo_tests/shell_lib_test.py::test_shell_suite_passes[scripts/lib/session-handoffs_test.sh]fix(session): stop the handoff reaper deleting work that never landed (…
fix(session): a case-mismatched handoff filename must not read as a d…
chore(session): sweep the 32 handoffs whose branches have all merged (#…
chore(session): reap handoff files whose branch is gone, keep the unl…
fix(session): stop the handoff reaper deleting work that never landed (…
fix(session): a case-mismatched handoff filename must not read as a d…
chore(session): sweep the 32 handoffs whose branches have all merged (#…
chore(session): reap handoff files whose branch is gone, keep the unl…
fix(session): stop the handoff reaper deleting work that never landed (…
fix(session): a case-mismatched handoff filename must not read as a d…
chore(session): sweep the 32 handoffs whose branches have all merged (#…
chore(session): reap handoff files whose branch is gone, keep the unl…
mrveiss commentedon Aug 25, 2026
Delivered by PR #15034, merged to
Dev_new_guias627dca93e. Evidence below is read from the merged base, not from the PR branch.scripts/lib/session-handoffs.shdefineshandoff_disposition(keep-live/keep-unknown/keep-unlanded/reap) andreap_session_handoffs;scripts/cleanup-worktrees.sh:275-283wires it as Phase 4 of the start-of-session sweep, honouring--dry-run. "Automatic" here means the next session's sweep, which is Option 1 as this issue framed itgit ls-tree origin/Dev_new_gui .session/now returns onlyREADME.md— all 32 gone. Landing was proven per file before deletion, not assumed from "branch is gone": 30 via a merged PR from that exact branch, and the two slash-named ones via #10033 and #9932 directly, since--headcannot match a dash-flattened filenamescripts/lib/session-handoffs_test.shbuilds a throwaway repo per run and asserts both directions: a live branch keeps its handoff, and deleting that branch then re-sweeping reaps it.passed=34 failed=0. Registered in CI viarepo_tests/shell_lib_test.py, which also fails if a new*_test.shis not added toSHELL_SUITESThe one file the reaper refused to delete is why criterion 2 reads the way it does.
HANDOFF-issue-12647.mdsaidstatus: partial; #12647 is closed; four of itsuser_managementfiles are still forked across both backends. That is real unfinished work whose only record was a file nothing reads — precisely the failure this issue describes. Filed as #15068 with the drift table and the carried-over traps, and only then removed. "Blocked → issue first, then removed" was followed literally.Three defects found in review and fixed before merge, each with a contrast mutation proving the new tests are not vacuous:
grep -m1 '^status:'matched the first such line anywhere in the file — and.session/README.mddocuments the schema as a literalstatus: complete | blocked | partialline, so a handoff written by pasting the template parsed ascompleteand was reaped even when it saidblocked. Parsing is now scoped to the leadingkey: valueblock, skips fenced blocks, and refuses to guess when twostatus:lines compete. Reverting togrep -m1fails 4 named assertions.git show-refexits 1 for a missing ref and ≥2 for a hard error; collapsing both into "branch gone" let one broken git call reap the whole directory. Now a third outcome,keep-unknown; andgit fetch --prune ... || trueno longer swallows failure — Phase 4 refuses to run on stale refs. Collapsing the exit code back fails 3 assertions, includingunresolvable branch is never reapedflipping toreap.for-each-ref.Closing: all four criteria met with code evidence from the merged base.