migrate
Invocation: User- and model-invocable
Allowed tools: Read, Write, Edit, Bash
Source: skills/config/migrate/SKILL.md
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
Section titled “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
Section titled “How it works”The driver script skills/config/migrate/scripts/run-migrations.sh orchestrates the migration lifecycle:
- Clean-tree pre-flight. Checks
meta/,.claude/accelerator*.md, and.accelerator/for uncommitted changes. Aborts if any are found. SetACCELERATOR_MIGRATE_FORCE=1to bypass (advanced users only). - Read state. Loads
.accelerator/state/migrations-appliedand.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. - Discover migrations. Globs
skills/config/migrate/migrations/[0-9][0-9][0-9][0-9]-*.shin sorted order. The directory can be overridden viaACCELERATOR_MIGRATIONS_DIR(used in tests). - Compute pending. A migration is pending if its ID is in neither the applied nor the skipped set.
- Preview banner. Prints one line per pending migration —
<ID> — <description>— with a per-migration skip hint (--skip <id>). If nothing is pending, printsNo pending migrations.(plus any skipped names) and exits 0 immediately. - Apply in order. For each pending migration: runs it with
PROJECT_ROOTexported; on success atomically appends its ID to.accelerator/state/migrations-applied; on failure prints the migration’s output to stderr, exits 1, and leaves the state file at the last successful migration. If the migration emitsMIGRATION_RESULT: no_op_pendingon stdout, it is treated as a soft skip — the migration stays pending and will be retried on future runs. - Summary. Prints counts of applied, skipped, and pending (no-op) migrations.
Per-migration contract
Section titled “Per-migration contract”Each migration script under migrations/ must:
- Start with
#!/usr/bin/env bashthen# DESCRIPTION: <short imperative description>on line 2 - Receive
PROJECT_ROOTas an exported env var pointing to the consumer repo - Self-detect and exit 0 on already-applied state (idempotent)
- Use atomic write patterns (source
scripts/atomic-common.shforatomic_write,atomic_append_unique,atomic_remove_line) for any file rewrites - NOT honour any
DRY_RUNenv var — this framework has no dry-run mode - Optionally emit
MIGRATION_RESULT: no_op_pendingon stdout to signal that preconditions are not met and the migration should remain pending (retried on future runs)
State file format
Section titled “State file format”.accelerator/state/migrations-applied contains one migration ID per line, in the order migrations were applied:
0001-rename-tickets-to-work0002-some-future-migration.accelerator/state/migrations-skipped contains one migration ID per line for migrations the user has chosen to defer:
0002-some-future-migrationBoth 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
Section titled “Skip-tracking”Skip a migration to defer it indefinitely:
bash "${CLAUDE_PLUGIN_ROOT}/skills/config/migrate/scripts/run-migrations.sh" --skip <migration-id>Unskip a previously skipped migration so it becomes pending again:
bash "${CLAUDE_PLUGIN_ROOT}/skills/config/migrate/scripts/run-migrations.sh" --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.
MIGRATION_RESULT contract
Section titled “MIGRATION_RESULT contract”A migration that exits 0 and emits MIGRATION_RESULT: no_op_pending on stdout is treated as a soft deferral — it stays pending and will be retried on future runs. The sentinel line is stripped from user-visible output.
Migrations emitting this sentinel MUST guarantee they performed no destructive operations before the line was emitted. Migrations doing destructive work must either succeed (recorded as applied) or fail non-zero.
Optional interactive contract
Section titled “Optional interactive contract”Mechanical migrations are the default — they run start-to-finish with no user interaction. A migration that needs to ask the user about ambiguous transformations can opt in to the interactive contract by adding # INTERACTIVE: yes to its header. The framework then drives a per-transformation accept / edit / skip prompt loop, persists each decision durably before any artefact mutation, and resumes from the on-disk session log on subsequent runs.
API reference at a glance
Section titled “API reference at a glance”| Name | When called | Args | Returns | Side effects |
|---|---|---|---|---|
migration_emit_transformations | once at handshake | none (uses harness_emit_transformation to emit each record) | (no return contract) | accumulates extras via harness_extras_set (reset after each emission) |
migration_evaluate_predicate | per transformation | TSV line on stdin (use harness_field) | exit 0 = prompt; exit 1 = mechanical; other non-zero = FAIL | none |
migration_validate_edit | per edit decision | positional: key path anchor proposed user_value | exit 0 = valid; non-zero (via harness_reject) = invalid | should write error to stderr |
migration_apply_decision | per accept/edit, AFTER runner persists; never for skip | positional: key path anchor decision value | exit 0 = success; non-zero = FAIL aborts migration | mutates the artefact at (path, anchor) |
migration_session_log_path (optional) | once at handshake | none | path string on stdout | none |
migration_verify_applied (optional) | per resumed-accepted/edited key | positional: key path anchor recorded_outcome recorded_proposed [recorded_user_value] | exit 0 = mutation present; non-zero = absent → re-prompt | none |
harness_emit_transformation | inside migration_emit_transformations | named: key= path= anchor= proposed= predicate_value= display= | (helper) | emits one TSV transformation; auto-clears extras after |
harness_extras_set | inside migration_emit_transformations | positional: key value | (helper) | accumulates one extras pair for the next emission |
harness_extras_clear | optional inside migration_emit_transformations | none | (helper) | drops accumulated extras |
harness_field | inside migration_evaluate_predicate (TSV on stdin) | positional: field name | value on stdout | none |
harness_reject | inside migration_validate_edit | positional: message | (helper, exits non-zero) | prints [interactive] <message> to stderr |
harness_run | last line of the migration | none | (drives the protocol loop) | the entire interactive protocol |
Header marker
Section titled “Header marker”A migration declares the hook by adding # INTERACTIVE: yes somewhere in its first five header-comment lines. Template skeleton:
#!/usr/bin/env bash# DESCRIPTION: <short imperative># INTERACTIVE: yes
set -euo pipefailsource "$CLAUDE_PLUGIN_ROOT/scripts/atomic-common.sh"source "$CLAUDE_PLUGIN_ROOT/scripts/interactive-harness.sh"
migration_emit_transformations() { ... }migration_evaluate_predicate() { ... }migration_validate_edit() { ... }migration_apply_decision() { ... }
harness_runAuthor-facing helpers
Section titled “Author-facing helpers”Authors NEVER hand-write TSV positional fields, base64-encode display blocks, or hand-write JSON. The helpers exposed by scripts/interactive-harness.sh cover every wire-protocol concern:
harness_emit_transformation key=K path=P anchor=A proposed=V predicate_value=PV display=$'multi\nline'— emits one transformation. Extras fromharness_extras_setare attached then cleared.harness_extras_set <key> <value>— accumulate one extras pair. Keys must match^[a-z][a-z0-9_]*$and cannot collide with framework-mandatory names (transformation_key,schema_version,outcome,proposed_value,user_value,timestamp). Extras are auto-cleared after eachharness_emit_transformation— set them inside the emit loop, not once before it.harness_extras_clear— drop all accumulated extras.harness_field <name>— insidemigration_evaluate_predicate, extract a field by name from the TSV transformation line on stdin.harness_reject "<message>"— insidemigration_validate_edit, reject the user’s input in a uniform format ([interactive] <message>). Always return non-zero from the validator after calling this.
Callback contracts
Section titled “Callback contracts”migration_evaluate_predicateexits 0 to route the transformation through the prompt loop, exit 1 to apply it mechanically (no prompt), or any other non-zero status to abort the migration withFAIL.migration_apply_decisionis called once peracceptoreditdecision, after the runner has durably persisted the JSONL session-log record (write-ahead-log invariant). It is not called forskip. A non-zero exit aborts the migration without ledger append.migration_verify_applied(optional) is called on resume before emittingRESUMED_APPLIED. Returning non-zero tells the framework the recorded mutation is absent (e.g. from a partial-apply crash); the framework removes the stale record via DRIFT and re-prompts.
Callbacks may be invoked more than once per run, so they must be deterministic and side-effect-free. A
--decisions-filerun runsmigration_emit_transformationsandmigration_evaluate_predicateduring--listenumeration and again during the dry-apply validation pass before the live run, and runsmigration_validate_editduring dry-apply and again at live apply. In particularmigration_validate_editmust be a pure function of its arguments — it must not read corpus state that an earlier transformation in the same run could mutate. (If it did, dry-apply could pass against the unmutated corpus while the live run fails validation at a later position after earlier files changed, re-opening the partial-mutation hole dry-apply closes.) A validator that depends on mutable corpus state is unsupported; this is documented rather than enforced.
Runner guarantees (ADR-0037 §§1–4)
Section titled “Runner guarantees (ADR-0037 §§1–4)”- §1 Predicate routing: each transformation is routed to either the prompt loop or the mechanical path based on the predicate’s exit code.
- §2 Display elements: every prompt shows the proposed mutation target and value, the source location as
path:anchor, and the predicate’s evaluated value, plus any author-declareddisplay=content. - §3 Resumability: every prompted decision is durably persisted to
.accelerator/state/migrations-<id>-session.jsonlbefore the artefact is mutated (write-ahead-log invariant). On re-entry, already-decided keys emitRESUMED_APPLIED/RESUMED_SKIPPEDand skip the prompt. A migration completes (and the ID is appended tomigrations-applied) only when the harness emitsDONE. - §4 Decision verbs:
acceptapplies the proposed value;edit <value>substitutes a user-provided value (validated bymigration_validate_edit);skiprecords the user’s decision but does not mutate the artefact.
Runner-level decisions
Section titled “Runner-level decisions”- Source-drift: if a recorded record’s
proposed_valuediffers from the live emission’s, the runner re-prompts and the old record is discarded viaatomic_jsonl_remove_by_key. Because the write-ahead-log invariant guarantees persistence before mutation, DRIFT unambiguously means “the migration source changed” — partial-apply crashes resume viaRESUMED_APPLIED(or viamigration_verify_appliedif declared). - Transformation ordering: emission order from
migration_emit_transformationsis the canonical iteration order. - Sticky skip semantics: a transformation skipped on a prior run remains skipped on every subsequent run, even if its predicate would have changed. Users who want to re-prompt a previously-skipped key delete the corresponding session-log line and re-run.
Session log
Section titled “Session log”- Path:
.accelerator/state/migrations-<id>-session.jsonl(override viamigration_session_log_path). Relative paths are resolved againstPROJECT_ROOT. - One JSON object per line, canonical field order:
transformation_key,schema_version: 1,outcome∈{accepted, edited, skipped},proposed_value, optionaluser_value(only foredited),timestamp, followed by any author-declared extras in receipt order. - 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_versionand prints a clear recovery instruction.
Worked example
Section titled “Worked example”The fixture in scripts/test-fixtures/interactive/doc-example/ ships with the plugin and is exercised by test-migrate-interactive.sh (AC-13). Three transformations are emitted — one ambiguous (prompted), one resolved (mechanical), one ambiguous with an empty-value validator. With the scripted decisions edit (empty), edit 0123-renamed, skip, the user-facing transcript is:
<!— @transcript-start —>
Session log: <SANDBOX>/.accelerator/state/migrations-0099-doc-example-session.jsonl (resume from this file by re-running /accelerator:migrate)
── Transformation 1 ────────────────────────Proposed: 0034-fooSource: meta/work/example-A.md:14Predicate: ambiguous
Proposed value: 0034-fooSurrounding prose: the linkage paragraph
[accept | edit <new-value> | skip] >[interactive] empty value not allowed── Transformation 1 ────────────────────────Proposed: 0034-fooSource: meta/work/example-A.md:14Predicate: ambiguous
Proposed value: 0034-fooSurrounding prose: the linkage paragraph
[accept | edit <new-value> | skip] >── Transformation 2 ────────────────────────Proposed: 0007-bazSource: meta/work/example-C.md:21Predicate: ambiguous
Proposed value: 0007-bazSurrounding prose: the paragraph the author wants to revise
><!— @transcript-end —>
And the on-disk session log (with timestamps redacted) is:
<!— @session-log-start —>
{"transformation_key":"link-A","schema_version":1,"outcome":"edited","proposed_value":"0034-foo","user_value":"0123-renamed","timestamp":"<REDACTED>","band":"ambiguous","prose":"the linkage paragraph"}{"transformation_key":"link-C","schema_version":1,"outcome":"skipped","proposed_value":"0007-baz","timestamp":"<REDACTED>"}<!— @session-log-end —>
The middle transformation (link-B, band resolved) routed through the mechanical path — predicate exited 1, the harness emitted MECHANICAL_APPLIED, no record was persisted, the artefact was mutated unconditionally.
Answering prompts as an agent (the invoker contract)
Section titled “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.
-
list —
bash …/run-migrations.sh --listdry-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
\there; 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;--listprints a stderr note when more than one is pending). -
decide — choose a verb per transformation:
accept,skip, oredit <value>. -
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 existingmigrations-<id>-*state files. For example:Terminal window printf 'accept\nskip\nedit work-item:0100\n' \> .accelerator/state/migrations-<id>-decisions.txt -
resume — re-run with
--decisions-file <path>(or the equivalentACCELERATOR_MIGRATE_DECISIONS_FILEenv var, discoverable via--help). The stall’s copy-pasteable command is exactly this bare form — noACCELERATOR_MIGRATE_FORCE=1is 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 shipped in 0119 lets the re-run proceed over that own output withoutFORCEwhen the base revision is unchanged, printing a one-line affordance listing the owned paths being resumed over.FORCEis required only when the pre-flight refuses — i.e. the tree carries dirt this run does not own (foreign changes, or you have committed since the partial run so the base revision moved). In that case, re-run once withoutFORCEfirst to read the refusal / in-flight-session guidance, confirm viajj status/git statusthat the dirty paths really are this migration’s own, and only then addACCELERATOR_MIGRATE_FORCE=1. (The guarded-resume behaviour is owned bymeta/work/0119-resume-safe-partial-migration-failure.md, now landed.)
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. (The dry-apply pass mirrors the live run, so
a migration_validate_edit rejection followed by a corrected value on the next
line is accepted just as it would apply.) 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 — 0119’s guarded resume replays the run’s own partial output without
FORCE when the base revision 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 — see meta/work/0116-structured-stall-on-no-decision-input.md.
This contract is scoped to a single pending interactive migration (the realistic case); decisions files are consumed per migration.
Executing the migration
Section titled “Executing the migration”Invoke via Bash:
bash "${CLAUDE_PLUGIN_ROOT}/skills/config/migrate/scripts/run-migrations.sh"The driver script resolves PROJECT_ROOT automatically from the current working directory. Run it from within the consumer repository. Run /accelerator:migrate from a single shell at a time; it does not acquire a lock.
Cross-references
Section titled “Cross-references”meta/decisions/ADR-0023-meta-directory-migration-framework.md— framework design rationalemeta/decisions/ADR-0037-optional-interactive-contract-supplement-to-adr-0023.md— optional interactive contractmeta/decisions/ADR-0038-interactive-validation-parameters-for-unified-schema-linkage-migration.md— first consumer’s parameterisationmeta/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 availableskills/config/init/SKILL.md—initbootstraps fresh repos;migrateupgrades existing onesskills/config/configure/SKILL.md— configuration reference

