#!/usr/bin/env bash
# VERSION: 3
#
# SessionStart snapshot — what a session needs to orient itself, in under 2 KB.
#
# WHY THIS EXISTS. One of my projects had a SessionStart hook that was literally
# `cat context-log.md`. That file had grown to 440 KB and gained an entry every
# session, so every start AND every resume spent ~110,000 tokens — more than half a
# 200k context window — before the first instruction was read. The cost had no
# ceiling and no off switch. A second project had no SessionStart hook at all, which
# is cheaper but leaves a session to re-derive "where am I" by grepping, or to skip
# orienting and duplicate work that was already done.
#
# This answers the three questions a session actually opens with: where am I (branch,
# working tree, recent commits), what happened lately (recent session titles), and
# what is blocked (open Needs Human items). Detail is retrieved on demand by grepping
# the logs — which is why the pointer line matters as much as the data.
#
# BUDGET: keep total output under ~2 KB. If you add a block, take the space from
# another one. `bash .claude/hooks/session-snapshot.sh | wc -c` is the check. The
# failure mode is silent: this file grows, nobody measures it, and in a year it is
# the old hook again.
#
# Runs in every repo, including ones with no context-log.md, no TODOS.md and no git.
# Degrading quietly is a feature — a hook that errors on a scratch repo gets removed.
#
# INSTALL: save to ~/.claude/hooks/session-snapshot.sh (and/or <project>/.claude/hooks/),
# chmod +x, then register it as a SessionStart hook in the matching settings.json.

set -uo pipefail

# In a worktree this resolves to the worktree root, which is correct: each worktree
# has its own context-log.md and its own branch.
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
cd "$ROOT" 2>/dev/null || exit 0

# ONE file serves as both the global copy (~/.claude/hooks/) and the per-repo copy.
# Claude Code merges hooks from global AND project settings, so in a repo that ships
# its own copy both would fire and every session would open with the snapshot printed
# twice. If a repo-local copy exists and this is not it, defer silently.
#
# Written as a path comparison rather than an env flag so the two copies stay
# byte-identical — a global variant with its own extra branch is a second version to
# keep in sync, and it would drift.
_SELF_NAME="$(basename "${BASH_SOURCE[0]}")"
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
if [ -f "$ROOT/.claude/hooks/$_SELF_NAME" ] && [ "$_SELF_DIR/$_SELF_NAME" != "$ROOT/.claude/hooks/$_SELF_NAME" ]; then
  exit 0
fi

IS_GIT=0
git rev-parse --git-dir >/dev/null 2>&1 && IS_GIT=1

echo "=== PROJECT STATE ==="
if [ "$IS_GIT" = "1" ]; then
  echo "branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?') | uncommitted: $(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') file(s)"
  git log --oneline -3 --format='  %h %s' 2>/dev/null
else
  echo "(not a git repository — no branch or commit history to report)"
fi

# Record where this session started, so the Stop hook can measure what it wrote.
# `--git-dir` rather than a literal .git: inside a worktree .git is a FILE pointing
# elsewhere, and writing to it would clobber the pointer.
if [ "$IS_GIT" = "1" ]; then
  GITDIR="$(git rev-parse --git-dir 2>/dev/null)"
  if [ -n "$GITDIR" ] && [ -d "$GITDIR" ]; then
    # Total bytes across the whole context-log family. Rotation moves bytes between
    # files without changing the total, so it cannot look like a huge session write.
    START_BYTES=$(cat context-log*.md 2>/dev/null | wc -c | tr -d ' ')
    printf '%s\n' "${START_BYTES:-0}" > "$GITDIR/claude-session-start" 2>/dev/null || true
  fi
fi

# Recent sessions, newest first BY DATE.
#
# Not `head -8`. Most entries are prepended, but some sessions have appended to the
# end instead — in one of my repos the newest entry sits at the BOTTOM of the file, so
# taking the first eight lines silently reports a stale set as "recent". Sorting on the
# date inside each heading is the only ordering that survives both conventions, and it
# also merges the live file with its rotated archives in one pass.
shopt -s nullglob
LOGS=(context-log*.md)
shopt -u nullglob

if [ ${#LOGS[@]} -gt 0 ]; then
  echo ""
  echo "=== RECENT SESSIONS (newest first) ==="
  grep -h '^## ' "${LOGS[@]}" 2>/dev/null \
    | sed -E 's/^## +//' \
    | awk '{ if (match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2}/)) k=substr($0,RSTART,RLENGTH); else k="0000-00-00"; print k "\t" $0 }' \
    | sort -r \
    | head -8 \
    | cut -f2- \
    | cut -c1-96 \
    | sed 's/^/  /'
  LIVE_KB=$(( $(wc -c < context-log.md 2>/dev/null || echo 0) / 1024 ))
  echo "  Older history: grep '^## ' context-log*.md for the index, then read only the"
  echo "  range you need. Never read these whole — the live file alone is ${LIVE_KB} KB."
else
  echo ""
  echo "No context-log.md here yet. If this session does real work, start one:"
  echo "  a '## YYYY-MM-DD — <what changed>' heading and 5-8 bullets."
fi

if [ -f TODOS.md ]; then
  echo ""
  echo "=== OPEN: NEEDS HUMAN (TODOS.md) ==="
  grep -n '^## Needs Human' TODOS.md 2>/dev/null | head -6 | cut -c1-96 | sed 's/^/  /'
fi

echo "=== END ==="
