| name | handoff |
| description | Session wrap-up — produces a HANDOFF file at `${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md` for the next session's AI. Format spec: `standards/ai-friendly-artifacts.md` § 1.4.2. Pending matters more than Done. Use when builder says handoff / 收尾 / 总结一下 / 交接 / 交代一下 / wrap up / session 结束, or PreCompact hook prompts. Skip pure Q&A, mid-execution unstable state, or if the same target handoff was already written within 5 min (per Phase 1.5 recency guard). |
/handoff — Session handoff skill
Contract
One invocation produces:
- HANDOFF file — single per-topic task card at
${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md, per § 1.4.2 of the canonical standard. Every file MUST carry kind: handoff in its frontmatter — this is the identity stamp /pickup uses to identify handoff files.
- Verbal report (4 lines, printed to chat) — what builder copy-pastes into the next session
Canonical specs (do not duplicate here):
- HANDOFF schema →
${BDC_HOME:-$HOME/.bdc}/standards/ai-friendly-artifacts.md § 1.4.2
- AI-friendly artifact shape →
${BDC_HOME:-$HOME/.bdc}/standards/ai-friendly-artifacts.md
Why one schema, not two: the previous design had a Schema A (<project>/HANDOFF.md, multi-branch index) alongside Schema B (this one). Schema A's per-branch sections required parsing markdown bodies (## {branch} headers, **Topic**: fields, fallback bullets) — fragile against any project that hand-wrote a HANDOFF.md with free-form ## sub-sections, which leaked into /pickup as ghost rows. The fix is to make identity explicit at write-time (kind: handoff stamp in frontmatter) and let /pickup trust the stamp instead of guessing structure.
This skill is the executor. To change format rules, edit the canonical doc, not this skill.
Phase 1 — Pick the topic file
Every session that touched code/config writes to one file: ${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md.
- Same topic, same file. Before writing, scan
${BDC_HOME:-$HOME/.bdc}/handoff/*.md for an existing topic match (frontmatter title or filename slug). If found → update in place. If not → create a new file ${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md (slug only, no date prefix; date lives in frontmatter).
- The slug should be a stable kebab-case identifier (e.g.
launch-traffic-fix, pickup-display-bugs). Pick something that survives across sessions even if the headline phrasing changes.
- Skip the handoff entirely for pure Q&A sessions with no code/config changes.
Determine "did this session touch code/config" by inspecting Edit/Write tool calls in the conversation + git status in relevant dirs. If yes → write a handoff. If no → exit without writing.
Phase 1.5 — Recency guard
After the target file path is known but before Phase 2 writes anything, check whether the SAME target was just written. Repeat invocations within a few minutes overwrite a fresh handoff with potentially-worse extraction (same conversation context, different field inference).
Rule: compute latest_mtime = max mtime across:
- the target handoff file at
${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md
Files that do not exist contribute nothing — a fresh topic with no prior handoff trivially passes the guard.
If now - latest_mtime < 300 seconds, abort with a one-line prompt to builder: ⏸ /handoff just ran <Ns> ago against <target_path>. Re-run anyway? (yes / no). Wait for an explicit yes before continuing.
Skip the guard only when builder explicitly says "force" / "重跑" / "再跑一次" in the same message that invoked the skill.
Why scoped to the target file (not the whole directory): scanning ${BDC_HOME:-$HOME/.bdc}/handoff/ as a whole would let an unrelated concurrent session block this one. The scope must be the target topic.
Phase 2 — Write the HANDOFF file
Phase 2 emits the HANDOFF file with the structured session brief (9-field YAML). Generate the brief once per invocation (§ 2.1), then apply the write rules (§ 2.2).
The 9-field brief is an AI-first artifact. Do not replace it with prose or rename fields. Instead, preserve stable fields, exact paths, status, evidence, and next action. Because: the next AI and /pickup need a parseable recovery surface.
2.1 Generate the session brief
The ## Last session brief section is a 9-field YAML block. Per-field sources:
-
date: today (date +%Y-%m-%d).
-
plan_progress: if an active plan in .bdc/plans/ is being executed, format "step N/M → N+1/M (advanced K steps)" where N = - [x] count at session start, N+1 = current - [x] count, M = total checkboxes, K = flips this session. No active plan → omit field.
-
files_touched: 4 sources merged, each entry labeled with origin. The skill explicitly does NOT auto-commit, so working-tree sources are the norm — missing them would silently drop the most active session work.
- committed:
git log <start>..HEAD --stat → entry as path (+24/-18, committed)
- staged:
git diff --cached --stat → entry as path (+5/-2, staged)
- unstaged:
git diff --stat → entry as path (+12/-0, unstaged)
- untracked:
git status --short filtered to ^?? → entry as path (untracked)
<start> comes from § Phase 2.5 (last-handoff-hash). Don't merge entries by path — origin labels carry the meaning, so the same file in committed + staged stays as two rows.
-
commits: git log <start>..HEAD --pretty=format:'%h %s', one entry per commit.
-
key_decisions: extract explicit decisions from the session, plan Decision Log, commits, and builder corrections; one bullet per decision, trimmed to one line.
-
discoveries: extract insights, mistakes, pitfalls, or useful findings from the session; one bullet per discovery, trimmed to one line.
-
why_stopped: AI-inferred one-liner. Prefix value with inferred: so builder can overwrite on review. Best guess based on the conversation's last few turns (planned pause vs blocker vs end-of-task).
-
next_step: first - [ ] action line from the active plan's currently-executing feature. No active plan → use the first concrete pending item from the session.
-
open_questions: only items builder explicitly flagged as questions for the next session. Otherwise empty list [].
Empty list [] is preferred over omitting a field — keeps the schema stable for downstream parsers (the /pickup skill consumes this brief).
2.2 Write rules
- Read § 1.4.2 of the canonical doc for full schema (frontmatter + sections).
- Frontmatter MUST include
kind: handoff — fixed string, the identity stamp /pickup uses to recognize handoff files. Without it the file will be invisible to /pickup. Other handoff frontmatter (status, last_session, prs, etc.) per § 1.4.2.
- Update style per section (judgment-driven, not mechanical):
## Last session brief → replace with § 2.1 output (always current; structured).
## Next → replace (always current; human-readable paraphrase of brief.next_step if more context helps).
## Constraints → edit (add new, remove what no longer applies, keep what's still in force)
## Known Pitfalls → append (累积; each gotcha saves the next session from repeating)
## Related PRs → update to current state via gh pr view
- frontmatter
last_session → today
- frontmatter
prs → reflect current set
- frontmatter
status → active unless all PRs merged + no pending follow-up (then done)
- Body target: "the next AI can pick up this topic in 60 seconds." Don't pad. If a section has nothing real, omit it.
Phase 2.5 — Persist session-start hash
The brief's files_touched and commits fields are computed by git log <start>..HEAD. <start> is the HEAD as of the previous /handoff invocation; this section maintains that anchor.
Write step (end of each /handoff):
- After the HANDOFF file is written, store the current commit as the next session's start anchor:
git rev-parse HEAD > .git/last-handoff-hash in each touched project repo. This file is per-repo, never committed (.git/ is git's own state directory).
- For multi-repo work, write the hash to each touched repo's
.git/last-handoff-hash independently.
- For work in
${BDC_HOME:-$HOME/.bdc}/ or other non-git locations, skip this step — there is no diff to compute.
Read step (start of each /handoff):
- Try
cat .git/last-handoff-hash and git cat-file -e <hash> to confirm reachability.
- Primary path (hash file exists, object reachable) →
git log <hash>..HEAD is the diff range for files_touched / commits.
- Fallback A (hash file missing OR object unreachable: force-push, GC, shallow clone) → use a time-based window:
git log --since='<last_session timestamp>', where <last_session timestamp> reads from the target handoff's frontmatter last_session field. If not parseable → drop to Fallback B.
- Fallback B (both fail: fresh clone, first session) → committed entries return empty (no diff range), but staged / unstaged / untracked entries are still collected per Phase 2.1's 4-source aggregation.
commits: [] is genuinely empty (no diff range = no commits). Do not block the handoff. The brief annotates the committed-source omission inline: files_touched: ... # committed source unavailable: <reason>.
Why .git/: it is per-repo, gitignored by definition, and survives across sessions while never leaking into commits. No new top-level files outside the repo's own state.
Phase 3 — Update index
Update ${BDC_HOME:-$HOME/.bdc}/HANDOFF.md Active list:
- Topic already in list → update its description line to match the current
## Next
- Topic not yet listed → append a new line:
- [<topic-slug>](handoff/<topic-slug>.md) — <one-line description matching ## Next>
- Topic now
status: done → can be removed from Active list (or moved to a "Recently done" section if builder prefers)
Phase 4 — Verbal report
Print 4 lines to chat (builder copy-pastes into the next session):
📁 <handoff path>
🚪 Enter next time: cd <absolute path>
🎯 First step next session: <one sentence — extracted from ## Next>
⚠️ Unresolved: <git uncommitted · open PR · ${BDC_HOME:-$HOME/.bdc}/ untracked, etc.>
- 🎯 line is the most important — paraphrase
## Next into one sentence with a concrete action verb
- ⚠️ lists state only, takes no action: builder decides whether to commit/push next time
- BDC reminder: at the end of every session, run
cd ${BDC_HOME:-$HOME/.bdc} && git status --short | head -5 — if untracked/modified, mention it in ⚠️
If ## Next has no clear concrete action → ask builder one question: "Where should the next session start?" — then finalize the handoff.
Standards
- Do not auto-commit / push / open PRs / open issues — this skill only writes files. Those are builder's decisions.
- Do not omit
kind: handoff from frontmatter — without the stamp, /pickup ignores the file. Treat this as the identity contract between writer and reader; everything else (sort order, badges, brief parsing) downstream depends on the file being recognized as a handoff first.
- Do not pad the body with narrative or "Why this matters" filler — if a section has nothing real to say, omit it.
- Do not create a new dated file for an existing topic — same topic = same file (filename is topic-slug, no date prefix; date is in frontmatter).
- Do not write to
<project>/HANDOFF.md — the legacy Schema A path. Project-local handoffs are obsolete; everything goes to ${BDC_HOME:-$HOME/.bdc}/handoff/<topic-slug>.md.
Verification
- The HANDOFF frontmatter includes
kind: handoff.
- The final chat report includes handoff path, next command, first next step, and unresolved state.
Red Flags
- A new dated handoff file is created for an existing topic.
files_touched ignores unstaged or untracked changes.
Common Rationalizations
| Rationalization | Response |
|---|
| "A short chat summary is enough." | Do not. Pattern: long-running validation work requires durable next-session state instead of chat-only summaries. |
| "This topic changed, so create a new handoff file." | Do not unless it is a truly new topic. Incident: this skill's own anti-pattern history replaced fragile project-local Schema A with one stable kind: handoff identity stamp. |