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.jsonare the live source of truth for registered repos, paths, and routing — inspect/change via the/handover repos|register|initslash 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:
- Reads
branch_prefixfrom registry.json for the resolved handover repo (defaulthandover/). - Extracts the first
PROJECT-Nticket from the commit message and derives a slug from the remaining text (lowercase, non-alnum → dash, ≤30 chars). - Switches to
<branch_prefix><TICKET>-<slug>(or<branch_prefix>session-YYYY-MM-DDwhen no ticket is detected) — reusing the branch if it already exists locally or on origin. - Stages + commits .md changes there.
- 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:
python3 -I marketplace/plugins/himmel-ops/scripts/humanizer-facts.py --normalize body.md normalized.mdOnly 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 viagit cherry) → report by default;--cleanupdeletes 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:
ghmissing 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 printshandover-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_DIRunset →<repo>/handovers/. Now this resolves to a near-emptyhimmel/handovers/— the only tracked content is a pair of README stubs (handovers/README.mdand a per-userhandovers/<USER_SLUG>/README.md) that mark the layout; no handover state lives there. Useful only for himmel-scoped legacy code. - Mode B — external:
HANDOVER_DIRset → that path. Recommended default: ahandovers/folder inside your luna-style vault — state becomes part of the knowledge base (wikilinkable, qmd-indexed, graphify-able) with no extra repo to sync:A dedicated external state repo remains fully supported (the resolver is root-agnostic):Terminal window export HANDOVER_DIR="$HOME/Documents/<your-vault>/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 onlyTerminal window export HANDOVER_DIR="$HOME/Documents/github/<state-repo>/handovers"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--cwdrepo. Emitted from the seams that already run between stages (/afk-shiftpost-fanout step 7,/pr-check, the handover writer) — there is no per-step dispatch runtime to hook (inject-initiative.shis SessionStart-only; the auto-ship steps are self-paced prose).resolve— on resume, classifies the stop-point:- FRESH (
exit 0) — breadcrumb’shead_sha== currentHEAD→ deterministic resume; prints the recordednext_step+completed. - DEGRADED (
exit 3) — breadcrumb missing, stale (HEAD/branch moved), or corrupt → reconstructs candidate intent fromgit log(commits naming the ticket) + open Jira, and printsDEGRADED — confirm before proceeding. NEVER silent. Jira enrichment is fail-soft (--jira-cmdoverride /--no-jira; a jira failure degrades to git-only, never crashes).
- FRESH (
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 intocross/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:
$USER_SLUGenv var (preferred — explicit operator intent).- GitHub username via
gh api user -q .login, slugified (the slug is the GitHub user id; one network call, gated behind$USER_SLUG). git config user.nameslugified (kebab-case, lowercase, non-alnum → dash, ≤30 chars) — offline fallback when gh is absent/unauthenticated.- Fail with a hint pointing at
.env.example+gh auth/git configsetup.
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.