v0.12.0
Highlights
The models are the schema. 90 hand-written table declarations are gone. aegis init and aegis add run the generated project's own migration generator in its venv: replay the existing revisions onto a scratch database, autogenerate the difference against the model metadata, prune to additive operations, write one revision per service. Ownership comes from where a model lives, cyclic foreign keys emit once both tables exist, and a generated revision declares its own stamp signature so startup re-adoption reads it off the revision rather than a parallel table. A Postgres oracle asserts the diff against the models is empty, and every generated project carries the same check on SQLite. migration_generator.py drops from 5,080 lines to 775.
An update stops spending your work quietly. Answers are derived from the project rather than guessed from template defaults, so a question added after your project was generated no longer renders with an answer nobody chose. Every conflict is reported, inline <<<<<<< markers as well as .rej files, and aegis update --finish completes the job once you have resolved them: missing migrations, post-generation tasks, baseline advance, backup tag cleanup. Before you deploy, the update names the .env keys the new Settings no longer declares and the behavior changes this version range crosses, both of which are knowable at update time and invisible afterwards.
An N+1 gate on every database stack. Generated database projects get queryspy in the dev extra and a make check-queries target, inert on a plain pytest run. The framework's stack matrix runs its pytest step with --queryspy-strict against one baseline covering all 14 database stacks: a new finding fails, the known ones do not. make queryspy-baseline regenerates the fixture when a model change rewrites statement text.
Smaller things that bite less often but bite hard. /health/ is a constant-time liveness probe with component status behind a cached /health/detailed, dropping idle CPU on a full stack from 13.4% to 0.26%. aegis deploy-exec runs one command against a deployment without hand-writing the ssh and compose-file chain. A provider is described once in a registry instead of nine times across four files, and OpenRouter is one of them.
Changed
- Revisions are derived from the models at
initandadd; a documents-only stack ships a migration at last, because the gate is now the function that decides which services need one. aegis updatederivesollama_modefrom the URL the project talks to and the finance provider flags from the settings it renders, and computes the at-risk answer set fromcopier.ymlhistory instead of a hand-kept list.- A conflicted update records its target and can be completed with
aegis update --finish. uv sync --upgraderuns on the update path, orphan scheduler jobs are exported before the sweep deletes them, andAEGIS_STACK_TAGdefaults to the project slug instead of a sharedaegis-stack:latest.- The scheduler proves liveness with a heartbeat file rather than an HTTP probe;
system/health.pyis split by component, 1456 lines to 714. - A scaffolded plugin pins the aegis-stack that made it, and a plugin does not have to be on PyPI to be discovered.
Removed
- The
finance_importquestion, which was asked, stored, and never read by anything. The importers ship on the finance service alone, so deleting it changes no output.
Fixed
- A generated revision survives SQLite, where adding an index or a constraint rebuilds the table.
create_index(if_not_exists=True)is fatal in that path, and the rebuild refuses to proceed around an unnamed constraint, so a 0.10.1 project with auth and ai could not update at all. - The conflict report no longer tells you to "keep the right side", which is the template render and deletes your own change in every case the advice is printed for. Markers are labelled
your projectandnew template. LOGFIRE_TOKENin.envreacheslogfire.configure(). The guard read it through pydantic-settings, logfire reados.environ, and the app stopped importing outside a container.--to-version HEADstamps a parseable version, so the compatibility gate is not silently disabled for the rest of the project's life, and a run that used--template-pathsays the project is now pinned to a local path.- A pending resume no longer swallows
--to-version,--template-pathor--dry-run; a dry run with pending state used to run post-gen and delete the backup tag. - Updates deliver migrations the project predates, and a failed
alembic upgradeis no longer reported as success. - An update no longer plants an empty file for every gated-off template that changed.
- A merged
pyproject.tomlthat no longer parses is a conflict, instead of silently breaking every Python merge that follows it. - Startup re-adoption actually stamps: the inspector was used after its session closed and every failure was swallowed.
- Every async session runs with foreign keys on, and SQLite transactions open
BEGIN IMMEDIATEso a second writer queues instead of failing. STORAGE_ROOTreaches every container; four services replaced the anchor's environment list wholesale.- A Postgres project's test suite runs.
Upgrading
uvx aegis-stack@0.12.0 update
Installation
pip install aegis-stack==0.12.0Or run directly:
uvx aegis-stack init my-projectLinks
What's Changed
- Plugin scaffold pins its aegis-stack; document the source-only path by @lbedner in #1092
- CLAUDE.md: name the one async CLI exemplar by @lbedner in #1093
- Adopt queryspy: N+1 gate on the stack matrix, three query fixes by @lbedner in #1095
- Three bugs from a real ledger, and a payee named once stays named by @lbedner in #1094
- Refresh the query baseline after #1094; make regeneration one command by @lbedner in #1097
- STORAGE_ROOT reaches every container by @lbedner in #1086
- Add deploy-exec: run a one-off command on a deployment, scriptably by @lbedner in #1098
- Generate the documents migration at init; one source of truth for the gate by @lbedner in #1100
- A provider is described once, and OpenRouter is one of them by @lbedner in #1099
- Add a small, injectable GraphQL client to the template by @lbedner in #1102
- A bill's amount describes rows that were never its payments by @lbedner in #1103
- Health checks landed; Postgres-project tests run; slow suites run nightly by @lbedner in #1105
- update: gated-off stubs, predated migrations, and honest failure reporting by @lbedner in #1107
- Models as the source of schema truth: oracles, and the drift ported by @lbedner in #1108
- Update Path Hardening: operator notices + conflict handling (#1020, #1029, #1016, #1018) by @lbedner in #1111
- Derive alembic revisions from the models at init and add by @lbedner in #1112
- Update Path Hardening: re-lock, orphan export, image tag, sentinel owner (#1019, #1026, #1027, #1110) by @lbedner in #1113
- Four fixes found downstream, each generic by @lbedner in #1114
- update: merge ruff config last, flag a broken pyproject as a conflict by @lbedner in #1115
- Revisions sign themselves; the table declarations are gone by @lbedner in #1116
- update: close the six hardening gaps found updating aegis-pulse by @lbedner in #1124
- update: derive the answers it would otherwise invent, and keep it that way by @lbedner in #1126
- update: compute which answers are at risk instead of listing them by @lbedner in #1127
- Delete finance_import: a question nothing ever read by @lbedner in #1128
- Startup recovery and deploy sync: stop losing what was removed by @lbedner in #1129
- update: a pending resume no longer swallows the flags you just typed by @lbedner in #1133
- update: stop telling users to delete their own code, and stamp a real version by @lbedner in #1137
- Release 0.12.0rc1 by @lbedner in #1138
- kit: a strip that can count, a dialog title, and money with one home by @lbedner in #1139
- update: a stale default is not a removed key, and must not read like one by @lbedner in #1140
- Upgrade coverage is a matrix: every stack shape, from every release by @lbedner in #1141
- Release 0.12.0rc2 by @lbedner in #1142
- A generated revision has to survive SQLite, where a table is rebuilt by @lbedner in #1143
- Release 0.12.0rc3 by @lbedner in #1144
- Release 0.12.0 by @lbedner in #1145
Full Changelog: v0.11.1...v0.12.0