Writing session handoffs

Write a handoff that a fresh session can pick up and finish, and let it lint clean once the work is done.

What a handoff is

A handoff is a ready-to-run prompt for the next session, one file per handoff under docs/handoffs/<NNN>-<name>.md. It is never deleted. A session that picks one up sets its status to in_progress, then to consumed once the work lands.

1. Add the namespace

ctxgrd pack add handoff

This writes [HANDOFF] into ctxgrd.toml, path-claimed at docs/handoffs/**. If your project already uses the workflow pack for SPEC/TASK/PROMPT, you already have it – workflow depends on handoff, so ctxgrd pack add workflow writes both:

Added pack 'handoff' — 1 namespace, 0 rule families.
Added pack 'workflow' — 3 namespaces, 2 rule families.

2. Scaffold one

ctxgrd new HANDOFF "Ship the widget"

This writes docs/handoffs/001-ship-the-widget.md with an id, an empty status, an empty depends_on: [], and five H2 headings: Goal, Context, Next Steps, Routing, Verification. Goal, Context, Next Steps and Routing start empty. Verification already holds a placeholder sh fence:

## Verification

```sh
# the command that proves the work landed
```

Replace the placeholder comment with the real command.

3. Fill in the required headings

While a handoff is open, all five headings must hold content. Goal, Context, Next Steps, and Routing take prose:

---
id: HANDOFF-1
title: Ship the widget
status: pending
---

# HANDOFF-1: Ship the widget

## Goal

Ship the widget to production.

## Context

The widget passed review.

## Next Steps

Deploy it.

## Routing

developer

4. Put a runnable command under Verification

Verification is different: it must contain a fenced code block, not prose. Under the ## Verification heading, put the command in its own fence:

curl -sf https://widget.example.com/health

A handoff whose Verification holds only prose reports:

error[core.required-headings]: section 'Verification' contains no fenced code block
  help: put the command a reader runs in a fenced block under `## Verification`

A fresh session runs that command to find out whether the work is actually done, instead of taking your word for it.

5. Pick a status from the fixed vocabulary

ctxgrd rules core.allowed-values

[HANDOFF] allows five values: pending, in_progress, consumed, superseded, deferred. There is no done or open – done would duplicate consumed and open would duplicate pending.

6. Let a closed handoff stay as it is

The heading and code-block checks apply only while status is pending or in_progress. A handoff with no status at all is still checked – leaving it out is not the same as closing it. Once you mark a handoff consumed, superseded, or deferred, it can keep whatever shape it already has:

$ ctxgrd
ok: 1 document · 10 rules · 0 diagnostics

This holds even for a consumed handoff missing every heading. It is a record of finished work, not a live document, so nothing more is asked of it.

7. Watch the size budget

[HANDOFF] warns past 20,000 characters, because a fresh session loads a handoff whole:

warning[core.file-budget]: file is 21112 characters (budget 20000)
  help: trim 1112 characters — move settled detail into a linked file and reference it, or archive the parts that are done

It is a warning, so it never fails your build. Trim the handoff, or move settled detail into a file it links to.

8. Claim a handoff before you run it

When several sessions share one checkout, each one claims a handoff before it starts, so no two sessions run the same one:

ctxgrd claim HANDOFF-12                       # exit 0: this session holds it
ctxgrd claim HANDOFF-12                       # again later: renews claimed_at
ctxgrd release HANDOFF-12 --status consumed   # done; or --status pending

claim writes status: in_progress, claimed_by, claimed_at and, from $CLAUDE_CODE_SESSION_ID or --session, claimed_session. It changes no other line. It exits 1 and changes nothing when another session holds a live claim; move on to other work. A claim whose holder has been idle for more than 7 days is reported as abandoned by ctxgrd status and by the core.claim-stale warning. Take one over only after checking that no session is still working on it:

ctxgrd claim HANDOFF-12 --take-over

An in_progress handoff with no claimed_by or claimed_at fails core.claim-complete.

9. Lint and iterate

ctxgrd
ctxgrd --format json | jq '.diagnostics[] | select(.file | startswith("docs/handoffs"))'

Exit codes follow the standard contract: 0 clean, 1 diagnostics reported, 2 kernel or config error.

Reusing the status-scoped check elsewhere

statuses and code_in are general keys on core.required-headings, not specific to handoffs. Any namespace with a status lifecycle can require headings, or a code block in one of them, only while a document is in a given state:

[RUN."core.required-headings"]
headings = ["Trigger", "Steps", "Rollback"]
statuses = ["active"]
code_in = ["Rollback"]

Run ctxgrd rules core.required-headings for the full parameter list.

Next steps