--- name: handoff description: Distill the session into HANDOFF.md plus a session log from the capture buffer. Use when the user says "handoff", "wrap up", "save the session", "checkpoint", or signals done (thanks, that's all). Offer it proactively at wrap-up. license: MIT --- # throughline — Handoff (distillation) **Goal:** Turn a session's raw, continuously-captured actions into curated, durable project memory. Capture is automatic (hooks); **distillation is this skill** — the judgment layer that decides signal vs. noise. **Data directory** (resolve once): `$THROUGHLINE_DATA_DIR` if set, else `/.claude/throughline/`. Below, `DATA` refers to that path. In a linked git worktree, `` here is the **main** working tree, not the worktree itself (see `docs/REFERENCE.md` "Git worktrees") - so `DATA` is shared across every worktree of the repo. - Durable handoff: `DATA/HANDOFF.md` - Session logs: `DATA/logs/handoff-YYYY-MM-DD-HHMM.md` - Raw capture buffers: `DATA/buffer/session-.md` --- ## When to run (including proactively) Run when the user explicitly asks, **or** when you detect wrap-up: the planned work is complete, the user says "thanks / done / that's all / ship it", or a natural stopping point is reached. At wrap-up, **just run it and report the diff** — don't ask permission first. HANDOFF.md updates are idempotent, so an early run is simply refined by the next. Never run it more than once per unchanged state. --- ## Phase 1: Gather source 1. If there are buffers, re-scan them first: `sh /hooks/rescan.sh` with each `DATA/buffer/session-*.md` path (`` as in Phase 4). It rewrites them in place with the current redaction rules and prints one line per buffer it masked; quote those in the report. On a non-zero exit quote its stderr, and where it failed or does not exist (OpenCode, skills-only installs) do the manual re-scan in Reminders on those buffers. Then read **all** `DATA/buffer/session-*.md` — these are the continuously-captured actions (commands, file edits) for this and any unconsumed prior sessions. They survive context compaction, so trust them over fuzzy recollection. One session is one file (keyed by a session id stable across compaction). - Lines marked `` `[interrupted]` `` (or `` `[failed]` `` where a tool exposes an error flag) are actions that did not complete cleanly; do not record them as done. The Claude Code Bash result carries no exit code, so a plain failed command is unmarked: cross-reference the conversation to judge its success. - A `` line marks where a compaction happened. For actions **above** the most recent boundary, distill from the buffer text itself: the conversation's account of *why* they were done has been summarized away and your recall of it is no longer trustworthy. 2. Read the current `DATA/HANDOFF.md` (create from the Phase 5 template if absent). 3. Cross-reference the buffer against the live conversation: the buffer says *what happened*; the conversation says *why*. Distillation needs both (subject to the boundary caveat above). 4. **Check for legacy durable-fact sections, unconditionally — not just when the file is over budget.** If `DATA/HANDOFF.md` still has an "Architecture & Services", "Environment & Infrastructure", "Tools & Integrations", "Authentication & Secrets", or "Key Files & Resources" section (the older template), migrate it this run per Phase 5's migration note, regardless of the file's current size. A file can carry all five sections and still read as "under budget" today; leaving them until the size gate trips just delays the fix and lets the content erode the new budget as the file is edited meanwhile. --- ## Phase 2: Synthesize (signal vs. noise, then durable vs. in-flight) First cut: decide what is a **permanent, reusable change** vs. session-specific noise. Discard the noise. Second cut, and the one that keeps `HANDOFF.md` from bloating: of what's left, split by **durability**, using the test *"would this still be true in three months?"* - **Yes → native memory, not `HANDOFF.md`.** Architecture/topology, environment/infra facts, tool/integration inventory, config & secret **names** (never values), key files & resources, and anything else that reads as a fact about the project rather than a status. Write it as a memory topic file per Phase 4 step 4 below — that store is durably-recalled and does not cost resident context every turn the way `HANDOFF.md` does. - **No → `HANDOFF.md`.** Only genuinely volatile, in-flight state belongs here: - **Resolved issues** — recently closed, not yet promoted to memory (see Phase 4 cap) - **Pending items** — open problems, next steps surfaced this session - **Current state** — what's mid-flight: uncommitted work, open PRs/branches, blockers - **Consolidation passes** and **Memory hygiene passes** — not durable-vs-in-flight at all, they are the `consolidate` / `consolidate-memory` skills' own scope-tracking records. Never touch them here; leave them exactly as found. If you're unsure which bucket something falls in, prefer memory — `HANDOFF.md` is read in full every session (see Phase 4's size discipline); memory topic files are read on demand. --- ## Phase 3: Write the session log Create `DATA/logs/handoff-YYYY-MM-DD-HHMM.md` (local time; create `logs/` if needed): ```markdown # Handoff: **Date:** **Session:** **Follows:** [](handoff-YYYY-MM-DD-HHMM.md) ## Objective The big-picture goal of the session. ## What happened Distilled from the capture buffer + conversation: commands run, files changed, decisions made — with the *why*. ## What we tried (including what failed) Approaches attempted and abandoned, with the REAL evidence: the actual command, error text, or number that killed the approach, not a summary of it. A failed approach is the most expensive thing for a future session to rediscover from scratch; a summary like "tried caching, didn't work" gives a future session nothing to avoid re-trying. Omit this section only if nothing was actually tried and abandoned this session. ## Progress ### Completed ### In progress ### Not started ## Key learnings & gotchas Root causes, non-obvious config, workarounds, things that look like they should work but don't. ## Current state Repo (branch, uncommitted, sync), deploy status, open PRs/issues. ## Next steps Ordered, specific — exact commands, paths, expected outcomes. ## Files & resources Key paths, URLs, credential locations (names only). ``` **Chain link:** if this session continues work a prior log already started (same work stream, not just the same project), name that predecessor in the **Follows** line above so a reader can walk the decision history log-to-log without HANDOFF.md having to carry it. Omit the line entirely for a session that starts something new. --- ## Phase 4: Update durable HANDOFF.md + memory binding 1. Apply the Phase 2 updates to `DATA/HANDOFF.md`: durable facts go to memory (step 4 below), only in-flight state stays here. Keep each entry to 1–2 lines — it's a reference doc, not a journal. Update its **Last Updated** date. Keep the **"Consolidation passes"** and **"Memory hygiene passes"** sections untouched if present (owned by `consolidate` / `consolidate-memory`, not candidates for this trim). - **Match length to substance.** Cover the actual in-flight state; do not pad with filler sections, redundant summaries, or boilerplate. An empty section (nothing durable was moved out, nothing in-flight exists) should not appear at all. - **Cap "Resolved Issues" to the most recent ~5 rows.** Past the cap, promote the row to a memory topic file (step 4 below) if the fact is worth keeping, then drop it here — never leave the only copy of a fact in an already-over-budget file. 2. Add a link to the new session log under "Recent Session Logs" — keep only the **last 5**. 3. **Now that the file is final, measure it — a stop condition, not guidance.** This is the part that previously failed once already (it has regrown past budget before). `HANDOFF.md` is read in full every session — it is resident context, paid for on every turn, not a one-time write. Count it: `wc -l DATA/HANDOFF.md` and `wc -c DATA/HANDOFF.md`. Target **150 lines / ~8KB (~2,000 tokens)**. The byte count is the binding number — this file is table-heavy, so line count alone can look fine while running well over budget in tokens. If either exceeds the hard stop (**200 lines or ~11KB**), you are not done — go back to step 1 and cut before finishing this phase. (Native memory's own `MEMORY.md` index enforces a comparable limit at load time — writes past 200 lines/25KB get truncated silently on every load — which is the failure mode this measured check exists to avoid here, by catching it at write time instead.) - First, re-check every section against the Phase 2 durability test. Anything that would still be true in three months and isn't already in memory is the likely offender — move it now, don't defer it to a later `consolidate` pass. - If nothing left to cut is actually droppable (every line is genuinely in-flight state), the count is real signal that there's too much in flight at once — say so in your report rather than forcing an artificial trim. 4. **Memory binding (native system) — the default destination for anything durable.** Every durable item identified in Phase 2 gets written here, not just rare standout insights — but still **curated**, not auto-dumped: one entry per genuine fact, and check `MEMORY.md`'s existing index first so you update or merge into an existing topic file instead of writing a near-duplicate one. Types: a confirmed preference (`feedback`), a fact about the user/context (`user`), a project constraint/decision/architecture fact (`project`), a resource pointer (`reference`). Native memory is two layers: `MEMORY.md` is an always-loaded index (**truncated silently past 200 lines/25KB on every load** — a write past that limit isn't rejected, but everything past the cutoff becomes invisible to future sessions, so every entry there must stay a single short line), and each entry's full content lives in its own topic file under `~/.claude/projects//memory/`, read on demand. **Topic files have no enforced limit, so they need the same discipline applied by hand: keep each to ~400 words** (match the length to the fact — a one-line preference doesn't need 400 words either; this is a ceiling, not a target) **and split a topic that grows past it into two files** rather than letting one file become a second journal. Write the topic file with frontmatter shaped like this, then add its one-line pointer to `MEMORY.md`: ```markdown --- name: short-kebab-case-slug description: one-line summary used to judge relevance in a future session metadata: node_type: memory type: feedback # or: user, project, reference originSessionId: --- ``` 5. **Consume the buffers:** archive each distilled buffer into `DATA/buffer/archive/` so it isn't re-processed next session. Pass the buffer paths you read in Phase 1 to the plugin's script: ```sh sh /hooks/archive-buffer.sh DATA/buffer/session-.md ``` `` is two directories above this `SKILL.md` (`skills/handoff/` sits beside `hooks/`). The script never overwrites: a second handoff in the same session archives a buffer with the same name as the first run's, and the script appends it to the archived file instead. On a non-zero exit, quote its stderr in the report: it names each buffer it could not archive and where that window was kept. If you can't resolve ``, or the script isn't there (an OpenCode install, whose hooks come from npm and skills from `npx skills`, or any skills-only install), archive each buffer by hand with this one shell call, which never moves onto an existing archive and renames the live buffer before appending, the same order the script uses: ```sh b=DATA/buffer/session-.md a=DATA/buffer/archive/session-.md; mkdir -p "${a%/*}" && if [ -e "$a" ]; then mv "$b" "$a.tmp" && cat "$a.tmp" >> "$a" && rm "$a.tmp"; else mv "$b" "$a"; fi ``` Never delete an archived buffer: it is the recovery path if a distillation later turns out to have missed something. See "Housekeeping" in `docs/REFERENCE.md` for when an archived buffer is old enough to actually delete. 6. **Clear resolved breadcrumbs:** if `DATA/.capture-errors` exists and its contents were surfaced above (as a Phase 2 "Resolved Issues" entry or in the session log), clear the file now that it has been distilled - it exists to make a swallowed capture failure visible exactly once, not to keep nagging on every future onboard after it's already been read and acted on. 7. **Emit a next-session briefing.** Render a second, compact view of the session log's `## Next steps` (plus `## Objective`, what landed, and the sharpest `## Key learnings & gotchas` item) as a copy-pasteable fenced block in your report - this is not new synthesis, just a different rendering of what the log already holds, aimed at priming a brand-new agent/session rather than documenting for a human reader. Do not write it to a file; emit-only. ``` ## Next session briefing - Continuing: Last done: Next: Watch out for: ``` 8. **Offer to commit + push the handoff artifacts** (offer only, never auto-run - the write already happened; this is the review-gated action). throughline's data is local-only *by default* (see `docs/REFERENCE.md` "Local by default") - this offer only ever produces a commit when the project has deliberately opted in by un-ignoring `HANDOFF.md`/`logs/`, so on an unmodified default layout it will correctly find nothing to stage and skip silently. Do not treat "nothing offered" as a bug; it's the expected outcome unless the project opted in. - **Check for a repo first.** Run `git -C rev-parse --is-inside-work-tree` before any other git command here (the project root, not DATA; in a linked worktree, the main working tree). Unless it prints `true`, skip the offer silently and run no other git command - throughline also captures in plain directories, where those commands fail with `fatal: not a git repository`. - If `DATA` lives outside the current project's git tree (an absolute `THROUGHLINE_DATA_DIR` pointed at a shared, cross-harness location - a documented, supported config), skip the offer entirely: those files aren't part of this repo's commit at all, and `git check-ignore` on a path outside the repo fails with a fatal error rather than a clean answer (same reasoning `session-onboard.sh`'s gitignore nudge already applies - see its comment there). - **In a linked git worktree with worktree-sharing active, `DATA` lives under the MAIN working tree, not the worktree the session is actually running in** (see `docs/REFERENCE.md` "Git worktrees"). Run `git check-ignore` with `-C` pointed at (or `cd` into) the directory that actually contains `DATA` before checking it, not the session's own cwd - `git check-ignore` on a path outside the repo it's invoked from fails with the same kind of fatal error as the outside-the-git-tree case above (exit 128, "outside repository"), which is NOT distinguishable from "not ignored" by exit code alone. Treat that fatal error the same as the skip-the-offer case above, not as "not ignored." - Otherwise, check `DATA/HANDOFF.md` and the new session log **independently** with `git check-ignore`. Offer to stage whichever of the two is NOT ignored; if both are ignored, skip the offer silently - nothing committable exists (the default, local-only layout - including this repo's own dogfooded `.claude/throughline/` - is itself gitignored, so this is the common case, and announcing a skipped optional offer on every handoff is filler). When only ONE of the two is ignored, name the file being left out in a clause: a partial staged set is the one case here that surprises the reader. - Stage **exactly** the not-ignored file(s) by their literal paths - `DATA/HANDOFF.md` and/or the specific `DATA/logs/handoff-YYYY-MM-DD-HHMM.md` just written this session. Never a glob like `logs/handoff-*.md` (it would sweep in older, previously-declined session logs too), never `git add -A`, never `buffer/` or `.capture-errors` (see `docs/REFERENCE.md` "Local by default" - those two paths stay gitignored even when a project opts in to tracking `HANDOFF.md`/`logs/`). - Propose a conventional message, e.g. `docs: session handoff - `, matching whatever commit-message convention the repo already follows. - Treat push as a separate, remote-affecting confirmation after the commit - defer to the calling agent's existing git safety norms (branch-first on a default branch, respect any repo-configured push gate, don't bypass one without explicit authorization each time). --- ## Phase 5: HANDOFF.md template (if absent) In-flight state only — no Architecture/Environment/Tools/Auth/Key-Files sections. Those are durable facts; they go to native memory (Phase 4 step 4), not here: ```markdown # — Handoff **Last Updated:** ## Resolved Issues | Issue | Resolution | Date | ## Pending Items | Item | Priority | Tracking | ## Current State ## Recent Session Logs 1. [Title](logs/handoff-YYYY-MM-DD-HHMM.md) — YYYY-MM-DD ``` Do not pre-seed a **"Consolidation passes"** or **"Memory hygiene passes"** section: each is created by its own skill (`consolidate`, `consolidate-memory`) on its first run, and an empty one here only gives that skill a stray header to reconcile against. A project migrating from the old template: at the next handoff, sort its existing Architecture/Environment/Tools/Auth/Key-Files content into memory topic files (Phase 4 step 4), then delete those sections. Don't do this migration silently — report the diff same as any other handoff. **This check is unconditional, not just for an over-budget file** — run it whenever any of those five sections still exist, even if the file is currently under the 150/200-line target (see Phase 1 step 4 above). --- ## Reminders - **Distillation needs judgment** — that's why this is a skill, not a hook. Be terse, preserve structure, don't duplicate existing entries. - **Re-scan for secrets before writing.** Redaction is pattern matching with no entropy analysis: a bare opaque token with no keyword or prefix passes through, and text you bring from the conversation was never redacted. Before writing `HANDOFF.md` or `logs/` - and before any opt-in commit of them - check your draft for token-shaped strings (case-insensitive `token|key|secret|password|credential|auth`, `Bearer`, `ghp_`/`github_pat_`/`gh[oprsu]_`/`sk-`/`AKIA`, URL userinfo) and any long, random-looking string, and reduce them to key names. Two shapes need your eye: (1) a credential on a bare CLI flag (`curl -u user:pass`, or `-p` anywhere but after a mysql/mariadb client name); (2) a value only *partially* masked - a `***` does not mean the whole secret was caught. - **Report, then let the user review.** After writing, show the HANDOFF.md diff + session-log path, then the copy-pasteable next-session briefing block, then - only if something is committable (step 8) - the commit/push offer; with both artifacts gitignored there is no offer to make and nothing to say about it. The review gate is post-write, not pre-write - commit/push is the only action in this sequence that waits on an explicit go. - **Buffers are the source of truth for *what happened*** — they don't lie about which commands ran or files changed, even after a long, compacted session.