Apply pending Accelerator meta-directory migrations to bring a repo into line with the latest plugin schema. Destructive by default but guarded — refuses to run on a dirty working tree and prints a one-line preview per pending migration before applying.
Installs into .claude/skills of the current project.
Are you the author of Migrate?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/atomicinnovation-migrate)
---
name: migrate
description: Apply pending Accelerator meta-directory migrations to bring a repo into line with the latest plugin schema. Destructive by default but guarded — refuses to run on a dirty working tree and prints a one-line preview per pending migration before applying.
allowed-tools:
- Read
- Write
- Edit
- Bash(accelerator migrate *)
---
> **Warning: this skill rewrites files in `meta/` and `.claude/accelerator*.md`.** Recovery is via VCS revert. Before running, ensure your repo is committed and you understand what each pending migration does. The safety guards (clean-tree check, preview) exist to give you a moment to stop — they are not a substitute for understanding the changes.
## When to invoke
Run `/accelerator:migrate` after upgrading the Accelerator plugin to a version that bundles new migrations. The SessionStart hook will tell you when this is needed:
```
[accelerator] .accelerator/state/migrations-applied is behind the plugin
(highest applied: 0001-rename-tickets-to-work; highest available: 0002-...).
Run /accelerator:migrate to bring it up to date.
```
You can also run it proactively — if no migrations are pending, it prints `No pending migrations.` and exits cleanly.
**Upgrade sequence.** After pulling a new plugin version, run `/accelerator:migrate` before invoking any skill that reads or writes paths affected by pending migrations. Skills do not gate themselves on pending migrations; the SessionStart hook only warns when migrations are pending. Running write-side skills (e.g., `/accelerator:research-codebase`) between the plugin upgrade and the migration may produce results written to or read from new-default paths that do not yet exist on disk.
## How it works
`accelerator migrate` (the `accelerator-migrate` sub-binary) runs the migration lifecycle in-process — there is no forked child per migration and no wire protocol to a subprocess.
1. **Clean-tree pre-flight.** Scans `meta/`, `.claude/accelerator*.md`, and `.accelerator/` for uncommitted changes. A clean tree proceeds with a fresh run. A dirty tree where every dirty path belongs to this run's own prior partial output (a resumable interactive session log, in-flight manifest-tracked writes) proceeds too, printing a one-line resume affordance. A dirty tree carrying anything else aborts. Set `ACCELERATOR_MIGRATE_FORCE=1` to bypass entirely (advanced users only) — skipped migrations stay skipped even under FORCE. A run-level advisory lock (`.accelerator/state/`) refuses a second concurrent invocation fast rather than racing it.
2. **Read state.** Loads `.accelerator/state/migrations-applied` and `.accelerator/state/migrations-skipped` — newline-delimited lists of migration IDs. If either file is absent, its set is empty. Unknown IDs (from a newer plugin version) are preserved verbatim and warned about. An ID appearing in both files triggers a warning; applied takes precedence.
3. **The registry.** Migrations are not discovered by scanning a directory of scripts — they are a fixed, compile-time-ordered list (`migrate::registry::registry()`) baked into the binary. `ACCELERATOR_MIGRATIONS_DIR` is not recognised; there is no directory to override.
4. **Compute pending.** A migration is pending if its ID is in neither the applied nor the skipped set.
5. **Preview banner.** Prints one line per pending migration — `<ID> — <description>` — with a per-migration skip hint (`--skip <id>`). If nothing is pending, prints `No pending migrations.` (plus any skipped names) and exits 0 immediately.
6. **Apply in order.** For each pending migration: a mechanical migration's `apply()` runs to completion in one call; an interactive migration (see below) runs the full accept/edit/skip prompt loop. On success, atomically appends its ID to `.accelerator/state/migrations-applied` and prints `[<id>] applied`. On failure, prints the error and `[<id>] failed`, exits 1, and leaves the state file at the last successful migration. A migration whose `apply()` returns `NoOpPending` is treated as a soft skip — it stays pending, printing `[<id>] no-op (stays pending)`, and will be retried on future runs.
7. **Summary.** Prints counts of applied, skipped, and pending (no-op) migrations.
## Authoring a migration
A migration is ordinary Rust, not a bash script — there is no opt-in header, no published shell hook set, and nothing to source. Add a module under `cli/migrate/src/migrations/` and register it in `cli/migrate/src/registry::registry()`.
```rust
pub trait MigrationMeta {
fn id(&self) -> &'static str;
fn description(&self) -> &'static str;
}
pub enum ApplyOutcome { Applied, NoOpPending }
pub trait Migration: MigrationMeta {
fn apply(&self, ctx: &dyn MigrationContext) -> Result<ApplyOutcome, MigrationError>;
}
```
`ctx: &dyn MigrationContext` is the only way a migration touches the outside world — `migrate` itself carries no filesystem, config, or VCS dependency. The capabilities it exposes (all in `cli/migrate/src/ports.rs`, with a full doc comment on each):
- `doc_type_dirs()` — the configured doc-type → directory table (the in-process replacement for shelling out to read config).
- `revision()` — the current VCS revision, used for guarded-resume staleness checks.
- `corpus_index()` — target-existence checking (`target_exists(type, id)`), for migrations that validate cross-references.
- `write(path, content)` — the **only** content-mutation path. Every write routes through the path manifest as a side effect of the call itself, so "recorded at time-of-mutation" is structural, not a discipline to remember.
- `config_value(key)` / `configured_path_override(key)` — full-stack config lookup, including legacy key names migrations still read.
- `read(path)`, `dir_exists(path)`, `remove_file(path)`, `remove_dir_if_empty(path)`, `list_md_files(dir)`, `list_all_under(dir)` — filesystem access, sandboxed and manifest-aware.
- `merge_move(src, dst)` — whole-file/whole-directory relocation (directory renames, state relocation).
- `canonicalise_work_item_id(bare_number)` — renders a bare number under the configured `work.id_pattern`.
- `validate_frontmatter(files)` — runs `accelerator corpus frontmatter validate` in-process.
A mechanical migration must self-detect and no-op on already-applied state (idempotent) — the ledger filter is not the only guard, matching ADR-0023's belt-and-suspenders requirement. Return `ApplyOutcome::NoOpPending` when its preconditions are not yet met and it should stay pending, retried on future runs; guarantee no destructive work happened before returning it. There is no `DRY_RUN` concept — this framework has no dry-run mode.
## State file format
`.accelerator/state/migrations-applied` contains one migration ID per line, in the order migrations were applied:
```
0001-rename-tickets-to-work
0002-some-future-migration
```
`.accelerator/state/migrations-skipped` contains one migration ID per line for migrations the user has chosen to defer:
```
0002-some-future-migration
```
Both files are human-readable and constitute the audit trail. Do not edit them manually unless you are deliberately marking a migration as applied, unapplied, or skipped.
## Skip-tracking
Skip a migration to defer it indefinitely:
```bash
accelerator migrate --skip <migration-id>
```
Unskip a previously skipped migration so it becomes pending again:
```bash
accelerator migrate --unskip <migration-id>
```
Skipped migrations never run and do not block other pending migrations. The pre-run banner includes a `--skip` hint for each pending migration. `ACCELERATOR_MIGRATE_FORCE=1` bypasses the dirty-tree pre-flight only; skipped migrations remain skipped even with FORCE.
## Optional interactive contract
Mechanical migrations are the default — they implement `Migration` and run start-to-finish with no user interaction. A migration that needs to **ask the user about ambiguous transformations** implements `InteractiveMigration` instead:
```rust
pub struct Transformation {
pub key: String, // session-log identity, "{path}#{anchor}"
pub path: String,
pub anchor: String,
pub proposed: String,
pub predicate_value: String,
pub display: String, // author-declared prompt content
pub extras: Vec<(String, String)>,
}
pub enum PredicateOutcome { Prompt, Mechanical, Fail(String) }
pub enum Decision { Accept, Edit(String), Skip }
pub trait InteractiveMigration: MigrationMeta {
fn emit_transformations(&self, ctx: &dyn MigrationContext) -> Vec<Transformation>;
fn evaluate_predicate(&self, t: &Transformation) -> PredicateOutcome;
fn validate_edit(&self, t: &Transformation, value: &str) -> Result<(), String>;
fn apply_decision(&self, t: &Transformation, d: &Decision, ctx: &dyn MigrationContext) -> Result<(), String>;
fn verify_applied(&self, t: &Transformation, recorded: &corpus::Record) -> bool { true }
fn finalise(&self, ctx: &dyn MigrationContext) -> Result<(), String> { Ok(()) }
}
```
There is no wire protocol, no TSV frames, no base64 display blocks, and no hand-written JSON — every callback is a plain Rust function call against typed values.
### Callback contracts
- `emit_transformations` runs once, unconditionally, before any prompting — mechanical work (a precondition pass, a rewrite pass) happens here. It has no `Result` of its own; a mechanical failure is carried as a single sentinel `Transformation` whose `evaluate_predicate` returns `Fail(message)`.
- `evaluate_predicate` routes a transformation to the prompt loop (`Prompt`), applies it mechanically with no prompt (`Mechanical`), or aborts the whole migration with `[<id>] <message>` (`Fail`).
- `validate_edit` returns `Err(message)` to reject a user's edit; the engine prints `"[interactive] {message}"` and re-prompts. No record is persisted for a rejected attempt.
- `apply_decision` is called once per `accept` or `edit` decision, **after** the engine has durably persisted the session-log record (write-ahead-log invariant) — enforced structurally, never the reverse. It is **never** called for `skip`.
- `verify_applied` (optional; defaults to `true`) is consulted on resume, before replaying an accepted/edited record. `false` is handled identically to source drift: the stale record is discarded and re-prompted. Never consulted for a skipped record.
- `finalise` (optional; defaults to a no-op) is called exactly once, unconditionally, after every transformation this run decided has been applied — or immediately if there were none to begin with. This is where whole-corpus post-apply validation belongs; it is not threaded through any one transformation's own callback.
> **Callbacks may be invoked more than once per run, so they must be deterministic and side-effect-free.** A `--decisions-file` run calls `emit_transformations`/`evaluate_predicate` during `--list` enumeration and again during the dry-apply validation pass before the live run, and calls `validate_edit` during dry-apply and again at live apply. `validate_edit` in particular **must be a pure function of its arguments** — it must not read corpus state an earlier transformation in the same run could mutate, or dry-apply could pass against the unmutated corpus while the live run fails later after earlier files changed.
### Runner guarantees
- **Predicate routing**: each transformation is routed to the prompt loop or the mechanical path based on `evaluate_predicate`'s outcome.
- **Display elements**: every prompt shows the proposed value, the source location as `path:anchor`, the predicate's evaluated value, plus the author's own `display` content.
- **Resumability**: every prompted decision is durably persisted to `.accelerator/state/migrations-<id>-session.jsonl` **before** the artefact is mutated. On re-entry, already-decided keys replay silently (`verify_applied` consulted for accepted/edited keys only) and skip the prompt. A migration completes (its ID is appended to `migrations-applied`) only when every transformation has a terminal record.
- **Decision verbs**: `accept` applies the proposed value; `edit <value>` substitutes a user-provided value (validated by `validate_edit`); `skip` records the decision but does not mutate the artefact.
### Runner-level decisions
- **Source drift**: if a recorded record's `proposed_value` differs from the live emission's, the engine discards the old record and re-prompts — this applies to every resumed record regardless of outcome (accepted, edited, or skipped alike), and takes priority over everything else, including sticky skip.
- **Transformation ordering**: emission order from `emit_transformations` is the canonical iteration order.
- **Sticky skip**: a transformation skipped on a prior run remains skipped on every subsequent run, unless its source drifts. Delete the corresponding session-log line to re-prompt it.
- **The 30-second decision timeout**: a live TTY prompt that receives no answer within 30 seconds fails the run (stderr message, exit non-zero, session log left as of the last completed decision — the next run prompts only what's still undecided). This is new behaviour the Rust port introduces, not a port of anything bash did — bash's own terminal read had no timeout at all. It is not user-configurable (no flag, no config key). When there is no TTY and no decisions file at all, the run stalls immediately without arming any timeout — see the structured stall below.
### Session log
- Path: `.accelerator/state/migrations-<id>-session.jsonl`. Relative paths in `--decisions-file`/stall messages are resolved against the project root.
- One JSON object per line, canonical field order: `transformation_key`, `schema_version: 1`, `outcome` ∈ `{accepted, edited, skipped}`, `proposed_value`, optional `user_value` (only for `edited`), `timestamp`. Author-declared `extras` on a `Transformation` are not persisted to the session log — they exist only within the run (e.g. `--list`'s short-key display reads a `linkage_key` extra).
- The session log is retained as an audit artefact after full completion; users may delete it manually. The runner refuses to resume from a log with an unknown `schema_version` and prints a clear recovery instruction.
## Worked example
`meta/work/0001-improve-startup-time.md` references another work item in an unstructured `## References` section:
```markdown
## References
- `meta/work/0042-add-a-caching-layer.md`
```
Migration `0007-unify-meta-corpus-frontmatter` recognises this as an ambiguous body-section linkage — a `relates_to` reference it can propose but must not apply without confirmation. `--list` reveals it, dry-emitting the pending transformation without mutating anything:
<!-- @list-output-start -->
```
1 relates_to relates_to=work-item:0042 meta/work/0001-improve-startup-time.md:body:references#0
```
<!-- @list-output-end -->
Writing `accept` to a decisions file and resuming with `--decisions-file` applies it:
<!-- @decisions-file-start -->
```
accept
```
<!-- @decisions-file-end -->
```bash
accelerator migrate --decisions-file decisions.txt
```
The frontmatter gains a typed `relates_to` reference, and the session log records the decision:
<!-- @after-frontmatter-start -->
```
relates_to: ["work-item:0042"]
```
<!-- @after-frontmatter-end -->
<!-- @session-log-start -->
```
{"transformation_key":"meta/work/0001-improve-startup-time.md#body:references#0","schema_version":1,"outcome":"accepted","proposed_value":"relates_to=work-item:0042","timestamp":"<REDACTED>"}
```
<!-- @session-log-end -->
At a real terminal (no `--decisions-file`), the same transformation renders as a live prompt instead:
```
Session log: <SANDBOX>/.accelerator/state/migrations-0007-unify-meta-corpus-frontmatter-session.jsonl (resume from this file by re-running /accelerator:migrate)
Proposed linkage: relates_to: "work-item:0042"
Section anchor: body:references#0
Band: ambiguous
proposed: relates_to=work-item:0042
source: meta/work/0001-improve-startup-time.md:body:references#0
predicate: ambiguous
accept | skip | edit <value>:
```
## Answering prompts as an agent (the invoker contract)
When `/accelerator:migrate` runs without a human at a terminal, an agent answers the interactive prompts with a **decisions file**, following the four steps `list → decide → write → resume`:
In practice the agent first runs the migration and hits the **structured stall**, which names the exact decisions-file path — including the migration `<id>` — (`.accelerator/state/migrations-<id>-decisions.txt`) and a copy-pasteable resume command. `--list` is then the step that **reveals the proposed values** (which the stall does not show), so the realistic order is run → stall (learn the `<id>` and path) → `--list` (see proposed values) → write → resume. The `<id>` comes from the stall/preview, not from `--list` output.
1. **list** — `accelerator migrate --list` dry-emits every pending
interactive transformation, one tab-delimited line each, without mutating the
corpus:
```
<position>\t<key>\t<proposed>\t<path>:<field>
```
(Fields are separated by a literal TAB, shown as `\t` here; the same column
vocabulary — `<path>:<field>` — is used in `--help`.) Proposed values are
revealed only here, so list before deciding. When parsing, **skip lines
beginning with `#`**: with more than one pending interactive migration the
output is segmented by a `# migration <id>` header and `<position>` restarts
at 1 per migration. Resume each `# migration <id>` section separately with its
own decisions file (a single multi-migration decisions file is not yet
supported; `--list` prints a stderr note when more than one is pending).
2. **decide** — choose a verb per transformation: `accept`, `skip`, or
`edit <value>`.
3. **write** — write one verb per line to a decisions file,
**matched by emission order** (line *i* answers list position *i*;
skipped/mechanical transformations consume no line). Create the file yourself
at a path that exists and is readable — the stall message points at
`.accelerator/state/migrations-<id>-decisions.txt`; do not overwrite existing
`migrations-<id>-*` state files. `#`-prefixed lines are **not** comments —
they parse as an unknown verb, matching bash's own behaviour exactly. For
example:
```bash
printf 'accept\nskip\nedit work-item:0100\n' \
> .accelerator/state/migrations-<id>-decisions.txt
```
4. **resume** — re-run with `--decisions-file <path>` (or the equivalent
`ACCELERATOR_MIGRATE_DECISIONS_FILE` env var, discoverable via `--help`; a
supplied flag always wins over a pre-existing env var). The
stall's copy-pasteable command is exactly this bare form — **no
`ACCELERATOR_MIGRATE_FORCE=1` is needed** in the normal case. A partial
interactive run dirties the tree only with files this run owns (the
interactive session log, plus any frontmatter already written), and the
**guarded resume** lets the re-run proceed over that own output without
`FORCE` when the run base (the commits the working copy is based on) is
unchanged, printing a
one-line affordance listing the owned paths being resumed over. When the
pre-flight refuses, it lists the changes the run does *not* own. If the
refusal says a previous run's recorded base no longer matches (the run base
moved since the partial run, e.g. a parent was rewritten), confirm via the
session's VCS status command (see the SessionStart VCS Command Reference)
that the listed paths are only that run's output, commit them, and re-run
**without** `FORCE` to resume. Otherwise the listed paths are changes made
outside the run: commit or discard them. Reach for
`ACCELERATOR_MIGRATE_FORCE=1` only after confirming the listed paths are safe
to migrate over.
The driver **validates the decisions file up front (a no-mutation dry-apply pass)
and fails closed**: an unknown verb, a count mismatch (too few or too many
verbs), or a rejected `edit` value that no following line corrects exits
non-zero, names the offending position, and leaves the corpus **unmutated** —
validation never partially applies. Once validation passes and the live
apply begins, transformations are applied in order without rollback, so an
apply-time failure can leave a partial corpus; recover with VCS revert, then
re-run — guarded resume replays the run's own partial output without
`FORCE` when the run base is unchanged.
When no decision input is available at all, the run emits the structured stall
(`MIGRATION STALLED: no decision input available`) and stops without further
mutation:
<!-- @stall-start -->
```
[0007-unify-meta-corpus-frontmatter] MIGRATION STALLED: no decision input available
[0007-unify-meta-corpus-frontmatter] pending decision: meta/work/0001-improve-startup-time.md#body:references#0
[0007-unify-meta-corpus-frontmatter] No decisions file, terminal, or piped input was available to
[0007-unify-meta-corpus-frontmatter] answer this prompt, so the migration cannot proceed.
[0007-unify-meta-corpus-frontmatter]
[0007-unify-meta-corpus-frontmatter] This migration may have already partially modified the
[0007-unify-meta-corpus-frontmatter] working tree. Re-running /accelerator:migrate resumes this
[0007-unify-meta-corpus-frontmatter] partial run when the run base is unchanged (decided
[0007-unify-meta-corpus-frontmatter] transformations are replayed, not re-applied).
[0007-unify-meta-corpus-frontmatter]
[0007-unify-meta-corpus-frontmatter] To resume: each run answers the current prompt only (you
[0007-unify-meta-corpus-frontmatter] may be stalled again for the next undecided transformation):
[0007-unify-meta-corpus-frontmatter] 1. write the decision (accept | skip | edit <value>),
[0007-unify-meta-corpus-frontmatter] one per line, to: <path>
[0007-unify-meta-corpus-frontmatter] (create this file yourself; do not overwrite existing
[0007-unify-meta-corpus-frontmatter] migrations-0007-unify-meta-corpus-frontmatter-* state files)
[0007-unify-meta-corpus-frontmatter] 2. then run (copy-pasteable):
accelerator migrate --decisions-file <path>
[0007-unify-meta-corpus-frontmatter] equivalent env-var form:
ACCELERATOR_MIGRATE_DECISIONS_FILE=<path> accelerator migrate
```
<!-- @stall-end -->
This contract is scoped to a single pending interactive migration (the realistic
case); decisions files are consumed per migration.
## Executing the migration
Invoke via Bash:
```bash
accelerator migrate
```
`accelerator migrate` resolves the project root automatically from the current working directory. Run it from within the consumer repository.
## Cross-references
- `meta/decisions/ADR-0023-meta-directory-migration-framework.md` — framework design rationale
- `meta/decisions/ADR-0037-optional-interactive-contract-supplement-to-adr-0023.md` — optional interactive contract
- `meta/decisions/ADR-0038-interactive-validation-parameters-for-unified-schema-linkage-migration.md` — first consumer's parameterisation
- `meta/work/0116-structured-stall-on-no-decision-input.md` — the structured stall (`MIGRATION STALLED: no decision input available`) the invoker contract defers to when no decision input is available
- `skills/config/init/SKILL.md` — `init` bootstraps fresh repos; `migrate` upgrades existing ones
- `skills/config/configure/SKILL.md` — configuration reference