Alokai

AI-assisted migration

Have your coding agent move your project to a target Alokai version in one run - it places every installed package against the compatibility matrix, applies only the published migration steps your repository qualifies for, and validates with lint, typecheck, build, boot and your own tests until they pass.

Introduced in Alokai 2.5.0

Every release publishes its migration steps for every Alokai project at once - the upgrade guide under the versions table is where you read them. Most of them don't apply to yours: a SAP Commerce Cloud step in a Commercetools-only project, a Nuxt step in a Next.js storefront, a package that moved for an integration you never installed. Working out which ones do apply means reading every step against your own repository - and applying one that doesn't apply is worse than skipping one that does, because it edits code for something you don't use and you have no reason to look there again.

The alokai-migration skill does that gating for you, and works out where your project stands in the first place. It doesn't read the guide as prose: the same steps are published as data at /alokai-versions/steps.json, each one carrying the scope it applies to, the condition it turns on, and who is able to settle that condition. It ships in @alokai/ai-toolkit, so any coding agent that reads skills can run it - the examples on this page use Claude Code.

Using it

Run it with no arguments to move to the latest release:

/alokai-migration

or just ask - "upgrade this project to the latest Alokai version", "bring this project up to date". To stop at a particular release, name it:

/alokai-migration --to 2.4.0

A version in the prompt - "upgrade to 2.4.0" - means the same as --to. Without either, the target is the newest release in the compatibility matrix.

One run covers the whole range. The skill works out the release your project is on, collects every step between that release and the target, and applies them in one pass - the steps of the releases in between included, so nothing is jumped. It does not hop release by release. A project rarely sits cleanly on one release: the CLI is on the latest, the middleware two minors back, and hopping to each intermediate release in turn would mean moving the packages that are already ahead backwards onto that release's pins. A single range run keeps every package that is already ahead where it is and never moves anything back.

Once it has placed the project - and before its first edit - it tells you the plan: where every package stands, the start that follows from it, the target and the range, the human guide for the same range, which areas it is handing back, which packages it will not touch, and how many steps it is about to gate. That's a plan, not a request for permission - it carries on unless you stop it.

The other argument is --base <url>, which points the fetch at a preview deployment or a local docs server instead of https://docs.alokai.com. It names the base it used in both the plan and the report, and with a non-default base it treats every applied edit as a draft to show you rather than something to leave unreviewed - the default host is the one Alokai publishes, while a preview is built per pull request and a local server is whatever is on that machine.

How it works out where your project is

Your project's version is not one number, and the skill does not treat it as one. It reads the installed version of every package Alokai's taxonomy knows about - out of node_modules, never out of your manifests, because a manifest is what a half-finished upgrade edits - and places each one against every version the compatibility matrix ever pinned for it. A package is on a release (or on a run of releases that pinned the same version), between two pins, ahead of every pin the matrix knows, or behind the oldest one.

The start is the release most of your packages agree on, lowered to the newest position any package below it can hold. A package pinned identically across ten releases agrees with everything and decides nothing; a SAP Commerce package two minors behind the CLI sets the start on its own. Packages whose positions all lie above that start are ahead of it - they stay where they are, and the steps that would have moved them are settled as already done rather than applied.

Consecutive releases often pin the same versions, so a placement like on 2.2.1..2.2.4 is a tie: node_modules cannot tell those releases apart. The skill takes the oldest member. That's the only direction whose failure mode is harmless - the steps of the later members whose packages already sit at the shared pin come back as already done, rather than a release's steps being skipped in silence.

Nothing else places the project. Your root package.json version field is often a sprint tag; yarn alokai version report falls back to that label when nothing matches; a version you remember is the least reliable of the three. Where any of them disagrees with the placement, the report says so beside it - and none of them is used. The skill never runs version check itself, because that command writes the resolved version into your manifests.

A package below the map

Where a package sits below the oldest version the matrix records for it, the steps that would bring it up predate the published steps, so the skill can't read them. It hands that package's whole area back - the package, its version, the oldest version the matrix knows, and the guide URL to read - and migrates the rest of the project. Nothing in that area is guessed.

The target is --to or the latest release. A target at or below the start is nothing to migrate, and the skill says so rather than moving anything backwards to reach it.

How it decides what applies

It profiles your repository before it reads the steps

Before it fetches the steps at all, the run builds a fact sheet of the project. Framework per store, every integration and module, the store list from alokai.config.json, the root scripts and what each covers, every patched package, every test suite and whether it needs a running server - each one backed by a path or a dependency it actually saw.

The order is the point. Profiling after reading the steps turns into hunting for confirmation of whatever the release happened to mention; profiling first produces a profile that doesn't know what the steps are looking for.

Each step's scope is an area id from Alokai's own taxonomy, and the skill carries a table mapping every area onto the file or dependency that proves it. That table is generated from the same taxonomy the steps are authored against, so the two can't disagree about what sapcc or nextjs means. Where a release is newer than the toolkit your project has installed, its steps can name a scope the local taxonomy has never heard of - the skill names that one in the report and gates it by hand rather than guessing.

What a step carries, and which fields gate it

The steps arrive as data - one JSON document carrying every release, each step a record:

{
  "id": "instrumentation-register",
  "version": "2.3.6",
  "title": "Call Alokai's `register` from your own instrumentation hook",
  "appliesTo": "nextjs",
  "onlyIf": "your `instrumentation.ts` exports a `register` that does not reference `@vue-storefront/next/instrumentation`",
  "onlyIfKind": "repo",
  "phase": "after-upgrade",
  "order": 2,
  "packages": ["@vue-storefront/next", "@alokai/cli"],
  "area": "storefront",
  "body": "..."
}

version is what the run selects on - every step whose release lies inside the range - and body holds the instructions themselves.

If that document can't be fetched

The run applies nothing and says so, rather than reconstructing steps from a changelog or from memory. The placement is planned from the matrix and the steps together, so without the steps there is no plan either: you get the profile it built, the URL and status that failed, and the upgrade guide, where the same range is assembled for reading - so the migration is still yours to do by hand.

Two verdicts are settled by the placement before any condition is read: a step whose named packages already sit at or past its release's pin is already done, and a step in an area below the map is handed back with that area - both listed in the report with their titles, so you can see what was assumed, and never applied.

Every other step applies only when appliesTo matches the profile and onlyIf holds. appliesTo is checked first, so an onlyIf about SAP Commerce internals is never even evaluated in a Commercetools-only project. appliesTo: all is every project, and onlyIf: always is unconditional inside the scope.

appliesTo is the gate nothing survives. Where the signal for an area is absent the verdict is skipped, and the report names the signal that was looked for - even where the step's mechanics would plainly have worked here. Scope is a fact about your project, not a preference.

Of the remaining fields, packages is the one the already done verdict above is read from - it names what delivers the change, and where every package it names already sits at or past the step's release there is nothing left to apply. The rest describe the change rather than gate it: phase puts a step before or after the package move, order places it within its release, area says where the change belongs - which isn't always the same as its scope - and supersedes names a step of an earlier release that this one replaces. Inside the range the skill folds those chains the same way the upgrade guide does: a later step replacing an earlier one removes the earlier one from the plan, and a step retiring one from before your start is reported as completing that chain.

Who settles a condition is published with the step

onlyIfKind says which of three kinds the condition is. It's authored with the release rather than guessed by your agent, and it decides who answers:

onlyIfKindExampleWhat happens
repo - names a file, an import, a config key, an API, or something the repository recordsyour instrumentation.ts exports a custom registerThe skill greps for it and then opens every hit before ruling - a match is a place to look, not a verdict. "Probably not present" isn't an answer; it runs the check.
outside - CI settings, deployment variables, backend or infrastructure configurationyou deploy against a non-default Console API URLHanded back to you as an action item stating the exact change and where to make it. Nothing in the working tree can settle it, so it is never guessed - and never quietly dropped because it isn't code.
judgement - intent, preference, or a fact about the teamyou want coding agents to follow Alokai conventionsPut to you. Any repo-checkable half of the condition is settled first, so you answer one question instead of the whole step.

Where your repository records the answer to an outside or judgement condition, the skill settles it rather than handing you a decision you don't have - git remote -v answers "you host code on GitLab", a missing patches/ directory answers a condition about patches - and says both the kind the release assigned and what settled it. That stops at what the repository records: a deploy workflow existing doesn't prove anyone runs it.

A condition often has several clauses that don't share a kind. Each is settled independently, and the joiner decides what an unsettled one costs: with and, one false clause ends the step; with or, a false clause rules out nothing and the rest govern. A step ruled out by a clause the skill can settle is simply skipped, whatever the rest would have needed. A step still in scope with one clause it can't settle is handed back with the others already answered, so you're left with one question rather than the whole step.

A step with several numbered parts is gated per part - a Nuxt-only part in a project with no Nuxt app is skipped on its own, and the report records the verdict part by part.

The skill prints the whole gated table before its first edit: every step in the range, its verdict, and for the ones that apply what is about to change. If you disagree with an applies, that's the moment to stop it.

Applied steps reach your per-store overrides

If a step edits apps/storefront-unified-nextjs/instrumentation.ts, every store that overrides that same relative path under apps/stores/<store>/storefront-unified-nextjs/ gets the same change. The override wins at build time, so a base-only edit silently does nothing for that store - which is the kind of miss that surfaces weeks later as one store behaving differently from the rest.

The same logic runs in reverse for scope: a step whose scope matches only some of your stores is applied only in those stores.

The package move isn't a step, and it's the biggest piece

Every release moves packages, and that move is published as the compatibility matrix rather than as a step. The skill applies it once, between the range's before-upgrade and after-upgrade steps, because the later steps target APIs that only exist once it lands.

It is where a migration most often stops half-done. yarn alokai version upgrade moves the core packages only - @alokai/cli, @alokai/connect, @vue-storefront/next, @vue-storefront/nuxt - and it moves them as one matrix row, so it aborts outright on a project whose packages sit on different releases. Most of the diff is not those four. Run the command, stop there, and the project still compiles and still boots - a half-upgraded project passes a build check.

So the skill doesn't run it. It works the diff package by package, forward only, and what each package needs comes from its placement and from the delivery type the taxonomy records for it:

  • A package below the target is written at the target version, bare, into every manifest that declares it - the apps, shared packages/* configs, and per-store overrides - after confirming that version exists on the registry.
  • A package already at or ahead of the target is left untouched, and the report lists it as such.
  • A package the matrix stopped pinning, or never pinned, is moved or removed only where a step in the range asks for it. A package no step names is not touched.
  • cli-plugin delivery is handed back, never run for you. A @alokai/cli-plugin-* package lives in the CLI's machine-wide plugin store rather than in any manifest, so installing it affects every project on your machine and would replace a plugin you have linked to a local checkout. You get the installed version, the target, and the command.
  • add-module delivery is yarn alokai add-module <name>, and only where your project already has the module or a step asks for it.
  • A package that's there transitively can sit in node_modules while appearing in no manifest. No manifest line is written for it up front; after the install the skill confirms its parent carried it to the target, and only a package left lagging gets a resolutions entry.

Then one install. It fires your project's install hooks, and the skill reads them before running it: a patch-package postinstall reapplies every recorded patch, and a patch that no longer applies to the moved package is handed back with the failing hunk; a preinstall that cleans .out means the composed workspace is gone until the next store build; a generated project's postinstall runs yarn alokai version check and yarn alokai ai sync, so the version label and the synced skills change under the CLI's hand rather than the agent's, and the report attributes those edits to the CLI. Afterwards every moved package is confirmed at its target in node_modules, and every removal is confirmed absent from the manifests and the lockfile.

A step of the release overrides all of this for the package it names, and the report says where that happened - a step is written against its release, while the list above is written against projects in general. That override stays inside what a step is allowed to ask for at all: an edit inside your working tree, a dependency bump, or a command your project already exposes. A step body asking for anything else - a download piped into a shell, a write outside the tree, a credential, a git remote, a disabled check - is treated as a defect in the published data and reported rather than run, and so is text in a step that addresses the agent instead of describing the release.

Your version label may move - but not by the skill

The skill never edits the version field in your root package.json itself, except where a step names it. In a generated project the install hook does: postinstall runs yarn alokai version check, which writes the CLI's own resolution of the project into the root, middleware, storefront and Playwright manifests. That resolution leans towards the newest release pinning the same core packages, so the label can land one patch above the target you named. In a project without that hook the label still reads what it read before the run. Either way the CLI injects it into every composed .env as NEXT_PUBLIC_ALOKAI_VERSION and the storefront publishes it, so the report states where the label and the dependencies now disagree in a form you can act on - "the label reads 1.0.0, the project carries 2.4.1's dependencies".

Validation

A migration isn't reported as done until every check that can run passes. Before its first edit the skill records a baseline: it runs lint, typecheck and every test suite that needs no running server, and writes down what already fails. After the migration, a failure that is also in the baseline is pre-existing - reported under its own heading and left alone. The correction loop targets only what the migration broke.

Then, in this order, each compared against that baseline:

  • Lint and typecheck - your project's root lint and typecheck scripts. Both run before the build because in a generated project they recompose the workspace and leave the storefront looking unbuilt. next build proves nothing about types in a generated app - ignoreBuildErrors is on - so typecheck runs whatever the build says, and a turbo replay that checked nothing is rerun with the cache forced.
  • Tests that need no server - the unit and integration suites the profile found, each by its own command.
  • Build - yarn alokai store build --store-id=<id> for each store whose scope matched an applied step, one deployable store when every applied step was scoped all. Success is read off the artifacts, not the command's log: a cached build unpacks an archive and reports success having compiled nothing, so the skill checks that the outputs carry a real compile's spread of timestamps and forces one if they don't.
  • Start - a store dev smoke run, polled until the storefront answers, then cross-checked by the listener's working directory - a status code proves something is on the port, and only the process's own directory proves it's your store. While the server is up, every applied step that changes runtime behaviour gets one curl that proves it: a header, a redirect, an endpoint.
  • Tests that need a server - end-to-end suites, against the server the skill started or after it shuts that one down, depending on whether the suite starts its own.
  • Shutdown - whatever the run started is stopped, and the report states in one line that the ports are free again.

Projects the CLI did not generate run through their own root scripts - lint, typecheck, build, dev or start - and the test suites the profile discovered, because customers share no layout. A project with no test suites at all is validated by lint, typecheck, build and start, and the validation section of the report says so, because a reader otherwise assumes tests ran.

When a check fails

A failure that isn't in the baseline is work left, not a result to write down. The skill classifies it and corrects the migration: a failure in a file a step edited means the step was misapplied, so it rereads the step against the file and redoes the edit; a failure naming a symbol a moved package changed, in a file no step names, is the release's own fallout, which the skill fixes and lists as a gap in the published steps; a failure matching a step it had skipped or settled as already done means a gate was wrong, so it re-gates that step on the evidence and says the verdict changed; a failure naming a config, an environment variable or a backend is outside the tree and is handed back with the exact change. Then it reruns the check.

The fix comes from the published steps, from the release's fallout in your tree, or from you - never invented. Disabling a check, loosening a type, adding a lint suppression or building with the compose-only flag is not a fix.

A correction that leaves the same failure standing is not repeated on the skill's own. It puts the failure to you in the session - the error text, what it tried, what it would try next - and you choose: try again, stop, or say what to do. An instruction you give that no step carries is applied and reported as a documentation gap marked as user-directed. On stop, the failure is handed back with every attempt and the error text. With nobody in the session to ask, the skill hands back after that first attempt. A rerun that fails on a different error is progress, and that failure gets its own correction round.

Blocked is not failed

A check has four outcomes, not two. Passed and failed are what they sound like. Blocked means the check never ran for a reason outside your project - a port already held by your docs server or another store, a suite that needs a credential the machine doesn't have, an unreachable host. Nothing of yours was started, nothing was killed to clear the way, and the report names the one thing that unblocks it. A migration with a blocked check is partially validated and the report says so.

The fourth is a pass that could not see the change: a build and a boot are blind to a type made required in a type your project never names, or a hook that fires only on install. For those the report says what the checks covered and what static artifact - a .d.ts, a manifest's exports, the composed .env - it read instead.

What you get back

The report opens with where the run got to: the placement per package, the start and what fixed it, the target, the areas handed back whole, and whether validation is green, partially blocked, or stopped. Then one table, one row per step in the range in plan order - including the steps that didn't apply, each with the field that ruled it out:

StepReleaseApplies to / Only ifVerdictWhat happened
instrumentation-register2.3.6nextjs / your instrumentation.ts exports a register that does not reference @vue-storefront/next/instrumentationappliesapps/storefront-unified-nextjs/instrumentation.ts edited, and the same path in two store overrides

Every step gets exactly one of seven verdicts: applies; skipped, with the gate and the signal that ruled it out; already done, where the packages the step names were at or past its release before the run; handed back, for an outside condition nothing in your repository can settle or for an area below the map; needs an answer, for a judgement condition with nobody in the session to ask; satisfied, no action, where both gates hold and nothing in your project changes; and applied, mechanism inert, where the step was applied as written and doesn't achieve what it says - reported with the evidence that it's inert, and with the smallest change that does achieve it.

Then eight sections, in this order:

  1. Needs an answer - each judgement call with the default and the one thing the answer turns on.
  2. Handed back - areas below the map with their packages and the guide URL, outside conditions with the exact change and where, plugin moves, patches that failed to apply, steps that couldn't be applied from what's in the tree.
  3. Already done - the steps the placement settled, with their titles.
  4. Packages - one line per package: what happened to it and where it landed.
  5. Documentation gaps - every fix validation needed that no step named, with the file and the error; the ones you directed are marked so.
  6. Pre-existing failures - items from the baseline still failing.
  7. Validation - each check with its outcome, what the passes verified and what they couldn't observe, and the shutdown line.
  8. Changes - the files that changed and the files the run created, so you can review and stage them; the skill stages nothing itself.

Every section is printed even when it's empty, saying None. That's the same reason the table lists steps that didn't apply: silence can't be told apart from an omission.

Getting the skill

The skill reaches your agent through alokai ai sync, like the rest of the toolkit (Syncing skills):

ls .claude/skills/ai-toolkit-alokai-migration   # Claude Code
ls .agents/skills/ai-toolkit-alokai-migration   # Codex, Gemini CLI, GitHub Copilot

A project still on an older @alokai/ai-toolkit doesn't have it yet - bump the package and re-sync. The steps themselves always come from the docs site, so the skill can migrate a project to any published version regardless of which toolkit version shipped it.

On this page