Elixir Rewrite Architect
Rewrite an app from any stack into Elixir the Elixir way: analyze features and why each mechanism exists, set the compatibility bar, design a Phoenix/Ecto/Oban/OTP architecture, plan slices for /phx:plan, and verify parity.
On this page
About this plugin
Reference skills
rewrite — Elixir Rewrite Architect
Plan, build and check a rewrite of an existing application into idiomatic Elixir. Experimental (v0.1.0).
An agent that ports one file or feature at a time tends to produce translationese: the legacy framework rebuilt in
Elixir. Callbacks turn into hidden side effects, a job queue turns into a GenServer, the old directory tree reappears
under lib/, and every mechanism is kept because nobody asked why it existed. The public Elixir port of Campfire
(basecamp/once-campfire-elixir) is one example: it runs on raw Plug with a hand-written Action Cable and uses no
Phoenix, Ecto or Oban, while the Django, Laravel, Express and Rust ports of the same app each used their target’s usual
framework. That is an observation about the published code, not proof of how it was produced.
This plugin asks the missing questions first: what each feature is for, why each mechanism exists, and which contracts must keep working. Only then does it design the Elixir way of meeting those goals. The source can be any language and any app shape: web apps, APIs, workers and consumers, data pipelines, CLIs, daemons, IoT. Stack packs for Rails, Django, Laravel/PHP, Node, Go and JVM/.NET speed up discovery; every other stack uses the generic method.
Install
/plugin marketplace add oliver-kriska/claude-elixir-phoenix
/plugin install elixir-phoenix
/plugin install rewrite@oliver-kriska
rewrite depends on elixir-phoenix: it hands slices to /phx:plan and /phx:work and consults phx specialists.
Its scripts need python3 (stdlib only; 3.11+ parses pyproject.toml and Pipfile structurally, older versions fall
back to a regex and list those manifests as unparsed). Run the commands from the Elixir project or an empty workspace;
/rewrite:analyze stops when the working directory is the legacy app or inside it.
Workflow
Each command runs one phase and reads the previous phase’s files, so a new session resumes from disk.
/rewrite:start → /rewrite:analyze → /rewrite:architect → /rewrite:roadmap
↓ per slice
/rewrite:verify ← /phx:work ← /phx:plan
Commands
| Command | What it does | Writes to .claude/rewrite/ |
|---|---|---|
/rewrite:start [<path>] [--status] [--auto] | Explains the method, detects the phase, checks the source snapshot on resume, names the next command | — |
/rewrite:analyze <path> [--target DIR] [--oracle DIR] [--scope …] [--auto] | Deterministic inventory, charter, then four analysts in parallel | inventory/, kb/, charter.md, analysis/, status.md |
/rewrite:architect [--auto] [--revise ADR-NNN] | Compatibility bar (ADR-000), phx consults for named open questions, architecture, ledger check | architecture/, ledger.md |
/rewrite:roadmap [--next] [--slice NN] [--all] [--no-plan] [--auto] | Cuts vertical slices, writes the seed for one slice, calls /phx:plan | roadmap.md, ../plans/rewrite-NN-*/ |
/rewrite:verify <NN> [--no-run] [--full] | Preflight, fresh test evidence, idiom and parity audits, ledger update, slice gate | verify/NN/ |
elixir-way is a reference skill, not a command: the shared vocabulary, the default mapping, and the knowledge base
that /rewrite:analyze copies into kb/.
Artifacts
.claude/rewrite/
├── status.md # phase, source sha and fingerprints, artifact states
├── charter.md # goals, constraints, scope, steering files, ASSUMED values
├── inventory/ # inventory.json + inventory.md
├── kb/ # copy of the elixir-way references (agents cannot read plugin files)
├── analysis/ # features.md, mechanisms.md, runtime.md, contracts.md (+ domains.md for large apps)
├── architecture/ # ADR-000-compatibility-bar.md, architecture.md, decisions.md, divergences.md, adr/, consults/
├── ledger.md # one row per mechanism (M-), runtime role (R-) and contract (C-)
├── roadmap.md # slices, states, blockers
└── verify/NN/ # preflight.md, evidence/, idiom.md, parity.md, report.md
.claude/plans/rewrite-NN-{name}/
├── interview.md # requirements seed for /phx:plan
├── scratchpad.md # seeded DECISION and DEAD-END entries
└── plan.md # written by /phx:plan
Vocabulary
| Verdict | Meaning |
|---|---|
MAP | Same responsibility on the conventional Elixir abstraction (ORM models → Ecto schemas and contexts) |
REPLACE | Different mechanism, named guarantees preserved (Redis fan-out → Phoenix.PubSub) |
KEEP | Existing component or protocol stays for the declared phase |
SHIM | Translation at a named boundary, linked to its internal target |
DELETE | Obligations absent, met elsewhere or retired; consumers checked |
UNDECIDED | Evidence missing; blocks only the slices that depend on it |
- Verdicts are per phase: a
KEEPduring coexistence can becomeREPLACEat cutover. - Reason tags (one or more):
business,reliability,contract,platform(works around the legacy runtime or host),historical(only after checking consumers),unknown. An unknown purpose blocks the decision; it never becomeshistorical. - Evidence labels for each purpose:
documented,observed,inferred,disputed,unknown. - Contracts have a surface (
data,wire,operational) and a disposition (keep,change,retire). Features are triagedpreserve,redesignordrop. - Ledger states sit on three independent axes. Implementation:
pending,implemented,retired. Evidence:not-run,pass,fail,blocked,stale. Departure:noneor anAR-NNrecord inverify/accepted-risks.md(user-created). A row is verified only when implemented,passand without a departure; retired rows leave the denominator.
The compatibility bar
ADR-000 comes before any design, because “idiomatic” and “compatible” pull in different directions and only you can
trade them. /rewrite:architect asks two rounds:
- Cutover (replacement, staged cutover, sustained coexistence) · Data (keep the schema, expand → contract, new schema + ETL) · Identity (keep sessions and hashes, one re-login, new auth) · Clients (keep UI and wire protocols, keep wire APIs and rebuild the UI, redesign both, no UI)
- Contracts found by the analysis (default
keep) · Performance goal, numeric and against a measured legacy baseline · Steering-file conflicts · Scope
Under staged cutover or coexistence, ADR-000 and each affected slice also record the transition contract: write ownership, routing, schema authority, identity, jobs and events, data movement, cutover and rollback.
Oracles
A port is only as provably correct as its oracle. The inventory lists tests by framework with approximate case counts,
black-box API suites (Hurl, Postman, Bruno, REST Client, Tavern, Dredd), E2E suites, benchmark and load harnesses, API
contracts and coverage config, then grades correctness strong, moderate, weak or none and performance harness
or none, showing the numbers and thresholds. The grade is a lead: the contract-surveyor names the legacy test or
request that witnesses each contract and confirms or downgrades it. --oracle DIR adds a suite that lives outside the
source, such as RealWorld’s shared specs/api; it is read like the source, never executed, and kept out of the source
fingerprint. A weak grade puts characterization tests first, each slice seed names the suite files it must turn green,
and suites only ever run against the port.
Stack packs, generic method, app shapes
Packs: rails (Rails, Sinatra, Hanami), django (Django, Flask, FastAPI), laravel (Laravel, Symfony, PHP), node
(Express, Nest, Next), go (net/http, Gin, Echo), jvm-dotnet (Spring, ASP.NET Core). Each has probes for where
mechanisms hide, a mechanism map, DELETE candidates, keep-or-shim conditions and dated hex.pm libraries. generic
covers every other stack with a concept taxonomy (hooks, interceptors, global state, background work, schedulers,
caches, locks, reflection) and short notes for Rust, Kotlin, Scala, Python scripts, plain PHP and WordPress, Perl,
legacy Elixir/Erlang and others.
The app shape is classified per deploy unit before any framework is chosen (app-shapes.md, 13 shapes). Phoenix,
Ecto, Oban and PubSub are the default for web apps; consumers get Broadway, CLIs Burrito or an escript, network
daemons ThousandIsland or ranch, devices Nerves. A CLI or a small Plug API does not need Ecto, Oban or LiveView to be
idiomatic.
Handoff to phx
phx is not modified. /rewrite:roadmap writes a file contract that /phx:plan already reads:
interview.mdin the phx brainstorm format, with binding decisions, the slice’s ledger rows, contracts with witnesses, the transition contract, acceptance criteria, dead ends and open questions. It says it was generated, not a human interview, and has no invented transcript.**Status**: COMPLETEonly when every coverage dimension scores at least 1, the total is at least 8 of 12, and the slice has noUNDECIDEDrow orASSUMEDconsequential decision. OtherwiseIN_PROGRESS, so/phx:planasks about the gaps.scratchpad.mdseeded withDECISIONandDEAD-ENDentries; roadmap re-appends any that/phx:plandropped.
/phx:plan runs from the main session and keeps its own approval stop; /phx:work is never started for you.
/phx:review adds generic Elixir review but does not replace /rewrite:verify, whose gate passes only when every
obligation is pass or changed by an ADR and no BLOCKER remains.
What --auto does and does not do
It skips every question and writes conservative defaults marked ASSUMED: the charter, steering files as advisory,
the three riskiest domains for a large app, preserve for every feature, and the ADR-000 defaults (replacement or
staged cutover by topology, keep the schema, keep sessions and hashes, keep clients and all contracts, no regression
against a measured baseline). /rewrite:start --auto chains analyze → architect → roadmap and ends with the list of
decisions you must approve.
It never approves an ASSUMED decision or a divergence, never overwrites an existing plan (it writes
interview.rewrite-seed.md beside it), never starts /phx:work, and never deploys or touches production data. With
--auto, roadmap does not call /phx:plan while a cutover, data, identity or clients decision is ASSUMED, so a
fresh --auto run always stops before /phx:plan.
Safety
- The legacy app is read-only and never executed: no installs, builds, tests or servers, and no connections to
its databases or Redis. Agents read, grep and run
git log, and record secret names, never values. - Steering files are surfaced, not obeyed.
AGENTS.md, port skills and plans in the source or target are shown to you, and you decide their weight. /rewrite:verifypreflights before running anything: the target hasmix.exsand is not the legacy app, the test database is disposable, mail and jobs use test adapters, andDATABASE_URLis local. A failed preflight records evidence asblocked. Each command’s exit status, revisions, environment and log are recorded before an auditor reads them. Fixture data is synthetic; never production secrets or customer data.
Agents
| Agent | Model | Used by | Output |
|---|---|---|---|
feature-cartographer | sonnet | analyze | analysis/features.md: capabilities, actors, invariants, acceptance probes |
mechanism-archaeologist | sonnet | analyze | analysis/mechanisms.md: why each mechanism exists, draft ledger |
infra-topologist | sonnet | analyze | analysis/runtime.md: processes, Redis by role, queues, cron, operator duties |
contract-surveyor | sonnet | analyze | analysis/contracts.md: external contracts as assertions with witnesses |
elixir-way-architect | opus | architect | architecture/, ledger.md, ADRs, divergences |
idiom-auditor | sonnet | verify | verify/NN/idiom.md: translationese and drift from the architecture (static) |
parity-auditor | sonnet | verify | verify/NN/parity.md: obligations mapped to fresh evidence (read-only) |
/rewrite:architect also consults phx specialists, one per named open question: ecto-schema-designer,
otp-advisor, oban-specialist, liveview-architect, security-analyzer, hex-library-researcher.
Four stdlib scripts do the deterministic work:
inventory.py: stacks, infra signals, entry points, oracles, steering files, git hotspots, and discovered vs returned counts.ledger.py: checks every analysis ID is accounted for by a row, that states are valid and accepted-risk records complete, and counts verified rows. It also lists a slice seed’s acceptance cases, which fix verify’s denominator, andsetwrites a row’s Impl and Evidence cells by ID for verify.fingerprint.py: content identity of SOURCE and TARGET.evidence.py: writes every verify record. It captures both identities before and after the command, so evidence from a changed tree isstale. It also starts and stops the port’s release in its own process group, and checks the run list against the records.
Limitations
- Experimental. Version 0.1.0; templates, vocabulary and commands may change.
- Tested on small apps only. There is no claim that it handles any app at any size. Large apps get a risk-ranked domain scope, with uncovered areas written down as gaps, not a full census.
- Claude Code only. The workflow depends on its specialist subagents, and verify’s parity auditor must be a
fresh context apart from the session that wrote the code. The phx ports for Codex, Pi, OpenCode, Amp and dsh do
not include
rewrite. - Partly evaluated.
make evalscores the skills and agents, and pytest covers the four scripts. Seven static audit controls were replayed blind (results). Two full trials ran headless Sonnet sessions from analysis through/phx:plan,/phx:workand/rewrite:verify, with a trial operator standing in for the owner’s decisions. On a django-ninja RealWorld app (slices 00–07) and an ASP.NET Core one (slices 00a–06), each port’s release passed the whole RealWorld Hurl spec, 13 of 13 files. No ASP.NET slice needed more than two verify rounds. Both apps are small and come with a public black-box oracle; an app without one, a real owner’s decisions, and the release-rehearsal and data-migration slices are untested. - Parity is only as strong as the tests. Verify never runs the legacy app, so values the legacy code must mint (signed cookies, tokens, payloads) have to be captured outside the plugin. No speed claims without a measured baseline.