Appearance
Background warmup
Starting state
A Rust workspace — three crates. From a fresh cargo fetch, the first cargo build --workspace on your laptop takes about 4 minutes. You've already adopted Toolchain bootstrap, so daft start feature/x populates the registry cache before returning. That part's good.
What's still bad: the first cargo run (or cargo test) in a fresh worktree still pays those 4 minutes. Every worktree. You started the worktree because you wanted to context-switch fast, and now you're watching the cursor blink for as long as it would have taken to deal with the original task. By the time it's done, you've already lost the thread.
The reach for daft: do the slow work while you're still opening the editor. By the time your first edit is ready, the cache is warm and the incremental compile is a few seconds.
What changes
daft.yml gains a second job — backgrounded, downstream of the install. Worktree creation still returns as soon as the install finishes; cargo build keeps running, detached, after the daft command exits. You drop into the new worktree right away.
What you don't get: a guarantee. Background warmups are an optimization, not a correctness contract. The job can fail, get cancelled, or simply be slower than your first edit. None of that breaks anything; the worst case is that the first build still pays the slow path. Background warmup is the kind of automation you can add and forget about.
Recipe
yaml
# daft.yml
hooks:
worktree-post-create:
jobs:
- name: fetch-deps
run: cargo fetch --locked
- name: warmup-build
run: cargo build --workspace
background: true
needs: [fetch-deps]background: true detaches the job from worktree creation. The hook returns as soon as fetch-deps finishes; warmup-build keeps running. You're typing in the new worktree while the compiler grinds.
needs: [fetch-deps] ensures the build doesn't race the fetch. Without it, cargo build would try to download crates that the parallel fetch is already pulling.
The default fail mode for worktree-post-create is warn, so a failed warmup never blocks worktree creation. That's the right default — a warmup is an optimization, and an optimization that occasionally fails is still a net win.
Variants
By tool. The shape is the same — background: true, needs: the install — only the run: line changes.
Rust — debug binary, scoped or workspace
yaml
# Whole workspace — simplest, slowest
- name: warmup-build
run: cargo build --workspace
background: true
needs: [fetch-deps]
# Scoped to packages you work on most — faster, less complete
- name: warmup-build
run: cargo build -p server -p worker
background: true
needs: [fetch-deps]--workspace is the right default for small/medium repos. For a big multi-crate workspace where each developer focuses on a couple of packages, scoping pays off — it cuts CPU time and battery drain at the cost of cold-compiling whatever you didn't pre-build.
The build cache (target/) is per-worktree by design — sharing it silently corrupts artifacts. For sharing compiled output, use sccache (next variant), not CARGO_TARGET_DIR. The full failure modes are at Anti-pattern: shared mutable state.
Go — build cache priming
yaml
- name: warmup-build
run: go build ./...
background: true
needs: [fetch-modules]Go's build cache ($GOCACHE) is shared across worktrees by default and content-addressed, so a warmup in one worktree pre-compiles for every other one too. The cleanest cache model of any major toolchain.
Vite / Next.js — dep optimizer prime
yaml
- name: warmup-vite
run: pnpm exec vite optimize --force
background: true
needs: [install-deps]
root: apps/webVite's dep optimizer is the slow part of the first vite dev. Running vite optimize ahead of time means the dev server starts hot. root: is a per-job working directory — useful when the warmup target is a single app inside a monorepo.
Gradle — daemon spin-up
yaml
- name: warmup-gradle
run: ./gradlew --no-daemon dependencies
background: true
needs: [install-deps]Gradle's biggest cold-start cost is dep resolution and configuration. ./gradlew dependencies resolves the full graph and primes the configuration cache.
sccache — share the compile work, not the artifacts
yaml
- name: warmup-build
run: cargo build --workspace
background: true
needs: [fetch-deps]
env:
RUSTC_WRAPPER: sccache
SCCACHE_DIR: ${HOME}/.cache/sccachesccache is content-addressed by source-and-flags, so a warmup in worktree A primes the cache for worktree B. The target/ directories stay per-worktree (correct), but the actual compile work runs once. The bigger your workspace, the more this pays off.
Idempotency & safety
Warmups are idempotent by construction — building twice with no source changes is a near-no-op. Two specific concerns are worth being explicit about:
Cancellation. Removing the worktree while a warmup is running sends SIGTERM to the job's process group. cargo, go, and gradle all unwind partial work cleanly. If your warmup is a custom script that holds long-lived locks (a daemon, a database connection), trap the signal and clean up explicitly.
No critical work in a warmup. Anything required for correctness must run synchronously. The background: true job can fail, get cancelled, or finish late — depending on it for correctness produces flaky worktrees. The most common mistake is putting code generation here.
Don't run code generation in a warmup
If your project generates source files at build time (proto, GraphQL schema, OpenAPI client), code that imports the generated module breaks if the codegen isn't done. Run codegen synchronously as part of the install, not as a backgrounded warmup.
Where to next
- Toolchain bootstrap — the install job a warmup
needs:. Always upstream from warmup; can't skip it. - Sharing caches across worktrees — sccache, ccache, the Go build cache, and what makes them safe to share when
target/andnode_modules/aren't. - Job orchestration —
background,needs,parallel,priorityreference for composing multi-step hooks.