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 upgrade does what is the same for every brain: it moves the pin, installs and regenerates the files the engine owns. Then it writes upgrade/TASK.md.
  • The agent follows upgrade/TASK.md to 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)
  1. Refuses a brain whose @omnislash-ai/* packages sit outside peerDependencies (contract.md § 1).
  2. Resolves the target version and records from, the version the brain is on: --from, else the current exact pin, else the engine installed in node_modules.
  3. Rewrites every engine peer pin to the target and runs pnpm install. First it drops from pnpm-lock.yaml each locked third-party peer the target CLI’s range no longer admits (an auto-installed bullmq below a raised floor): pnpm keeps a locked version even when it fails strict-peer-dependencies, and no pnpm flag re-resolves it. Each dropped entry is printed.
  4. 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 range from < version <= target into upgrade/ and writes upgrade/TASK.md, then prints the task. A range with no entries writes nothing and prints no agent steps. It reads no brain code, so a brain the new engine cannot load yet still gets the entries that explain the fix.
  5. Runs the new CLI’s omnislash regen: Dockerfile, .dockerignore, .github/workflows/{deploy,ci}.yml, docs/engine/, docker/*.yml.example, skills/engine/, constitution/engine-capabilities.json and the engine block of CLAUDE.md / AGENTS.md (brain-dev-guide.md § Updating Engine has the detail and the opt-outs). Regen loads omnislash.config.ts; when that fails with a task written, the error points at upgrade/TASK.md: apply its entries, then run omnislash regen.
  6. 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

  1. Commit the bump: everything omnislash upgrade left, upgrade/ included, as chore: update engine to v<version>. Do not push.
  2. Before deploy: each entry’s Detect and Verify, read-only against production (db tools, doctor_check). A failed blocker stops the upgrade: report it to the owners and go no further.
  3. In repo, in order: Detect → Apply → Verify, one commit per entry (upgrade(<version>): <summary>), its tick included.
  4. Gates: omnislash doctor, pnpm type-check, the brain’s build.
  5. 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).
  6. After deploy: Detect → Apply → Verify against production, ticks committed.
  7. Tell the owners: one message with every entry’s “Tell the owner” section and every skipped entry with its reason.
  8. 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 upgrade on a brain that is already on the target and still has upgrade/ prints the unfinished task again. Resume at the first unticked entry.
  • omnislash doctor warns Upgrade in progress while upgrade/ exists and names the next unticked entry. omnislash doctor --check upgrade-in-progress passes 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: --from cannot 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)
FieldMeaning
phase: before-deployRead-only checks against production before anything ships: views or keys a migration would trip on, table sizes that decide the deploy window.
phase: in-repoEdits to the brain, committed with the bump.
phase: after-deployProduction data fixes through db tools, runtime checks (doctor_check), owner notices.
severity: blockerWithout it the brain does not boot or the deploy fails.
severity: requiredWithout it, behaviour breaks.
severity: cleanupNothing breaks if it waits.
sqlThe 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_check at 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.

Screens from Sixty Seconds Studio, a fictional demo studio, running on the real dashboard.