Profiles and aerix tune
Prompts live in a per-project git profile with a branch per model, and aerix tune proposes revisions from your sessions.
What a profile holds
The prompts are built into the binary: system.md, routing.md,
brief.md (the subagent brief; the placeholders brief, check and done_looks_like, each in double braces), verifier.md
(brief, check, summary and files, each in double braces), tools/<tool>.md for read, write,
edit, bash, glob, grep, agent, context, plan, repo_map and skill, and tune/reflect.md
and tune/reconcile.md (the prompts aerix tune and aerix profile sync use). A session does not
read them from the binary: it reads them from its profile, a bare git repository at
$XDG_DATA_HOME/aerix/profiles/<name>.git that Aerix creates and owns. The turn contract (the
{"calls": ...} / {"final": ...} shape and its example) is rendered by code into the system
segment and is in no profile.
- The name. By default the project key (one profile per repository);
profileor--profile NAMEattaches another. A name is[A-Za-z0-9_-][A-Za-z0-9._-]*; anything else is a usage error. - Branches.
mainis the model-agnostic base; its first commit is the embedded set, laid out as the built-in prompts are.model/<model>is made frommainthe first time a session of the profile runs that model, the model being the main member's model reference exactly as configured, percent-encoded into one ref component (model/qwen3-0%2E6b); an alias and its full reference are two branches. The session reads its prompts once at startup from its model base's head (a resumed one: its recorded commit);session/<id>is made at that commit by the session's first request, which recordsprofile,profile_branchandprofile_commitin itssession.start; a later commit does not change a running session. - Resume reads the files the session tuned from the commit it recorded, even if the main
member's model changed since; a file it never tuned follows the current release's embedded text
(the rule below).
--profile-refreshmoves the session's branch to its model base's head first. A recorded commit that no longer exists is named in one line and the model base's head is used. - Nothing propagates upward: no merge from a session branch to a model base, nor from a model
base to
main. - Startup lines. A prompt the commit lacks comes from the embedded set, with a line naming it.
An artifact whose recorded upstream changed (a newer Aerix's prompt, an edited skill file) is
named stale, one line each; sessions never reconcile. A tuned one is used as it is. A built-in
prompt never tuned (its text is still the upstream it records, as every file a model base
inherited from
mainis until tuned) uses this release's embedded text, its line saying so, and nothing is written;aerix profile syncrecords it. A profile that cannot be opened is one line, and the session uses the embedded prompts. A<cwd>/.aerix/prompts/or$XDG_CONFIG_HOME/aerix/prompts/that still holds files (a symlink to a file counts, and a directory past the 256 entries the check looks at gets the line too) gets one line: they are ignored. - Skill and agent overlays. A commit may hold
skills/<source>/<skill name>/SKILL.mdandagents/<source>/<name>.md, the source being the plugin id oruser/project. A loaded skill or agent definition whose overlay path the commit holds takes the overlay's text instead of its file's. A name or source that is not one path component (empty,.,..,.git, holding/,\or a control character), or a plugin iduserorproject, has no overlay path: one line, and the definition loads from its file. Every prompt and overlay comes from the main member's branch.projectoverlays (skills/project/...,agents/project/...) are applied, tuned and synced only when the profile's name is the cwd's project key: another project attaching the profile with--profilehas its own project skills at those paths, so there they are skipped, with one line naming the profile. - Upstream hashes.
upstream.jsonon a branch maps each tuned artifact to the SHA-256 of the upstream text it was tuned from; an overlay's upstream text is kept beside it asupstream/<path>. Staleness is that hash differing from the current upstream's.
Editing by hand
Customizing by hand is editing a model branch: aerix profile edit model/<model> FILE, or git on
the bare repo. edit refuses main, a session/<id> branch (nothing reads a session branch's
later commits: a resumed session reads its tuned files from its recorded commit, and
--profile-refresh moves the branch to the model base, discarding them), upstream.json and upstream/, an overlay path no
loaded skill or agent has, and a text that drops what code depends on (below); it commits with a
compare-and-swap on the branch, so a concurrent write is refused by name, not lost.
aerix profile listshows a profile's branches andaerix profile show model/<model> system.mdone text. Sessions started after an edit use it; sessions already running, and resumed ones, keep their commit until--profile-refresh.- Shorter system and tool text shrinks every context; a sharper
routing.mdchanges which shape requests get. The turn format is not in any prompt: code renders it, so no edit can break the parser.
aerix tune
aerix tune reads the profile's sessions whose session.start names this profile and the
model (newest first, at most --sessions; imported Claude Code sessions are left out), extracts
evidence per artifact (each request's outcome, failed verifier verdicts and their reasons, refused
and failed tool calls, dropped turns, compactions, a next request that reads as a correction,
skills and agent definitions used), and asks the reflection member once per artifact that has
evidence, under context.max_new_tokens and the Json constraint, for {"revise": bool, "text": ..., "why": ...}. Before committing, a revision is refused by name if it drops a placeholder of
brief.md or verifier.md, the verifier's verdict shape ("verdict", "pass", "fail",
"reason") or one of routing.md's shape words (answer, edit, investigate, plan), or is
over twice its embedded size (an overlay: twice its source's); an empty or unchanged answer revises
nothing. The revisions are one commit on model/<model>, its message naming the reflection
member, the session ids and each artifact's why; nothing revised commits nothing and says so.
stdout carries one line per artifact asked (revised, kept or refused), then committed <commit> on <branch> with a review: aerix profile ... diff line naming that commit against its
parent, or nothing to revise (...). With --dry-run the revisions are not committed: after the
per-artifact lines, each revision is printed as diff <path> followed by its unified diff, then
dry run: N revisions from M sessions; nothing is committed on <branch>. When it has sessions to
read, tune first runs a sync of that base, dry run or not (the sync commits what it reconciles,
as aerix profile sync would); a path the sync leaves stale is not tuned.
Set tune.member to reflect with a different model than main (it is loaded for the run and
unloaded after) and tune.flags to its serving flags; --sessions N bounds how much history is
read. Every revised line carries the model's why, and the commit holds them all. Read the
commit before relying on the revisions, and revert by hand (aerix profile edit, or git on the
bare repo) what you do not want. aerix tune --dry-run prints each revision as a diff and
commits none, so you can read what the model would change first; the sync that precedes a tune
still commits what it reconciles.
Nothing measures whether a tuned prompt is better. A small model's revisions are a proposal to read, not an improvement.
aerix tune commits what the model writes without a review step, and its evidence comes from your
sessions: requests, tool errors that quote repository files, verifier reasons. Text planted in any
of those reaches the reflection model, and a revision that carries it is committed on the model
base and used by every later session of that model: a prompt injection can persist in a profile.
This is a known limitation; no confirm step exists. --dry-run shows the revisions without
committing them; review what a run committed with the review: line it prints, aerix profile log model/<model> and aerix profile diff, and revert with aerix profile edit or git on the
bare repo.
aerix profile sync
aerix profile sync reconciles: for each tuned artifact whose upstream changed, the reflection
member gets the old upstream text, the diff to the new one and the tuned text, and answers
{"text": ..., "why": ...}; the guards above run on the answer, and the result is committed with
the new upstream hash and the why. An answer equal to the tuned text keeps it and records the new
hash. An untuned artifact simply takes the new upstream, and an embedded file the branch lacks is
added; neither asks the model. An artifact it cannot reconcile (its old text or its source is gone,
the answer is refused) is named and stays stale.
After an Aerix upgrade changes a built-in prompt, or after a skill or agent file you tuned changes,
startup names each stale artifact; aerix profile sync reconciles them (aerix tune syncs first).
A built-in prompt you never tuned already follows the new release at startup (its line says the
embedded text is used); sync records it.
tune.member's load and unload are not leased: the pool has no lease, so running aerix tune or
sync while another session uses the same model as its reflection member can unload it under that
session.