Upgrading the engine
An engine upgrade is an agent’s job, never a person’s: a coding agent working in the brain repo, on the strongest model available. It has two halves:
omnislash upgradedoes what is the same for every brain: it moves the pin, installs and regenerates the files the engine owns. Then it writesupgrade/TASK.md.- The agent follows
upgrade/TASK.mdto the end. The task holds what the crossed releases need done in this brain that no command can do: renamed names your files spell, checks against your production database, data fixes, behaviour changes to tell the owners about.
Nothing upgrades a brain from CI. The engine block that omnislash regen keeps in your CLAUDE.md
(and AGENTS.md) tells an agent asked to upgrade the engine the same thing, and every other agent
(your production agents load that file too) to leave upgrade/ alone.
What omnislash upgrade does
omnislash upgrade # latest release
omnislash upgrade 1.22.0 # an exact version
omnislash upgrade --from 1.20.0 # plan the agent task from 1.20.0 instead of the current pin (merged into an open task)
- Refuses a brain whose
@omnislash-ai/*packages sit outsidepeerDependencies(contract.md § 1). - Resolves the target version and records
from, the version the brain is on:--from, else the current exact pin, else the engine installed innode_modules. - Rewrites every engine peer pin to the target and runs
pnpm install. First it drops frompnpm-lock.yamleach locked third-party peer the target CLI’s range no longer admits (an auto-installedbullmqbelow a raised floor): pnpm keeps a locked version even when it failsstrict-peer-dependencies, and no pnpm flag re-resolves it. Each dropped entry is printed. - Runs the new CLI’s
omnislash upgrade-plan --from <from> --to <target>. Only the new CLI ships the upgrade entries of the releases you are crossing. It copies each entry of the rangefrom < version <= targetintoupgrade/and writesupgrade/TASK.md, then prints the task. A range with no entries writes nothing and printsno agent steps. It reads no brain code, so a brain the new engine cannot load yet still gets the entries that explain the fix. - Runs the new CLI’s
omnislash regen:Dockerfile,.dockerignore,.github/workflows/{deploy,ci}.yml,docs/engine/,docker/*.yml.example,skills/engine/,constitution/engine-capabilities.jsonand the engine block ofCLAUDE.md/AGENTS.md(brain-dev-guide.md § Updating Engine has the detail and the opt-outs). Regen loadsomnislash.config.ts; when that fails with a task written, the error points atupgrade/TASK.md: apply its entries, then runomnislash regen. - Offers to commit the bump (interactive terminals only). With a task written it commits without pushing, because a push deploys and the task’s first steps come before the deploy. A non-interactive run (an agent) never touches git: step 1 of the task commits.
The last line printed is the next step: follow upgrade/TASK.md, or run omnislash doctor when
there is no task.
Any failure exits non-zero with the reason and a hint. The command is idempotent: fix the cause and
re-run the hinted command (it carries --from, so the plan covers the same range). An install that
fails with ERR_PNPM_PEER_DEP_ISSUES means a version the brain pins itself (a dependency in
package.json or an override in pnpm-workspace.yaml) sits outside a peer range of the new
engine: pnpm names it (unmet peer <name>@<range>: found <version>). Move that pin inside the range,
then re-run.
An upgrade started by an older CLI. A CLI from before agent tasks never runs upgrade-plan. The
new CLI’s regen covers that case: when the package.json in your last commit pins an older
engine than the installed one and no task exists yet, it writes the task for that range before it
loads the brain’s config, and prints it.
So do not commit between the install and the regen. If you did, run
omnislash upgrade --from <the old version>. That older CLI then says to commit and push: commit,
but push only at the task’s deploy step. Until then the regenerated workflows’ Upgrade task gate
fails the deploy (below).
The task: upgrade/
upgrade/
TASK.md # the protocol + the ordered index
1.20.0/01-f2-ids-precheck.md # one file per entry: <version>/<NN>-<id>.md
1.20.0/02-record-agent-model-overrides.md
1.21.0/02-access-yaml.md
model-overrides.md # a working file an entry asked for (optional)
upgrade/ sits at the brain root and is committed. It is the upgrade’s only record: there is no
ledger of applied steps. The pinned version says where the brain is, and the folder is the to-do
list. The final commit deletes the folder.
TASK.md has the static protocol, then the index grouped by phase, in release order (version, then
NN):
### Before deploy
- [ ] 1.20.0/01-f2-ids-precheck.md — Agent and conversation ids become text; … (blocker)
### In repo
- [x] 1.21.0/02-access-yaml.md — chats.yaml is now access.yaml (blocker)
- [x] 1.21.0/07-no-dashboard-daily-brief.md — … (cleanup) — n/a: no dashboard.dailyBrief key
The agent ticks a line by changing - [ ] to - [x], and appends — n/a: <why> when Detect says
the entry does not apply. The file name stays first on the line. Entry files are never edited.
The protocol
- Commit the bump: everything
omnislash upgradeleft,upgrade/included, aschore: update engine to v<version>. Do not push. - Before deploy: each entry’s Detect and Verify, read-only against production (db tools,
doctor_check). A failedblockerstops the upgrade: report it to the owners and go no further. - In repo, in order: Detect → Apply → Verify, one commit per entry
(
upgrade(<version>): <summary>), its tick included. - Gates:
omnislash doctor,pnpm type-check, the brain’s build. - Deploy: push to the branch the deploy workflow ships. Its first gate,
Upgrade task, fails while a before-deploy or in-repo entry is unticked. A failed deploy is reported to the owners with its log (see When the deploy fails). - After deploy: Detect → Apply → Verify against production, ticks committed.
- Tell the owners: one message with every entry’s “Tell the owner” section and every skipped entry with its reason.
- Finish: delete
upgrade/in a final commit (upgrade: done) and push.
Work on a separate branch, in a separate worktree when the environment offers one, so that nothing deploys before step 5.
Resume
omnislash upgradeon a brain that is already on the target and still hasupgrade/prints the unfinished task again. Resume at the first unticked entry.omnislash doctorwarns Upgrade in progress whileupgrade/exists and names the next unticked entry.omnislash doctor --check upgrade-in-progresspasses once the folder is gone.omnislash upgrade --from <version>plans a range again. So does upgrading further while a task is in progress. The new entries are merged into the task: entries already listed stay with their ticks and notes, and everything is re-sorted into release order. A merge only adds entries:--fromcannot narrow an open task.- Detect is written to find work already done, so running an entry a second time is safe.
Working files
An entry may ask the agent to keep a file in upgrade/. Usually it is a note that a before-deploy
step hands to an after-deploy step, such as the agent models set by hand that the deploy is about to
reset. Re-planning never deletes these files. They are committed like a tick and go away with the
folder in step 8.
Anatomy of an entry
---
summary: chats.yaml is now access.yaml # one line, shown in the index
phase: in-repo # before-deploy | in-repo | after-deploy
severity: blocker # blocker | required | cleanup
sql: [] # engine migrations this entry covers
---
## Detect
## Apply
## Verify
## Tell the owner (optional)
| Field | Meaning |
|---|---|
phase: before-deploy | Read-only checks against production before anything ships: views or keys a migration would trip on, table sizes that decide the deploy window. |
phase: in-repo | Edits to the brain, committed with the bump. |
phase: after-deploy | Production data fixes through db tools, runtime checks (doctor_check), owner notices. |
severity: blocker | Without it the brain does not boot or the deploy fails. |
severity: required | Without it, behaviour breaks. |
severity: cleanup | Nothing breaks if it waits. |
sql | The engine migrations (_journal.json tags) this entry is about. The deploy’s db-migrate applies them; the entry says what they need from your brain, often nothing (“not applicable”). |
- Detect: how to tell whether the entry applies to this brain. It can be a grep, a file check, a read-only SQL query or a doctor check. “Not applicable” means skip it and say why.
- Apply: the steps. Code, SQL and file moves in it are material to adapt to this brain, not a script to run blind.
- Verify: a check that must pass. Preferably a doctor check by id
(
omnislash doctor --check <id>locally,doctor_checkat runtime), else a command or a query. - Tell the owner: a behaviour change the owners should hear about. The agent relays it in step 7.
A released entry never changes. A correction ships as a new entry in a later release.
The deploy is downtime
Every deploy stops the world for a few minutes: the drain, the migrations, one boot. Nothing of the
previous version runs while the new one migrates and starts, which is why a release can ship its
final shape with no compatibility layer. Pick the moment accordingly: a before-deploy entry may
report table sizes for that reason. The full sequence is in operations.md
§ Brain CI/CD.
What survives a deploy:
- Kept running: Postgres, Redis, Restate, Caddy (it holds requests for up to 30 s per restart) and the monitoring stack. Alerts are silenced for the deploy window.
- Queued jobs: the worker finishes the jobs it is running (up to 60 s). Jobs still waiting stay queued for the new version.
- Agent turns: not killed. A turn paused on a question to a person keeps waiting and resumes on the new code; a turn whose process died is cancelled cleanly when the engine boots.
- Your pipeline runs: not killed.
- The engine’s other in-flight work (workflow runs, planning, loop cycles and the like): killed, then re-derived from its database rows when the engine boots. One exception: a planning clarification still waiting for an answer at the deploy is lost.
When the deploy fails
The deploy log says how far it got.
- Before the migrations: the previous version was started again. Production is unchanged and still on the old engine. Fix the cause in the repo and push again. The after-deploy entries wait.
- In the migrations: no rollback. The engine services stay stopped, because the previous version may not read what the migration already wrote and the new one must not boot on a half-applied schema. Do not revert the pin. Read the migration’s error in the deploy log. If the cause is in the brain (one of your own migrations, a view or foreign key of yours that the engine migration trips on), fix it and push. If it is the engine’s, report it to the owners: the fix is a later engine release, upgraded to the same way.
- After the migrations: the new version is up as far as it starts. Fix forward: find the
failing service in the log and with
omnislash doctor, fix, push.
In every case, report to the owners with the log and leave upgrade/ in place. The task resumes
from its first unticked entry once a deploy succeeds.