name: create-migration description: Scaffold a new Clack migration file. Use when you need to create a migration for config changes, data format updates, or other upgrade tasks.
Create a new migration for Clack's boot migration system.
Input: The argument after /create-migration describes what the migration should do (e.g., "rename config field X to Y", "add new required config field").
Steps
If no input provided, ask what the migration should do
Use the AskUserQuestion tool to ask:
"What should this migration do? Describe the change needed."
Read existing migrations to determine next version
Read
src/migrations/index.tsto see which migrations are registered. If migrations exist, read them to find the highest version number. The new migration version = highest existing version + 1, or 1 if none exist.Determine priority
Use the AskUserQuestion tool to ask:
"Is this migration blocking or enhancement?"
Options:
- Blocking: Must complete before Clack starts (e.g., breaking config changes)
- Enhancement: Can run in background after boot (e.g., optional improvements)
Determine file scope
Based on what the migration does, identify which files it needs read/write access to. Common files:
data/config.json— for config schema changesdata/state/roles.json— for role data changesdata/state/user-preferences.json— for user preference changes
Determine migration type: static vs Claude
Decide based on the nature of the change:
- Static (use
staticfunction): For deterministic JSON transforms — adding fields, removing fields, renaming keys, mapping values. These run as TypeScript functions without invoking Claude. Use static when the transformation has no ambiguity and doesn't touch markdown/text files that may have user customizations. - Claude (use
prompt): For markdown/text file changes that require merging content into user-customized overrides, or complex transformations where AI judgment helps. - Mixed (both
staticandprompt): When you need deterministic JSON changes AND Claude-powered text changes. The static transform runs first, then Claude sees the updated files.
Default to static for JSON-only config changes. These are faster and more reliable.
- Static (use
Create the migration file
Create
src/migrations/NNN-<kebab-name>.tswhere NNN is the zero-padded version.For static migrations (JSON-only changes):
import type { Migration } from "./types.js"; export const migration: Migration = { version: <next-version>, name: "<human-readable name>", priority: "<blocking|enhancement>", files: [<list of files>], static: (files) => { const result: Record<string, string> = {}; for (const [path, content] of Object.entries(files)) { if (!path.endsWith("<target-file>") || !content) continue; const data = JSON.parse(content); // ... transform data ... result[path] = JSON.stringify(data, null, 2) + "\n"; } return result; }, };Static function rules:
- Identify files by path suffix (e.g.,
path.endsWith("config.json")) — not by exact path - Return only files that were actually modified
- Handle "already migrated" by checking if the change is needed and returning early
- Use
{ delete: true }as the value to delete a file
For Claude migrations (markdown/text changes):
import type { Migration } from "./types.js"; export const migration: Migration = { version: <next-version>, name: "<human-readable name>", priority: "<blocking|enhancement>", prompt: `<detailed prompt for Claude to execute the migration>`, files: [<list of files>], };The prompt should be specific and actionable:
- Describe exactly what to change
- Include the expected before/after state
- Handle edge cases (field doesn't exist, already migrated, etc.)
- Identify files by path suffix (e.g.,
Register in barrel export
Update
src/migrations/index.tsto import and include the new migration:import { migration as mN } from "./NNN-<name>.js"; // Add to the migrations arrayCreate test file
Create
scripts/migration-tests/NNN.tswith test cases for the new migration:import type { MigrationTest } from "./types.js"; export const test: MigrationTest = { version: <version>, cases: [ { name: "<describe what this case tests>", input: { /* config in the OLD format (before migration) */ }, validate: (output) => { // Return null if passed, error string if failed // Check that the migration transformed the config correctly return null; }, }, // Always include an "already migrated / no-op" case ], };Test case guidelines:
- Cover each transformation the migration performs (one case per variant)
- Include an "already migrated" case that verifies no-op behavior
- Include a mixed case if the migration handles multiple items (e.g., repos array)
- Validation should check both positive (new fields exist) and negative (old fields removed)
Register test in runner
Update
scripts/migration-tests/run.ts:- Add import:
import { test as testNNN } from "./NNN.js"; - Add to
allTestsarray:const allTests: MigrationTest[] = [..., testNNN];
- Add import:
Update the full-path test
In scripts/migration-tests/run.ts:
- Update
VERSION_0_CONFIGif needed (it should represent the oldest supported format) - Update
validateFinalState()to verify the output after ALL migrations including the new one
Run the tests
npx tsx scripts/migration-tests/run.tsVerify all individual tests and the full-path test pass.
Show summary
Display:
- Migration file path
- Test file path
- Version number
- Priority
- Type (static, Claude, or mixed)
- File scope
- Number of test cases
Guardrails
- Migrations MUST be idempotent — running twice should be safe
- The prompt should handle "already migrated" cases gracefully
- Version numbers must be sequential with no gaps
- File scope should be minimal — only list files the migration actually needs