Migration Runbook

On this page

Use this when bringing a downstream project into sync with the claude-quickstart template — a first migration onto the deterministic-first layout, or an ongoing re-sync after a template update. It has two parts: the engine-driven happy path (Part 1), and the mature-repo hazards the engine cannot know about because they live outside the manifest’s reconciliation surface (Part 2). The engine makes the synced surface safe; Part 2 is everything else.

Part 1 — The engine-driven flow (the happy path)

  1. Bring the engine into sync first. The check-docs engine is the versioned checkdocs crate (docs/adrs/adr-002-checkdocs-engine-crate.adoc), not copy-pasted source. Depend on it by git tag in xtask/Cargo.toml:

    checkdocs = { git = "https://gitlab.com/gadhs/templates/claude-quickstart.git", tag = "checkdocs-vYYYY.N" }

    On a first add, paste that into xtask/Cargo.toml and cargo build (or cargo fetch) pulls it — there is nothing to update yet. On a later engine bump, change only the tag = value, then cargo update -p checkdocs. The tag matches the engine’s ENGINE_VERSION.

    On a first migration only, hand-copy the engine SEAM — and ONLY the seam — once: the three thin wrappers xtask/src/cmd/{check_docs.rs, validate.rs, audit_claude_md.rs} (plus their mod.rs variants + main.rs match arms), the root Cargo.toml [workspace.lints] managed region, and the checkdocs = { git, tag } dep line above. Each wrapper is ~15 lines mapping the crate’s Outcome to an exit code. Do NOT bulk-copy the template’s xtask/ — a mature repo’s own commands, dependencies, and lint overrides live there, and a wholesale copy clobbers them. xtask is per-project source (not synced), so this is a one-time pull; thereafter engine updates are tag bumps.

    NOTE

    Three surfaces, three delivery mechanisms — don’t conflate them:

    • Engine logic (checkdocs) — pinned by git tag (checkdocs-v2026.10); updated by a tag = bump.

    • Synced content (rules, standards pages, hooks, tool-configs, the CLAUDE.md preamble) — fetched LIVE from the template’s main and reconciled by check-docs --fix; it tracks latest, NOT the pinned tag.

    • Per-project xtask source (the thin wrappers above) — a one-time hand-copy; neither tagged nor synced.

    So the engine tag (v2026.10) and the manifest’s manifest_version (v2026.15) are INDEPENDENT axes — the tag is the engine code, manifest_version is the synced-content stamp; they advance on different schedules and are expected to differ.

  2. Run the report. cargo xtask check-docs. Read it. The 3-state exit contract: 0 = in sync; 3 = advisory (an active sync-overrides entry, or a nothing-verified offline/transition run — not blocking); 1 = a real violation (drift, a neutered hook, missing mandatory content, an expired/unknown override, or a manifest/handshake error).

  3. Reconcile. cargo xtask check-docs --fix --yes --allow-exec. --allow-exec is REQUIRED for the git-hook entries (the engine refuses to write executable artifacts described by a remote manifest without it). --fix exits 0 after a clean repair of the byte-synced + managed-region entries — with one first-migration nuance: a .claude/CLAUDE.md lacking the claude-quickstart:managed markers is a full_restore_on_missing_markers = false entry, so --fix CANNOT insert the markers. Because the preamble entry carries missing_markers_severity = "advisory", check-docs (engine v2026.9+) reports it as a non-blocking advisory nudge (exit 3), not a blocking violation — add the markers by hand once (see Thin the CLAUDE.md below) to start receiving preamble syncs. (An engine pinned before v2026.9 still treats it as a blocking violation — bump the tag.) Review with git diff, then commit (chore: sync universal standards to template vYYYY.N).

Part 2 — Mature-repo hazards the engine cannot know

(a) .gitignore may hide new .claude/ artifacts

A mature downstream’s .gitignore may ignore more than the shipped .claude/settings.local.json. After --fix, confirm the new files are actually staged:

cargo xtask check-docs --fix --yes --allow-exec
git status --porcelain .claude/ ; git diff --cached --stat

If .claude/rules/ (or any synced path) is missing from the staged set, an ignore rule is swallowing it — un-ignore / force-add it.

(b) Retiring .claude/docs/ is dangerous — grep the WHOLE repo first

.claude/docs/ is retired (docs/adrs/adr-001-antora-distribution.adoc). Before rm -rf .claude/docs/, grep the ENTIRE repo (code AND docs) for residual references, and confirm each retired file’s prose actually landed in docs/modules/ROOT/pages/*.adoc:

grep -rn '\.claude/docs' .          # references anywhere (code, CI, docs)

Audit a unique content tail of each retired file against its new ROOT page — the engine does NOT verify hand-migrated prose (see (i)).

(c) Hooks are wholly replaced (full-restore) — re-add project content outside the markers

The three git-hook entries set full_restore_on_missing_markers = true, so --fix rewrites the ENTIRE hook file from canonical. The splice/restore only ever touches bytes BETWEEN the # >>> claude-quickstart:managed >>> / # <<< claude-quickstart:managed <<< markers. Any project-specific hook logic (reseed, asset build, extra security/perf checks) MUST live OUTSIDE the markers (after the closing marker line); content there is never touched. Re-add it after the first --fix if the downstream had customized a hook.

(d) Nested-engine + visibility contract differences

  • The engine returns a typed Outcome; only the thin check-docs wrapper maps it to a process exit code (no process::exit inside the engine — it composes). If you call the engine from your own tooling, map the Outcome yourself; do not expect it to exit the process.

  • The visibility-exception expiry in validate fails CLOSED: when the date command is unavailable its "today" is 9999-12-31, so every exception_expires reads as expired and a PRIVATE repo is refused. This is the OPPOSITE of check-docs, whose missing-date sentinel 0000-00-00 fails OPEN (no override expires). A private downstream must ensure date (or PowerShell on Windows) is available in CI, or its visibility gate will refuse to pass.

(e) Measure the FULL clippy surface across ALL feature views first

The [workspace.lints] block denies the pedantic AND cargo groups plus an explicit list. Before estimating migration effort, measure clippy across every feature combination, not the single default view:

cargo clippy --all-targets --all-features -- -D warnings
# plus each meaningful feature combo your crates expose

A default-view-only count badly under-estimates the work.

(f) Macro-generated code × pedantic lints explodes — scope an allow at the generation site

Pedantic lints fire inside derive/macro expansions you do not author. Do NOT edit or document generated code; place a scoped, reason-bearing allow at the generation site (the module/item that invokes the macro), mirroring the xtask crate-root carve-out:

#[allow(clippy::some_pedantic_lint, reason = "macro-generated; not our source")]

(g) The test carve-out does NOT reach integration-test crates

clippy.toml sets only allow-unwrap-in-tests / allow-expect-in-tests, and a per-lib #![cfg_attr(test, allow(…​))] covers that lib’s unit tests. A SEPARATE integration-test crate (tests/) gets the full denied set — indexing_slicing, arithmetic_side_effects, missing_docs_in_private_items, let_underscore_must_use, etc. Add the carve-out at the top of each integration crate as needed; do not assume the unit-test relaxation extends to it.

(h) Config-critical universal files carry project content — diff before overwrite

These are immutable-hashed and --fix will OVERWRITE them: rust-toolchain.toml (a project may carry an extra target, e.g. wasm32), .config/nextest.toml (concurrency / test-threads / profiles), and .gitattributes (LFS rules a downstream added). git diff each before accepting the restore. If a divergence is intentional, record a .claude/sync-overrides.toml entry (the run then reports exit 3 advisory instead of clobbering on every sync).

(i) Hand-migrated prose has un-gateable fidelity risk — name it

Moving .claude/docs/.md prose into Antora docs/modules/ROOT/pages/.adoc is byte-for-byte UNVERIFIABLE by the engine: it gates synced files, not the operator’s hand-migrated ROOT pages. Fidelity here is a manual review responsibility, not a gated one — read the old and new side by side.

(j) A git dependency on checkdocs may trip cargo-deny

Adding the checkdocs = { git = … } dep introduces a git source. If your deny.toml sets [sources] unknown-git = "deny" (a hardened posture), cargo deny check sources FAILS until you allow it:

[sources]
allow-git = ["https://gitlab.com/gadhs/templates/claude-quickstart.git"]

With the template default (unknown-git = "warn") you get a warning, not a failure — but allow-listing the source silences it cleanly.

Thin the CLAUDE.md

A migrating project’s .claude/CLAUDE.md is usually bloated. Cut it to the canonical structure in CLAUDE.md Skeleton:

  1. Remove restated rule prose (it lives in .claude/rules/*).

  2. Remove duplicated standards text (it lives in docs/modules/standards/*).

  3. Remove status / TODO / next-steps logs (they live in GitLab work items).

  4. Keep only the managed preamble + the project-owned section bodies.

  5. Leave the claude-quickstart:managed preamble markers intact (they re-sync).

Then cargo xtask audit-claude-md should report no duplicated-rule headers and the file under the size soft-limit. (The gate’s logic now lives in the checkdocs crate, so it arrives with the tag pin + the thin audit_claude_md.rs wrapper from the seam copy — no hand-port of ~345 lines.)

The e2e / perf gates after migration

cargo xtask e2e and cargo xtask perf ship as stubs that exit 4 (NOT_CONFIGURED) until you wire a suite. The pre-push hook soft-skips a not-configured suite, so a UI-less service or a fresh scaffold passes its own gate. Two equally-valid choices for a project with no such suite:

  • keep the stub (it exits 4 → soft-skipped), or

  • delete the subcommand from xtask (clap then exits 2 → ALSO soft-skipped).

Either way pre-push passes; a REAL e2e/perf failure exits via its own non-2/non-4 code and still blocks. Wire a real suite by editing xtask/src/cmd/{e2e,perf}.rs.

Edit this page · latest