Skip to content

Handover System — reference

Extracted from CLAUDE.md (state-not-prompt slimming). CLAUDE.md keeps a session-time pointer; the full reference lives here. The v2 handover skill (marketplace/plugins/handover/skills/handover/SKILL.md) + ~/.claude/handover/registry.json are the live source of truth for registered repos, paths, and routing — inspect/change via the /handover repos|register|init slash commands, not by editing docs.

Since 2026-05-25: all personal handover state has been centralized in your handover state repo (configured via /handover-setup / $HANDOVER_DIR). himmel handovers/<USER_SLUG>/ is empty except for a README pointer. Three resolution layers — the v2 handover skill’s multi-repo registry (preferred for skills), the single-root resolver (still wired for scripts), and a recommended HANDOVER_DIR bridge for non-skill consumers.

v2 multi-repo registry (preferred for skills)

The v2 handover skill (marketplace/plugins/handover/skills/handover/SKILL.md) is the source of truth. Don’t duplicate its state here — paths, registered repos, and routing rules belong in ~/.claude/handover/registry.json, not in this doc. Use these slash commands to inspect or change state:

  • /handover repos — list currently registered repos + their roles.
  • /handover register — add a new repo (prompts for path, user slug, aliases).
  • /handover init — bootstrap the registry on a fresh machine.

Bucket-name semantic: each registry entry carries an optional bucket_name field. The bucket-layout resolver reads this when deciding which <state-root>/<bucket>/ directory a repo’s handover state lands in. Default: slugified basename(path). /handover register + /handover repos add refuse on collision — two repos resolving to the same bucket_name is ambiguous; the operator must pass --bucket-name=<unique> to disambiguate. Existing entries auto-backfill from basename(path) on first read. Fresh-fork operators (e.g. forks of himmel named cool-tool) get the right bucket out of the box without code edits.

Branch-prefix semantic: the registry’s branch_prefix field is the handover-mutation prefix. It scopes branches created by new-epic, new-task, new-standalone, end-session, and update-status — the operations that mutate <state-root>/. Default: handover/ (changed from feat/). This is NOT the general feature-branch prefix used by /worktree.sh for ticket-driven feature development; those follow the type/slug convention enforced by scripts/worktree.sh (feat|fix|chore|docs|refactor|test).

Auto-branch + commit + push: scripts/handover/auto-commit.sh no longer commits on whatever branch is currently checked out. Each invocation:

  1. Reads branch_prefix from registry.json for the resolved handover repo (default handover/).
  2. Extracts the first PROJECT-N ticket from the commit message and derives a slug from the remaining text (lowercase, non-alnum → dash, ≤30 chars).
  3. Switches to <branch_prefix><TICKET>-<slug> (or <branch_prefix>session-YYYY-MM-DD when no ticket is detected) — reusing the branch if it already exists locally or on origin.
  4. Stages + commits .md changes there.
  5. Auto-pushes (-u origin <branch>) for resilience — every commit ends up on the remote even if the local clone is later lost.

HANDOVER_DIRECT_MAIN=1 restores the v1 commit-on-current-branch behavior (kept as an opt-in feature flag until the branched flow is fully validated). --no-push skips the push step (useful for tests or offline work). Smoke test: bash scripts/handover/test-auto-commit.sh.

Single-writer handover repos: a handover repo carrying a gitignored .single-writer marker at its root commits handover state directly on its default branch (main) — auto-commit.sh skips the per-ticket branch + PR entirely (same direct path as HANDOVER_DIRECT_MAIN=1, detected via [ -f "$handover_repo/.single-writer" ]). If such a repo is parked on a non-default branch it refuses (exit 7) rather than entangle handover state onto a feature branch — fail-loud, pointing the operator at git checkout <default>. The default branch is read from refs/remotes/origin/HEAD; when that is unknown the repo is treated as on-default (fail-open, never mis-refused). This is the design personal handover/state repos and vaults use so every session/worktree reads and writes one shared main — preventing the parallel-branch entanglement that blocked an earlier close-out.

PR open/update + squash finisher: After every push on the branched path, auto-commit.sh invokes scripts/handover/pr-open.sh to open (first push) or update (later pushes) the PR for <branch_prefix><TICKET>-<slug>. Body has three sections: ## Summary (Jira ticket title when available), ## Files changed (auto from git diff --name-status vs base), ## Ticket (HIMMEL-N link).

Before publication, both scripts/handover/pr-open.sh and scripts/lanes/worker-pr-open.sh offer optional deterministic punctuation normalization on stderr. No model rewrite is offered or run, and no plugin is enabled. Skip continues unchanged. Generate a separate candidate:

Terminal window
python3 -I marketplace/plugins/himmel-ops/scripts/humanizer-facts.py --normalize body.md normalized.md

Only after successful normalization, pass --humanized-body normalized.md. Leading bold/code labels get a colon instead of a spaced em/en dash, other spaced dashes become commas, and curly quotes become straight. Banned words are flagged, never rewritten. The normalizer refuses if it would change a protected literal; use the original rather than editing commands. The helper checks literal facts again and uses the candidate only on acceptance. Changed keys, numbers, file names, inline code or commands refuse the candidate, not the PR; unavailable checker or candidate also retains the original. Mark other commands as code and review meaning before supplying a candidate. Record skipped, kept or dropped passages in test notes, not as operator-kept counts.

For handover-generated bodies, first use --dry-run and save only the text between --- body --- and --- /body ---, without those delimiters. Normalize that body and rerun with the candidate flag. For the worker helper, keep the original body file as the second argument and add the candidate flag; its computed worker-burn: line is inserted after the fact check.

PR-create failures are best-effort: the branch is still pushed and an operator can open the PR manually later. HANDOVER_PR_AUTO=0 skips the PR layer entirely (no-op exit 0).

scripts/handover/pr-merge.sh fires the merge: gh pr merge <N> --squash --admin --delete-branch. Squash is the only allowed mode — repo settings forbid merge-commits (operator confirmed 2026-05-25). The script suppresses the cosmetic failed to run git: fatal: 'main' is already used by worktree error when the local branch-delete trips on a held worktree (remote PR is merged either way).

Slash commands: /handover-pr-open, /handover-pr-merge. Smoke test: bash scripts/handover/test-pr-open.sh.

Session-end consolidation sweep: scripts/handover/flush.sh walks every local handover/* branch in the resolved handover repo and reconciles state:

  • Unpushed → git push -u origin <branch>.
  • No open PR → invokes pr-open.sh.
  • Merged into origin/main (squash-detected via git cherry) → report by default; --cleanup deletes the local branch.

Wired into /context-hop: hop.sh runs flush.sh before writing the snapshot so cap-resume hand-off cannot leave un-pushed handover state.

Failure modes:

  • gh missing or unauthenticated → warns + dumps the exact commands the operator must run to open each PR. Push step still runs.
  • Per-branch errors surface in the table; sweep continues.

Slash command: /handover-flush (+ --dry-run, --cleanup, --no-pr-open flags). Smoke test: bash scripts/handover/test-flush.sh (14/14 pass).

Pre-push automated review runs on handover/* branches: The code-review-before-push pre-commit hook (.pre-commit-config.yaml lines 154–161) has no handover/* exclusion — intentional. Operator decision: automated review runs as a sanity audit to catch accidental code edits sneaking into handover branches. The docs-only-skip already wired into scripts/hooks/check-cr-before-push.sh (line 115 filters out .md / .txt / docs/ / handovers/ paths) gives the right behavior:

  • Pure handover state diff (only .md / handovers/* files) → hook prints handover-state-only change — skipping marker write, exits 0. No review gate. Runtime effectively 0s.
  • Mixed code + state diff (code creep into a handover branch) → review-owed marker written; operator must run /pr-check (or /pr-review-toolkit:review-pr) before opening the PR.

Smoke test: bash scripts/hooks/test-check-cr-before-push.sh (7/7 pass).

Previously the skill resolved per-repo state under <repo-root>/handovers/<user>/. Now the expected target is the <state-repo> for any personal state regardless of which repo triggered the skill. A bucket layer was added (<state-root>/<repo>/{epics,standalones}/ where <repo> ∈ himmel | luna | luna_brain | cross) — SKILL.md resolver auto-detects the layer when any bucket dir exists under <state-root> and falls back to the flat layout otherwise.

luna_brain is a legacy built-in bucket, not an active repo bucket. luna_brain ceased to be a separate local repo when its content was folded into himmel/templates/luna-second-brain/; the bucket is empty and no repo is registered against it. It stays in the built-in vocab so the resolver needs no change — the LUNA-BRAIN-* ticket-prefix rule still resolves here (marketplace/plugins/handover/skills/handover/references/resolution.md), it just has nothing to route. Do not read the bucket as evidence of a live repo. (Distinct from upstream OSS luna-brain, the publish target the template ships to — see public-propagation.md.)

Single-root resolver (still wired for scripts)

scripts/lib/handover-path.sh provides a single-root resolver that shell scripts use when they only know about one root:

  • Mode A — inline (default): HANDOVER_DIR unset → <repo>/handovers/. Now this resolves to a near-empty himmel/handovers/ — the only tracked content is a pair of README stubs (handovers/README.md and a per-user handovers/<USER_SLUG>/README.md) that mark the layout; no handover state lives there. Useful only for himmel-scoped legacy code.
  • Mode B — external: HANDOVER_DIR set → that path. Recommended default: a handovers/ folder inside your luna-style vault — state becomes part of the knowledge base (wikilinkable, qmd-indexed, graphify-able) with no extra repo to sync:
    Terminal window
    export HANDOVER_DIR="$HOME/Documents/<your-vault>/handovers"
    A dedicated external state repo remains fully supported (the resolver is root-agnostic):
    Terminal window
    export HANDOVER_DIR="$HOME/Documents/github/<state-repo>/handovers"
    Pick ONE root. Running both (state duplicated across a vault and a state repo) is unsupported — two writable roots drift and the resolver reads only HANDOVER_DIR.

Consumed by scripts/handover/auto-commit.sh, scripts/handover/arm-resume.sh, setup.sh’s step 6, and docs/handover/afk-mode.md Phase 1 (plans-root resolution).

arm-resume.sh --automerge opt-in wires ARMAUTOMERGE=1 and CR_MERGE_GATE_OK=1 into the relaunched session’s environment, so an armed chain’s merge auto-ship step can actually reach merge-on-green.sh; omitted (both vars absent) by default.

The live-worker guard is scoped to the bridge root, not the caller (#1551). Before arming, arm-resume.sh census-checks for live lane workers and refuses to arm (rc=10) while any are running or unprobeable — but that census reads a SHARED bridge root with no per-session scoping, so a second, unrelated concurrent dependency chain’s workers can block an arm that has nothing to do with them. ARM_WITH_LIVE_WORKERS=1 bypasses the refusal (and the worker-reconciliation/list failure cases that hit the same code path) — trace the blocking pid up to its root process before reaching for the override; a proven-ownership override is auditable, a habitual one defeats the guard on the day it is actually right.

Scripts, commands, and skills must NOT hardcode ./handovers/. Source scripts/lib/handover-path.sh and call handover_root instead. The resolver fails CLOSED when HANDOVER_DIR is set but points to a missing path. Mode + root are reportable via /handover-link (status default, doctor exits non-zero on misconfiguration).

Durable resume breadcrumb + resolver (C3)

scripts/handover/breadcrumb.sh makes an interrupted/armed resume failure-survivable. Twice an armed overnight resume found “no captured stop-point” and silently grounded in raw repo state, losing the armed work. C3 closes that: the loop drops a versioned breadcrumb after each pipeline stage, and the resolver REFUSES to silently degrade.

  • write — a versioned JSON breadcrumb at <handover-root>/breadcrumbs/<ticket>.json (root via the single-root resolver above; never hardcoded): ticket, branch, base_sha, head_sha, completed[], next_step. Auto-detects the git facts from the --cwd repo. Emitted from the seams that already run between stages (/afk-shift post-fanout step 7, /pr-check, the handover writer) — there is no per-step dispatch runtime to hook (inject-initiative.sh is SessionStart-only; the auto-ship steps are self-paced prose).
  • resolve — on resume, classifies the stop-point:
    • FRESH (exit 0) — breadcrumb’s head_sha == current HEAD → deterministic resume; prints the recorded next_step + completed.
    • DEGRADED (exit 3) — breadcrumb missing, stale (HEAD/branch moved), or corrupt → reconstructs candidate intent from git log (commits naming the ticket) + open Jira, and prints DEGRADED — confirm before proceeding. NEVER silent. Jira enrichment is fail-soft (--jira-cmd override / --no-jira; a jira failure degrades to git-only, never crashes).

Neighbour to handover-resume-armed and the armed-resume profile — it does not replace them; it adds the reconstruct-and-flag layer they lacked. Tests: scripts/handover/test-breadcrumb.sh (hermetic — temp HANDOVER_DIR + temp git repo + jira stub), incl. the epic-done self-test (deleted breadcrumb → flagged recovery; valid → deterministic).

Operator-gated wrap + resume briefs

On 2026-09-01 a worker sat idle for ~1.5h waiting on an operator settings edit, holding its queue lock and burning its prompt cache, until the chief woke it (a cold re-prime) just to produce a state summary the chief could have written from git. The ## Operator-Gated Wrap clause now carried verbatim in every *-next-session.md template (marketplace/plugins/handover/templates/, asserted byte-identical by scripts/handover/test-template-operator-gated-clause.sh) tells a worker to wrap at once when its only remaining dependency is an operator action: append a resume brief, release the queue lock, and end the turn instead of idle-waiting.

## Chief Rulings clause: the same templates also carry a ## Chief Rulings clause, asserted byte-identical by the same test suite, prefacing every worker brief with how rulings actually reach it — a claudex worker via the file inbox hook (scripts/hooks/claudex-inbox-hook.sh), a native worker via a direct SendMessage — so the doc’s own ## Chief Rulings section is what a worker checks, not something it idle-waits or gets pasted. See lane-calibration.md’s claudex supervisor session reachability note for the delivery mechanism itself.

scripts/handover/worker-resume-brief.sh <worker-doc> [--branch <name>] [--repo <path>] [--dry-run] is the chief-side half of that clause: it reconstructs everything about a worker’s state that git already knows (worktree path, git status --short, merge-base + head SHAs and their subjects, dirty paths) and appends a ## RESUME BRIEF (generated ...) section to the worker’s own handover doc, never rewriting or truncating it. The branch is read from --branch or inferred from a single feat/|fix/|chore/|docs/|refactor/|test/ token in the doc body (ambiguous or absent → exit 2, pass --branch explicitly). --repo overrides the script’s own repo, for a worker that lives in another repository. The ordered remaining steps are deliberately NOT filled in — they live only in the worker’s own reasoning, so the script emits a TODO block instead of inventing them.

The rule this encodes: wake a cold worker only when its private context is the only source of a fact. Anything git-recoverable is the chief’s job to write from git directly; only the worker’s own un-externalized reasoning (the ordered next steps, judgment calls made mid-session) ever justifies paying for a fresh cold-start. Test: bash scripts/handover/test-worker-resume-brief.sh.

Periodic seat-liveness reconcile

scripts/handover/reconcile-workers.sh used to run only when a dispatch or an arm triggered it, so an idle fleet never noticed a dead worker seat. scripts/handover/reconcile-cadence.sh arm [--interval-min N] (cron) runs reconcile-workers.sh --report every N minutes (default 10; N must divide 60, and arm requires timeout so a hung tick cannot overlap the next), appending to ~/.claude/handover/reconcile-cadence.log; arming twice replaces the one tagged job, and disarm/status manage it. Arm from the primary checkout: the job runs the script path it was armed from. Every running seat gets one SEAT line in one of three states. DIED: the recorded pid is confirmed dead and meta.json is older than RECONCILE_GRACE_SECS, so the row is orphaned and a dead holder’s shared lock released (detail=confirmed-dead); a seat that recorded a start but never a pid is also reported DIED once it passes the 48h RECONCILE_UNPROBEABLE_CEILING_SECS backstop (detail=unprobeable-ceiling). NEVER-STARTED: no pid and no started_at was ever written; the row is reaped only past that same ceiling. STILL-RUNNING: everything else (live, settling inside the grace, or unprobeable inside the ceiling), and it is never reaped. A pid recycled to an unrelated live process reads as live, erring toward never reaping. A dead seat is reported within one interval, plus the grace if it died within the grace of its last meta.json write. Test: bash scripts/handover/test-reconcile-cadence.sh.

Migration timeline + open work

  • Initial migration (done) — cross-project subset (luna/, manual_notes.md, random_dreams.md) from himmel/handovers/ → <state-repo>/handovers/.
  • Full centralization (in progress). 234 files migrated from himmel/handovers/<USER_SLUG>/* → <state-repo>/handovers/<USER_SLUG>/ (PR #138). himmel side reduced to a stub README. Remaining: registry.json + SKILL.md updates to make <state-repo> the resolved default for personal-state ops.
  • Bucket layout (done 2026-05-25) — split flat <state-repo>/handovers/<USER_SLUG>/ layout into cross/himmel/luna/luna_brain/ subfolders. 17 standalones + 15 epics routed by ticket prefix. <state-repo>/README.md + SKILL.md resolver updated; bucket layer is opt-in (activated by presence of any bucket dir under <state-root>).

User-slug resolution

Handover bucket paths (<state-root>/<USER_SLUG>/...), registry.json user field, and a few scratch dirs need the operator’s user slug. Resolved by scripts/lib/user-slug.sh:

  1. $USER_SLUG env var (preferred — explicit operator intent).
  2. GitHub username via gh api user -q .login, slugified (the slug is the GitHub user id; one network call, gated behind $USER_SLUG).
  3. git config user.name slugified (kebab-case, lowercase, non-alnum → dash, ≤30 chars) — offline fallback when gh is absent/unauthenticated.
  4. Fail with a hint pointing at .env.example + gh auth / git config setup.

Callers source the lib + call user_slug (raw) or user_slug_verify (prints resolved value + source to stderr; used in setup.sh’s step 0.5 to fail loud at install time).

Doc references to operator-specific paths should use the {{USER_SLUG}} placeholder in templates + <USER_SLUG> in prose. scripts/test-user-slug-resolve.sh smoke-tests all paths (18/18 pass).

This is the Phase 2 envification. Existing <USER_SLUG>-hardcoded references in journal/docs/* paths are out of scope — sweep deferred to a follow-up ticket if/when an alternate operator joins.