handoff — Session-End Handoff Writer
Overview
HANDOFF.yaml is split in two: items are the committed short-lived context layer for
still-open work, while log is the durable one-line history of finished work. GitHub issues,
reconciled through valerie and doob, are the active-work source of truth. Non-Valerie
skills should use the local SQLite database via handoff-db plus HANDOFF YAML, never doob
directly. The scripted bridge from HANDOFF into the backlog is handoff-reconcile; use that
instead of reconstructing doob commands by hand. Project state (build, tests, branch) lives separately in
.ctx/HANDOFF.<name>.<base>.state.yaml — generated, never committed. A rendered reference doc
is also written to .ctx/HANDOFF.md.
See references/schema.md for the full YAML schema, immutability rules, priority guide,
and file layout.
File Discovery
Use handoff-detect to resolve the HANDOFF.yaml path if available:
handoff-detect # returns path if exists, expected path + exit 2 if not
handoff-detect --name # expected filename only (e.g. HANDOFF.devkit.devkit.yaml)
handoff-detect --root # repo root
handoff-detect --project # project name
If handoff-detect is not on PATH, fall back to globbing the repo root for HANDOFF.*.yaml.
Legacy fallback (read-only): if no HANDOFF.yaml exists and a HANDOFF.md exists at repo root,
read it as freeform. Do not convert unless asked.
Steps
1. Get current state
git branch --show-current
git log --oneline -5
cargo check 2>&1 | tail -3 # or language equivalent
cargo test 2>&1 | tail -5
2. Read existing HANDOFF.yaml and .ctx/HANDOFF...state.yaml (if present)
3. Update HANDOFF.yaml
items — apply immutability rules (see references/schema.md):
- New gap → append with new
id - Completed or closed upstream → remove the item from
itemsafter recording the outcome inlog - Blocked → set
status: blocked, appendextraentry withtype: blocker - Do NOT edit title, description, priority, or files on existing items
- Do NOT retain done or parked items in committed
items
human-edit acknowledgement — for any extra entry with type: human-edit and no
reviewed field that was surfaced by handon this session, add reviewed: <today> to that
entry.
log — prepend a new entry (newest first). This section is durable, not transient. Required
fields and formats:
date: ISO 8601 datetime at session end —date +%Y%m%d.%H%M%S— never a bare datesession: increment from the previous log entry'ssessionvalue (start at 1 if no prior log)claude_session_id: capture from$CLAUDE_SESSION_IDenv var if set; omit the field if unsetsummary: one line, past tensecommits: use{sha, branch}object form for each commit — never bare hash strings:commits: - sha: abc1234 branch: main
updated — set to current UTC datetime: date +%Y%m%d.%H%M%S.
4. Write HANDOFF.yaml
Emit clean YAML. No anchors, no aliases.
5. Write .ctx/HANDOFF...state.yaml
Create .ctx/ if it does not exist. Overwrite completely with current state from step 1.
Populate touched_files from files changed in commits since the session started. If session
boundary is unclear, use files changed since the last log entry date in HANDOFF.yaml:
git diff --name-only $(git log --format="%H" --since="<last-log-date>" | tail -1)..HEAD
Omit the field if empty.
6. Sync to SQLite
Run handoff-db (available on PATH via the plugin's bin/):
handoff-db upsert --project <project> --handoff <path-to-HANDOFF.yaml>
If the script is not found or exits non-zero, skip and note it in output.
Do not call doob from this skill. valerie owns doob and GitHub issue sync.
6b. Reconcile open HANDOFF items into the backlog
Run the scripted Valerie bridge:
handoff-reconcile sync --project <project> --handoff <path-to-HANDOFF.yaml>
This is required. A handoff update is not complete until every open or blocked HANDOFF item has
been reconciled into the configured doob backend through this command. Do not recreate this
flow with ad hoc doob todo add / doob todo list commands unless you are debugging the
reconciler itself.
If the script is not found or exits non-zero, stop and report the failure instead of silently continuing.
7. Generate .ctx/HANDOFF.md and .ctx/HANDOVER.md
Run the helper script to write both rendered docs in one step:
generate-ctx-docs \
--handoff <path-to-HANDOFF.yaml> \
--state <path-to-HANDOFF.state.yaml> \
--ctx <repo-root>/.ctx
generate-ctx-docs is available on PATH via the plugin's bin/. It prefers nu if present
and falls back to POSIX sh. Both files are overwritten completely.
HANDOFF.md — combined reference doc:
- Header: project, updated timestamp, branch/build/tests from state file
- Items table sorted P0 → P2, open before blocked
- Log: last 5 entries only
HANDOVER.md — static ASCII dependency/flow diagram:
- Item flow with
[ ]open /[!]blocked /[x]done symbols depends_onshown inline when present- Recent sessions (last 5 log entries)
If the helper is not found or exits non-zero, fall back to writing .ctx/HANDOFF.md inline
using the Write tool (template from the previous step format). Skip HANDOVER.md in that case
and note the failure.
8. Ensure .gitignore covers .ctx/
Verify .gitignore has:
.ctx/*
!.ctx/HANDOFF.*.yaml
.ctx/HANDOFF.*.state.yaml
Add or update if not present. This pattern ignores all .ctx/ contents except HANDOFF files.
9. Migration preflight
Before writing, check if the HANDOFF file is still at the repo root. If so, migrate it first:
migrate-handoff <repo-root> <old-root-path>
Then stage the rename and continue with the new .ctx/ path.
10. Commit
git add <path-to-HANDOFF.yaml> .gitignore
git commit -m "docs: update handoff"
Stage only the durable HANDOFF file under .ctx/ plus any .gitignore update required for the
managed block. Never stage .ctx/HANDOFF.*.state.yaml, .ctx/HANDOFF.md, or .ctx/HANDOVER.md.
Creating from Scratch
Bootstrap from git context:
git log --oneline -10
git status
Populate log from recent commits. Leave items empty or with one P1 if there's an obvious
open next step. Do not backfill closed work into items, but do preserve durable log
history. Write .ctx/HANDOFF.<name>.<base>.state.yaml from actual build/test output.
Never add an item whose only content is "you have uncommitted changes" (e.g. "Uncommitted
Work", "Uncommitted changes"). Dirty git state is transient and already visible via
git status — it is not a task. This pattern was recurring across every session with a
dirty tree and syncing into doob as duplicate todos (see doob audit 2026-07-19/20). Only
add a P1 item if there's a concrete, actionable next step beyond "commit your changes."
Place the new HANDOFF file at .ctx/HANDOFF.<name>.<base>.yaml where <name> is the
package/crate name from the nearest manifest and <base> is the repo root dir name.
Use handoff-detect --name to get the correct filename.
Legacy HANDOFF.md
If HANDOFF.md exists at repo root and HANDOFF.yaml does not: read it as freeform context,
do not auto-convert. Note that a HANDOFF.yaml could be created.