Vercel Ship
26

Handoffs and fresh sessions

Each phase of the skill ends with a written handoff and a cutoff. You resume the next phase in a brand-new Claude Code session by naming the handoff file. This lesson covers why, how, and what to do when something goes sideways.

Why fresh sessions

Each phase does heavy work. Phase 1 reads packages, crawls docs, and renders a discovery summary. Phase 2 runs typechecks, grep-resolves, WebFetch calls over foundation URLs, and an optional rendered-site probe. Phase 3 runs a slug-collision check, materializes files, and runs two post-emit scripts.

Running all three in one session works in principle. In practice the context window fills with intermediate state that the next phase does not need. A fresh session clears the slate and gives the next phase its full context budget for the work it actually does.

The handoff file is the bridge. It captures every decision the prior phase made so the new session does not re-ask anything. It is small on purpose. It is not a transcript.

The three handoffs

phase-1.md: Phase 1 close

Written after you confirm the discovery summary. Captures:

  • Slug and target path.
  • Reference project and entry file (when one was supplied).
  • Proposing set (the components you approved).
  • DS package versions and paths.
  • Accepted foundation URLs.
  • The three headline rule candidates, verbatim, with their file:line cites.
  • Design-craft opt-out, only when you excluded it (default ship records nothing).

It does not capture the discovery exploration, raw npm or curl outputs, or per-component deliberation. Those are recoverable from the codebase.

phase-2.md: Phase 2 close

Written before the approval gate, re-written after every validate iteration so the resumed session sees the latest state. Captures:

  • The proof-point line verbatim (props verified, tokens resolved, hallucinations, TOKEN_COVERAGE, CITATION_VERIFICATION).
  • The scratch files Phase 3 will materialize: wiring-extracted.md, examples/*.md, foundations/*.md, tokens-extracted.md, shell-invariants.md when wiring was lifted, probe-recovered-tokens.txt when the source-blocked recover fallback ran.
  • The token-coverage tally.
  • Each [VERIFY] marker with its user-acceptance status (pending, accepted as known limitation, flagged for redo).

It does not duplicate the lifted CSS or the extracted prose. Those live on disk in scratch and the handoff references them by path.

phase-3.md: Phase 3 close

Written after the closing message lands and the optional snapshot step resolves. Distinct from the first two: Phase 3 has no next phase to resume into. The doc is a snapshot for sibling agents (a demo runner, an integration test, a post-extraction reviewer) to act on.

Captures:

  • The produced skill's absolute path.
  • The check-skill-docs.sh tally.
  • Any remaining [VERIFY] markers from the closing message.
  • Suggested follow-up actions: typecheck the consumer app, render a demo, run integration tests against the produced skill.

There is no cutoff and no exit. The session ending after Phase 3 is natural, not enforced.

What a handoff actually looks like

The sections above describe the shape. Here is the real thing: two trimmed excerpts from an actual Primer-React run, the same files the skill writes to .extract-ds-skill-scratch/handoffs/.

Phase 1 → Phase 2: decisions, not a transcript

The heading says it plainly. These are facts the next session cannot recover from the codebase alone, so the handoff writes them down.

# Phase 1 handoff: primer-react
 
## Decisions (irrecoverable from codebase)
 
- Slug: primer-react → .claude/skills/primer-react/
- Reference project: vercel-labs/primer-nextjs-template (entry app/layout.tsx)
- Proposing set (15 components): Button, IconButton, TextInput, Textarea,
  Select, Checkbox, FormControl, Heading, Text, Stack, Label, CounterLabel,
  Flash, StateLabel, BranchName
- DS packages: @primer/react@38.26.0, @primer/primitives@11.9.0,
  @primer/octicons-react@19.28.0
- Headline rules (verbatim):
  1. "BranchName renders <a> by default; pass as="span" for non-link chips"
     (packages/react/src/BranchName/BranchName.tsx:18)
  2. "StateLabel's required status is keyed to the lifecycle octicon map"
     (packages/react/src/StateLabel/StateLabel.tsx:17-23)
  3. "IconButton's aria-label is the accessible name AND the tooltip text"
     (packages/react/src/Button/IconButton.tsx:14,23,33-34)

It ends with the exact command the next session pastes. There is no auto-detection. You name the file.

/extract-ds-skill validate: .extract-ds-skill-scratch/handoffs/phase-1.md

Phase 2 → Phase 3: the proof point

This block is what the approval gate signs off on: verified counts, not guesses.

Validation complete.
- 91 props verified against source (87 positive + 4 octicons + 9 negative),
  with 0 typecheck errors across both validate.sh runs.
- 12 tokens grep-resolved · 12 assets grep-resolved
- 8 foundation-rules extracted (7 cited, 1 [VERIFY]) across 5 files
- 19 cited node_modules paths exist (test -e PASS)
- 5 URLs verified HTTP 200 at extract time
- 0 hallucinations
 
TOKEN_COVERAGE=PASS
CITATION_VERIFICATION=PASS

And its own pickup prompt:

/extract-ds-skill persist: .extract-ds-skill-scratch/handoffs/phase-2.md

A few hundred lines, decisions only, no transcript. That is the whole bridge between two fresh sessions.

How resume works

The skill recognises two resume keywords parsed from your invocation. Both require a path argument. There is no file-existence auto-detection. A leftover handoff in scratch never hijacks a new run.

# resume into Phase 2 from a Phase 1 handoff
/extract-ds-skill validate: .extract-ds-skill-scratch/handoffs/phase-1.md
 
# resume into Phase 3 from a Phase 2 handoff
/extract-ds-skill persist: .extract-ds-skill-scratch/handoffs/phase-2.md

The path is mandatory. /extract-ds-skill validate: with no path aborts with a clear error naming the missing argument.

What happens when you resume

The skill follows a fixed entry procedure:

  1. Read the handoff at the named path. If the file does not exist or is unreadable, the skill aborts with a clear error pointing at the path. There is no silent fallback to Phase 1.
  2. Validate the handoff's shape. The doc must name a slug, a scratch directory, and the phase it was written from. A truncated file or a phase mismatch aborts with a clear error.
  3. Compare the worktree label. If the handoff was written under one dryrun-NN worktree and you are resuming in a different one, the skill emits a one-line warning and continues. This is observability, not enforcement.
  4. Render a one-line resume summary. Resuming from phase-N handoff: slug=<X>, scratch=<path>, <detail>
  5. Enter the named phase directly. The prior phase's summary does not re-render. You are not re-asked for confirmations you already gave.

Handoff filename labels

Handoffs are prefixed with the dryrun label of the worktree they were written from. This makes cross-worktree mistakes visible and keeps parallel dryrun runs clearly separated so you can tell them apart.

Working directory at handoff writeResulting filename
.claude/worktrees/dryrun-06/dryrun-06-phase-1.md
.claude/worktrees/dryrun-15/dryrun-15-phase-2.md
Repo root or any non-dryrun worktreephase-1.md (no prefix)

When no label is applied, the cutoff message says so explicitly so you can rename manually if you intended a label.

The continue-inline override

If you want to push through in one session despite the context cost, reply continue inline at the cutoff prompt. Phase 2 or Phase 3 then enters in the same session.

The default is fresh. Use continue inline only when starting a new session is awkward (no terminal handy, the prior context contains something you want to reference). The default is almost always the right call.

The cutoff message

Both Phase 1 and Phase 2 close with a message in this shape:

Phase 1 complete. Handoff written to
.extract-ds-skill-scratch/handoffs/dryrun-06-phase-1.md.
 
To enter Phase 2 (validation), start a FRESH Claude Code session in
this same directory and run:
 
    /extract-ds-skill validate: .extract-ds-skill-scratch/handoffs/dryrun-06-phase-1.md
 
Starting fresh clears the context window so Phase 2's heavier work
has room to breathe. The handoff captures your decisions so the new
session won't re-ask anything.
 
If you'd rather continue in THIS session despite the context cost,
reply "continue inline" and I'll enter Phase 2 here. Default is the
fresh-session pickup.

The resolved filename appears verbatim in both the "Handoff written to" line and the resume command. Copy the command as-is.

What can go wrong

SymptomLikely causeWhat to do
"Handoff file not found" on resumePath typo, or the prior session ran in a different worktreeRun ls .extract-ds-skill-scratch/handoffs/ and copy the actual filename
"Phase mismatch in handoff"You passed a phase-2.md to validate: or a phase-1.md to persist:Check the file. Use the correct keyword for the phase the handoff was written from
Cross-worktree warningThe handoff was written under one dryrun and you are resuming in anotherRead the warning. Continue if intentional, otherwise switch to the original worktree
You confirmed the wrong slug at Phase 1 and resumed into Phase 2The handoff captured the wrong decisionRestart from Phase 1. Handoffs are decisions, not transcripts. Editing them by hand is fragile

What to take away

  1. Each phase ends with a written handoff and a cutoff. The fresh session is the default for the next phase.
  2. Resume is opt-in. You name the handoff path with validate: or persist:. There is no auto-detection.
  3. Handoffs capture decisions, not transcripts. They are small on purpose.
  4. The Phase 3 handoff is a snapshot for sibling agents, not a resume point.
  5. continue inline exists for the cases where a fresh session is impractical. Otherwise, start fresh.

Primary source: .claude/skills/extract-ds-skill/SKILL.md ("Resume from a prior phase", "Handoff filename labeling", Phase 1/2/3 close sections).