From axoniq-migration
Migrate Axon Framework 4 project code and configuration to Axon(iq) Framework 5. Migrates source code and wiring only — NOT stored data (event store contents / stored events, tracking tokens), which are left untouched.
How this skill is triggered — by the user, by Claude, or both
Slash command
/axoniq-migration:axon4to5-migrate-code framework=<axon|axoniq> configuration=<native|spring> mode=<single|project> [execution=<inline|subagent>] [source=<class|file|fqn>] [max-subagents=<0..N>] [auto=<true|false>]framework=<axon|axoniq> configuration=<native|spring> mode=<single|project> [execution=<inline|subagent>] [source=<class|file|fqn>] [max-subagents=<0..N>] [auto=<true|false>]The summary Claude sees in its skill listing — used to decide when to auto-load this skill
> Fully (or as most as possible) compiling, green-test codebase on AF5, **same architecture as AF4**.
CLAUDE.mdassets/learnings-template.mdassets/progress-template.mdreferences/DURABILITY.mdreferences/docs/index.adocreferences/docs/openrewrite-code-conversion.adocreferences/docs/paths/aggregates/configuration-migration.adocreferences/docs/paths/aggregates/event-tagging.adocreferences/docs/paths/aggregates/index.adocreferences/docs/paths/aggregates/multi-entity-migration.adocreferences/docs/paths/aggregates/polymorphism-migration.adocreferences/docs/paths/configuration.adocreferences/docs/paths/dlq.adocreferences/docs/paths/event-store.adocreferences/docs/paths/index.adocreferences/docs/paths/interceptors.adocreferences/docs/paths/messages.adocreferences/docs/paths/projectors-event-processors.adocreferences/docs/paths/sequencing-policies.adocreferences/docs/paths/serializers.adocFully (or as most as possible) compiling, green-test codebase on AF5, same architecture as AF4. No DCB. No new patterns. Legacy event storage preserved.
Scope: code and configuration only. This skill migrates source code and wiring. It does NOT migrate stored data — the event store contents (stored events) and tracking tokens are left untouched. Migrating to AF5 with the legacy event-storage layout keeps those stores compatible as-is; moving data to a new storage layout (e.g. DCB) is out of scope. The migration preserves the project's existing configuration style: a Spring Boot project stays on Spring auto-config (recipes use
@Component/@Beanidioms); a plain framework-configuration project stays on the directConfigurerAPI (recipes useEventSourcingConfigurer/MessagingConfigurer/CommandHandlingModule/EventSourcedEntityModule).
Run bash scripts/list-recipes.sh from the skill root directory. Output format:
- file: references/recipes/<dir>/RECIPE.md
id: <id>
title: <title>
description: <description>
applicable: |
<applicable section content, or "(none)" if missing>
framework (required): which Axon flavor to migrate. Currently supported values: axon, axoniq. Any other
value → STOP.configuration (required): how the application wires Axon. Currently supported values: native, spring. Any
other value → STOP.mode (required): what gets migrated in one invocation.
single — one element (a class, e.g. an Aggregate). Requires source.project — the whole application (default: current working directory). source ignored.execution (optional, default inline): how the orchestrator runs its steps. Only meaningful for mode=project —
for mode=single it has no observable effect.
inline — main session does discovery + recipe runs sequentially. No Agent tool use.subagent — orchestrator MAY dispatch via the Agent tool: discovery → Explore subagent, recipe sub-flow per
item → general-purpose subagent (parallel batches). Useful for project mode on large codebases.source (required for mode=single): hint identifying the thing to migrate (class name, file path, FQN).skip-openrewrite (optional, default false): when true, the orchestrator SKIPS Pre-step 2 (the OpenRewrite bulk pass) and goes straight to the mode-specific producer. Use this when (a) OpenRewrite Phase 1 has already been run separately on the tree, (b) the caller is exercising a recipe in isolation, or (c) the project is not built with Maven/Gradle so the OpenRewrite plugin is unreachable. Values: true / false. Any other value → STOP. The downstream recipe must still tolerate both AF4-shaped and partially-migrated sources (see each recipe's # Applicable predicates).max-subagents (optional, default 0): max parallel general-purpose subagents for item processing in the drain loop. mode=project only — ignored for mode=single. 0 = inline (no subagents, sequential). N > 0 = dispatch up to N items simultaneously as subagents; BLOCKER_RESOLUTION always runs in main session regardless of this value. Any non-integer or value < 0 → STOP.auto (optional, default false): when true, never calls AskUserQuestion — all interactive decisions resolved automatically (see ## Auto mode). Values: true / false. Any other value → STOP.auto=true)Orchestrator makes all decisions without AskUserQuestion. Every auto-resolved choice emits ⚙️ auto: <decision> so the log stays auditable.
| Decision point | Auto action |
|---|---|
Ambiguous recipe match (mode=single) | Pick first candidate by applicable score. |
| Blocker | Auto-select the Option the recipe marked (Recommended); if none is marked, skip. A recommended migration path (e.g. saga stateful-rewrite) re-enters the recipe with that option id; skip/revert resolve in-place. See BLOCKER_RESOLUTION.md § Auto mode. |
| Resume + selection-args mismatch | Args identical → auto-resume. Args differ → auto-start-over. |
| Working tree mismatch on resume | Proceed; record ⚠️ auto: tree mismatch ignored in progress.md. |
| OpenRewrite step completes | Immediately continue to mode-specific producer. Do NOT pause or end session. |
Load order — see § Recipe sub-flow. FLOW.md first, then DURABILITY.md (second). Defines state files under .axon4to5-migration/, hooks across pre-steps + queue + recipe results + caller decisions, and commit protocol. Reads progress.md on entry to decide resume vs fresh.
These run before any mode-specific logic — independent of whether mode=single, project, or anything added later.
framework, configuration, mode, execution, skip-openrewrite, max-subagents, auto from $ARGUMENTS.
framework is missing or ∉ {axon, axoniq} → STOP and report unsupported framework.configuration is missing or ∉ {native, spring} → STOP and report unsupported configuration.mode is missing or ∉ {single, project} → STOP and report unsupported mode.execution defaults to inline if missing. If present and ∉ {inline, subagent} → STOP and report unsupported execution.skip-openrewrite defaults to false if missing. If present and ∉ {true, false} → STOP and report unsupported value.max-subagents defaults to 0 if missing. If present and not a non-negative integer → STOP.auto defaults to false if missing. If present and ∉ {true, false} → STOP.skip-openrewrite=true. Otherwise, internally invoke
axon4to5-openrewrite via the Skill tool, passing --framework $framework --commit false. Do NOT pass --commit true or omit --commit; DURABILITY's on:openrewrite-done hook owns the single combined commit. This is a step of this orchestrator, not a separate command. Idempotent — safe even on a partially-migrated tree. If it fails → STOP and report the failure (no gap-filling on a broken bulk pass). When skipped, surface that fact in the eventual report (Notes or Learnings) so the caller knows the queue ran against unprocessed AF4 (or already-partially-migrated) sources and the recipes did all the work themselves.
auto=true: after this step returns (success or skip), immediately continue to the mode-specific producer — do NOT end the session or pause.Only after pre-steps complete does the mode-specific producer below run.
singleMigrate ONE element (one aggregate, one event processor, etc.) using exactly one recipe from the list above.
Steps (after the common pre-steps):
source to ONE recipe in the auto-listed set. Primary signal: the catalog's
applicable block (surface predicates against $SOURCE — annotations / type markers). Fallback signal: id +
title + description. If ambiguous → ask user via AskUserQuestion to pick (show title to the user; dispatch by
id). If no applicable block matches and description is also unclear → STOP and report.Read the chosen recipe file under references/recipes/ (<name>/RECIPE.md)
and execute it per the Recipe sub-flow (FLOW.md, already loaded). Recipe-local
auxiliary files (examples, fixtures, supporting docs) live alongside it in the same <name>/ directory.AggregateBasedEventStorageEngine, etc.).MUST NOT:
(framework, configuration) matrix — the rest of the codebase stays untouched.projectMigrate everything in the working directory that any recipe in the catalog declares applicable. source is ignored.
Steps (after the common pre-steps):
Recipe loop — iterate recipes in discovery order. For each recipe:
applicable predicates across the codebase to produce candidate sources.
execution=inline → orchestrator scans inline using Grep / Glob / Read.execution=subagent → dispatch one Explore subagent for this recipe. Read-only — no edits..java AND .kt, across all source roots. JVM projects are mixed and Kotlin files often sit under src/main/java/ (and vice-versa) — never filter candidates by extension or source directory. Key on the recipe's Axon annotations, not on path. See DEFAULT.md § Source file conventions (the Explore subagent must be told this too).(recipe, source) candidates. Deduplication is recipe's concern.max-subagents=0 (default) → inline, main session, sequentially.max-subagents=N → main session acts as coordinator. Dispatches up to N pending items simultaneously as general-purpose subagents (single Agent message per batch). Each subagent executes ONE recipe sub-flow and returns a result block (RESULT: line + Notes + a Learnings field — complete authored entries, or an explicit none — <why>). The subagent authors the learning prose (it witnessed the run) but MUST NOT write learnings.md itself — the coordinator is the single safe writer under parallelism and stamps the date + commit sha, relaying each entry verbatim (see DURABILITY § Proactive Learnings).
AskUserQuestion if auto=false; auto-skip if auto=true. Resolved → re-dispatch same item to a new subagent. Not resolved → mark blocked, dispatch next pending item.auto=false).on:recipe-done hook records status in progress.md Recipe status table.After all recipes drained → Debugging loop → Finalize → Report.
Context hygiene — after every 5 items drained, emit this tip once (then reset counter):
💡 Context is growing. Run
/clearand re-invoke the skill — it resumes automatically from.axon4to5-migration/progress.md, no work is lost.
MUST NOT:
execution=inline.(recipe path, source, framework, configuration) to a recipe subagent — context bloat defeats
the parallelism win.$SOURCE is referenced throughout the recipe sub-flow as the argument passed to the skill from source.
[[Execute recipe sub-flow]]=references/recipes/FLOW.md, loaded at skill start.[[Resolve blocker]]=references/recipes/BLOCKER_RESOLUTION.md, budget = 1 attempt per item; on exhaustion item is marked blocked and drain continues.
flowchart TD
A[Skill invoked] --> PARSE["<b>Parse</b><br/>framework, configuration, source"]
PARSE --> ORW[["<b>OpenRewrite</b><br/>(internal Skill, idempotent)"]]
ORW -- fail --> XORW[STOP: bulk-rewrite failed]
ORW -- ok --> B["list-recipes (catalog)"]
B --> MATCH{"<b>Match</b><br/>request + source → recipe"}
MATCH -- ambiguous --> ASK["AskUserQuestion<br/>(show titles, dispatch by id)"]
ASK --> EXEC
MATCH -- "no match" --> XNOMATCH[STOP: no applicable recipe]
MATCH -- matched --> EXEC[["<b>Execute</b> recipe sub-flow<br/>(FLOW.md)"]]
EXEC --> R{<b>RESULT?</b>}
R -- "Blocker (first attempt)" --> BR[["<b>Resolve blocker</b><br/>(BLOCKER_RESOLUTION.md)"]]
BR --> BRQ{"Resolved?<br/>budget = 1"}
BRQ -- yes --> EXEC
BRQ -- "no / exhausted" --> BLK["mark blocked<br/>(🚧 caller must resolve)"]
R -- "Blocker (already retried)" --> BLK
R -- "Success / Rejected / Failure" --> VER["<b>Verify</b><br/>behavior preserved<br/>same architecture as AF4"]
VER --> RPT["<b>Report</b> & END"]
BLK --> RPT
flowchart TD
A[Skill invoked] --> PARSE["<b>Parse</b><br/>framework, configuration, execution"]
PARSE --> ORW[["<b>OpenRewrite</b><br/>(internal Skill, idempotent)"]]
ORW -- fail --> XORW[STOP: bulk-rewrite failed]
ORW -- ok --> B["list-recipes (catalog)"]
B --> RL{"<b>Next recipe</b><br/>in order?"}
RL -- "yes: <recipe>" --> DISC["<b>Discover</b><br/>execution=inline: Grep/Glob<br/>execution=subagent: 1 Explore<br/>→ <b>Enqueue</b> items"]
DISC --> Q[(Recipe queue)]
Q --> L{"<b>Drain</b><br/>pending?"}
L -- yes --> INP["pick next → in-progress"]
INP --> W[["<b>Execute</b> recipe sub-flow<br/>execution=inline: main session<br/>execution=subagent: general-purpose (parallel batch)"]]
W --> R{<b>RESULT?</b>}
R -- "Blocker (first attempt)" --> BR[["<b>Resolve blocker</b><br/>(BLOCKER_RESOLUTION.md)"]]
BR --> BRQ{"Resolved?<br/>budget = 1"}
BRQ -- yes --> W
BRQ -- "no / exhausted" --> BLK["mark blocked<br/>(🚧 caller must resolve)"]
R -- "Blocker (already retried)" --> BLK
R -- "Success / Rejected / Failure" --> VER["<b>Verify</b><br/>behavior preserved<br/>same architecture as AF4"]
VER --> DONE["mark done in queue"]
DONE --> Q
BLK --> Q
L -- "no (recipe drained)" --> RDONE["<b>on:recipe-done</b><br/>update Recipe status table"]
RDONE --> RL
RL -- "no more recipes" --> DBG_COMP
subgraph DEBUG ["🔍 Debugging — all recipes applied, build still red"]
direction TB
DBG_COMP["<b>Full compile</b><br/>mvn compile / gradle classes"]
DBG_REDISC["<b>Re-discover</b><br/>re-scan applicable predicates<br/>any recipe match remaining errors?"]
DBG_COMP -- "⚠️ errors" --> DBG_REDISC
end
DBG_COMP -- "✅ green" --> FIN["<b>Finalize</b><br/>remove isolated-* scaffolding<br/>final compile · count by recipe"]
DBG_REDISC -- "new candidates" --> REENQ["re-enqueue as pending"]
REENQ --> Q
DBG_REDISC -- "nothing new<br/>recipes exhausted" --> FIN
FIN --> RPT["<b>Report</b> & END"]
Discovery order — Discover scans recipes in this fixed sequence. Aggregates first: they define the events and commands consumed by downstream types.
| # | Recipe (id per frontmatter order:) |
|---|---|
| 1 | aggregate |
| 2 | event-processor |
| 3 | command-gateway |
| 4 | query-gateway |
| 5 | query-handler |
| 6 | interceptors |
| 7 | saga |
| 8 | event-store |
Entered when drain empties but build is still red. All known recipes have been applied — this phase asks: "is there still something a recipe can fix?"
Before the first full compile: Scan for application classes in subdirectories that OpenRewrite does NOT process by default — e.g., a microservices/ tree or separately deployable modules at the same repo root. These classes often use the same AF4 patterns (AF4 ConfigurationEnhancer, SagaEntry, DeadlineManager) as the main modules but are missed by the recipe drain because discovery only scanned the main source sets. Add any found classes to the queue and drain them before running the full compile.
mvn compile / gradle classes (Java + Kotlin). Green → exit loop → Finalize.applicable predicates against current codebase. Sources mutated during drain may now match recipes that rejected earlier.(recipe, source) pairs not already terminal → re-enqueue as pending, resume drain (loop repeats).Grep all pom.xml / build.gradle(.kts) for isolated-* Maven profiles and isolated* Gradle source-sets added by axon4to5-isolatedtest. Edit each build file to remove found blocks. Commit: chore(af5): remove isolated-test scaffolding. Skip if none found.progress.md. Commit: chore(af5): record final compile status.Emit this block to the user on on:session-end:
🏁 Migration complete — all applicable recipes exhausted.
| Recipe type | ✅ Success | ⏭ Rejected | ❌ Failure | 🚧 Blocked |
|-----------------|-----------|------------|-----------|-----------|
| aggregate | N | N | N | N |
| event-processor | N | N | N | N |
| … | | | | |
Compilation: ✅ green
— or —
Compilation: ⚠️ N error(s) remain — manual intervention required.
Blocked items (caller must resolve):
- <recipe>/<source>: <blocker notes>
See .axon4to5-migration/progress.md for full details.
Load order at skill start (before any pre-steps or mode logic):
Read references/recipes/FLOW.md — recipe control-flow spec. Non-optional. Recipes fill in named sections; they never re-implement it.Read references/DURABILITY.md — state + resume protocol (mode=project; skim for mode=single).DEFAULT.md)ALWAYS Read references/recipes/DEFAULT.md BEFORE any per-recipe RECIPE.md under
references/recipes/. It holds shared defaults for every named recipe section (# Applicable,
# Scope, # References, # Success Criteria, # Blocker, # Toolbox, # Out of Scope, # Gotchas).
Merge rule when executing a recipe:
DEFAULT.md's content for that
section.RECIPE.md defines the same section → RECIPE.md overrides (full section replacement, not append).
RECIPE.md's section body references DEFAULT.md (e.g. literal
token @DEFAULT.md or prose like "inherits from DEFAULT.md" / "extends DEFAULT.md") → append the recipe's
content to the default's content for that section instead of replacing.RECIPE.md omits the section → the DEFAULT.md content stands.Shared cross-recipe knowledge base at references/docs/paths/. Recipes pick relevant entries
in their ### Migration Paths subsection, each with an apply-condition (a fact about current scope that triggers
loading the file). The orchestrator never reads these directly — only recipes do, gated by their declared
apply-condition.
Catalog (one file per topic; .adoc):
| Path | Topic |
|---|---|
aggregates/index.adoc | Aggregate migration entry point |
aggregates/configuration-migration.adoc | Aggregate Spring/Configurer wiring |
aggregates/multi-entity-migration.adoc | Aggregates with child entities (@AggregateMember) |
aggregates/polymorphism-migration.adoc | Polymorphic aggregates |
configuration.adoc | Global Axon configuration / Configurer |
messages.adoc | Command / Event / Query message changes |
event-store.adoc | Event Store engine + APIs |
snapshotting.adoc | Snapshot trigger + storage |
serializers.adoc | Serializer registration + payload formats |
interceptors.adoc | Command / Event / Query handler interceptors |
projectors-event-processors.adoc | Projection / Event Processor wiring |
sequencing-policies.adoc | Event sequencing policies |
dlq.adoc | Dead-Letter Queue |
test-fixtures.adoc | Test fixtures migration |
npx claudepluginhub axoniq/agent-skills --plugin axoniq-migrationCreates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.