| name | axon4to5-migrate-code |
| description | 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. |
| argument-hint | framework=<axon|axoniq> configuration=<native|spring> mode=<single|project> [execution=<inline|subagent>] [source=<class|file|fqn>] [max-subagents=<0..N>] [auto=<true|false>] |
| disable-model-invocation | true |
axon4to5-migrate-code
Goal
Fully (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 / @Bean
idioms); a plain framework-configuration project stays on the direct
Configurer API (recipes use EventSourcingConfigurer /
MessagingConfigurer / CommandHandlingModule / EventSourcedEntityModule).
Available recipes (auto-listed)
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>
Inputs
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 mode (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. |
Durability
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.
Pre-steps (common to every mode)
These run before any mode-specific logic — independent of whether mode=single, project, or anything added later.
- Parse — read
framework, configuration, mode, execution, skip-openrewrite, max-subagents, auto from $ARGUMENTS.
- If
framework is missing or ∉ {axon, axoniq} → STOP and report unsupported framework.
- If
configuration is missing or ∉ {native, spring} → STOP and report unsupported configuration.
- If
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.
- OpenRewrite — skipped entirely when
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.
Modes
single
Migrate ONE element (one aggregate, one event processor, etc.) using exactly one recipe from the list above.
Steps (after the common pre-steps):
- Match — map user's request +
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.
- Execute —
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.
- Verify — behavior is preserved (no DCB, keep
AggregateBasedEventStorageEngine, etc.).
- Report — render the report (see Queue flow § Render report).
MUST NOT:
- Run without all required parameters resolved to a supported value.
- Run multiple recipes in one invocation.
- Migrate more than the single source named by the user.
- Migrate anything outside the supported
(framework, configuration) matrix — the rest of the codebase stays untouched.
- Introduce DCB or swap event storage engine.
project
Migrate 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:
- Discover — evaluate the recipe's
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.
- Scan
.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).
- Enqueue — add
(recipe, source) candidates. Deduplication is recipe's concern.
- Drain — exhaust all pending items for this recipe before advancing to the next:
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).
- ✅ Success / ⏭ Rejected / ❌ Failure → main records result, immediately dispatches next pending item. No pause.
- 🚧 Blocker → BLOCKER_RESOLUTION in main session:
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.
- Main session never pauses unless waiting for user input on a blocker (
auto=false).
- Fallback — if a subagent cannot be spawned, process inline and continue.
- Mark recipe done —
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 /clear and re-invoke the skill — it resumes automatically from .axon4to5-migration/progress.md, no work is lost.
MUST NOT:
- Spawn a subagent under
execution=inline.
- Pass anything beyond
(recipe path, source, framework, configuration) to a recipe subagent — context bloat defeats
the parallelism win.
- Cross repository boundaries during discovery.
- Halt the queue on a single Failure — record and drain the rest.
- Introduce DCB or swap event storage engine.
Queue flow
$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.
Single mode flow
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
Project mode flow
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