- 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.
1. **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.
2. **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):
1. **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.
2. **Execute** — `Read` the chosen recipe file under [`references/recipes/`](references/recipes/) (`<name>/RECIPE.md`)
and execute it per the **Recipe sub-flow** ([`FLOW.md`](references/recipes/FLOW.md), already loaded). Recipe-local
auxiliary files (examples, fixtures, supporting docs) live alongside it in the same `<name>/` directory.
3. **Verify** — behavior is preserved (no DCB, keep `AggregateBasedEventStorageEngine`, etc.).
4. **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:
1. **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).
2. **Enqueue** — add `(recipe, source)` candidates. Deduplication is recipe's concern.
3. **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.
4. **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`](references/recipes/FLOW.md), loaded at skill start. `[[Resolve blocker]]` = [`references/recipes/BLOCKER_RESOLUTION.md`](references/recipes/BLOCKER_RESOLUTION.md), budget = 1 attempt per item; on exhaustion item is marked blocked and drain continues.
### Single mode flow
```mermaid
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
```mermaid
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
Ver no GitHub