fix(#66)(#64): stale-handoff warning + budget onboard's compact-restart output - #74
Merged
Merged
Conversation
Every handoff-file design shares one blind spot by construction: the file only reflects what was written, so it can look current while work has moved on without it. This project's own HANDOFF.md went 5+ weeks stale this way (nested-workspace gap). session-onboard.sh already reads HANDOFF.md's "Last Updated" line and already shells out to git for live state - comparing the two turns the silent failure into a warning. Compares HANDOFF.md's Last-Updated date against the branch's latest commit date via a portable civil-calendar day-count in awk (no `date -d`/`date -j`, which differ between GNU and BSD date and would need separate handling for Windows Git Bash per #67). Warns past a 14-day gap; silent when the gap is smaller, absent, or negative (an in-flight, uncommitted-state handoff ahead of the last commit is normal). Skipped on a `compact` re-fire, matching the gitignore nudge's own reasoning. fix(#64): budget session-onboard.sh's compact-restart output A realistic (not pathological) compact-restart scenario - a 30-line near-max-width buffer tail plus a normal HANDOFF.md and ~20 untracked files - measured at 11,026 characters, over the undocumented ~10,000-character cap a competing plugin (adrrr/persistent-handoff) claims Claude Code enforces on SessionStart output. Claude Code's own docs specify no limit at all, so the exact cutoff is unverified, but an unbounded worst case is worth budgeting against regardless. Two changes: TL_COMPACT_TAIL_LINES/TL_COMPACT_TAIL_LINE_CHARS shrunk from 30/300 to 20/200, and the buffer-tail inline (the one block with no fallback pointer... wait, it has one - "full history is at <path>" - unlike live git state, which has none) now renders LAST in the script instead of before the unconsumed-buffer scan and live git state. If truncation happens at all, it now eats into the block that already tells you where to find the rest, not into small bounded orientation content with no such fallback. Same worst-case fixture measured at 5,818 characters after both changes - roughly half - with live git state confirmed intact and preceding the tail inline. Adds 5 new assertions to tests/run.sh (167 -> 171 passing) and updates one existing assertion for the new tail bound (30 -> 20 lines). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXL2ffVDsAp4P7387Nr18G
…rbage /review-pr on PR #74 diffed the days_from_civil awk function against Python's calendar for every date from 1899-01-01 to 2100-12-31 (73,779 dates, zero mismatches) - the arithmetic itself was right - but found the regex extraction only validated that hf_date/commit_date were digit-shaped, not that they were real calendar dates. A hand-edited "**Last Updated:** 2026-00-00" computed a confident wrong answer ("275 day(s) before") instead of the "say nothing when unparseable" this check already promises elsewhere. days() now range-checks month/day/leap-February itself and sets a `bad` flag rather than returning a number for garbage input. Also (same review): `head -20` on `git status -s` bounds live git state's LINE COUNT but not each line's WIDTH - one modified tracked file's path can be arbitrarily long regardless of `head`, and a 20-modified-file monorepo fixture with ~8-segment paths measured over 2KB with nothing bounding it. Since issue #64 moved this block to render first specifically because it was "small and bounded", it needed the same per-line truncation discipline the buffer-tail inline already has. Added TL_GIT_STATUS_LINE_CHARS=200 with the same awk truncation pattern. Also updates CHANGELOG.md (this PR and the already-merged #73 docs/demo/ Related-work PR both landed without an entry) and adds one line to README.md's capture-point table describing the new stale-handoff warning. 3 new test cases (171 -> 174 passing). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UXL2ffVDsAp4P7387Nr18G
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Both fixes touch
hooks/session-onboard.shand came out of a competitive read ofadrrr/persistent-handoff(see the linked issues).#66 - stale-handoff detection. Every handoff-file design shares one blind spot by
construction: the file only reflects what was written, so it can look current while
work has moved on without it (this project's own HANDOFF.md went stale 5+ weeks this
way).
session-onboard.shalready reads theLast Updatedline and already shellsout to git - comparing the two now warns when HANDOFF.md sits 14+ days behind the
branch's latest commit. Uses a portable civil-calendar day-count in awk rather than
date -d/date -j(GNU vs. BSD, and eventually Windows Git Bash per #67).#64 - output-size budgeting. A realistic worst case (a 30-line near-max-width
buffer tail, a normal HANDOFF.md, ~20 untracked files) measured at 11,026 characters -
over the ~10,000-char cap a competing plugin claims Claude Code enforces on
SessionStartoutput (Claude Code's own docs specify no limit at all, so the exactnumber is unverified, but an unbounded worst case is worth budgeting against
regardless). Fix: shrunk
TL_COMPACT_TAIL_LINES/TL_COMPACT_TAIL_LINE_CHARSfrom30/300 to 20/200, and moved the buffer-tail inline to render LAST in the script -
after live git state and the unconsumed-buffer scan, both small and bounded with no
fallback of their own. If truncation happens, it now eats into the one block that
already tells you where to find the rest (
session-<id>.md), not into orientationcontent that has no such pointer. Same fixture now measures 5,818 characters.
Test plan
sh tests/run.sh: 171/171 pass (167 baseline + 4 new: stale-flagged,not-flagged-same-day, not-repeated-on-compact, git-state-precedes-tail-inline;
1 existing assertion updated for the new 20-line tail bound)
shellcheck -s sh hooks/*.sh tests/*.sh(CI's exact command): cleanlocal-ci.sh --strict: all 5 checks PASSnear-max-width buffer + 20 untracked files) before and after: 11,026 -> 5,818
characters, with
### Live git stateconfirmed present and preceding the🧷 Context was just compactedblock in the actual hook outputand a Feb 29 leap-year case before landing it
Closes #66
Closes #64