Changelog
What changed in each release, for the people who install multivac. The
reasoning behind each decision lives in .multivac/changes/archive/ — that is
the brain’s own ledger, written for the people changing the tool.
Every entry names the invariant it made true, because a rule quoted without its ID does not bind.
This file is the only copy. The documentation site mounts it rather than keeping a second one (MV-78).
0.15.0 — 2026-10-02
multivac keeps no code graph: a brain that declares grapher: still loads,
and doors takes back what multivac itself wrote, but nothing builds,
refreshes, gates, commits or names a graph any more, and graphify’s or
codegraph’s files stay until you remove them. Three more behaviours changed in
a way that can newly refuse what used to pass: a brain that was loading, an
opsx brain’s slugs and archives, and a change close whose claims cite no
stated row. And change new now writes a claim an older multivac cannot read.
Read the first four items before upgrading.
Changed — read before upgrading
- Graphers are gone (MV-153). multivac no longer declares, installs, builds,
refreshes, gates, commits or names a code graph, and no longer renders
.multivac/ecosystem.json.- An old config loads.
grapher,grapher_auto,graphersandrepos.<key>.grapherare read only to say they are ignored:verifyanddoctorprint one line naming them, whatever they hold. Deleting them is a config edit, so it needs an open change (MV-97). init --grapherandchange close --no-grapherare unknown flags, refused with exit 2.doorstakes back what multivac wrote: every post-edit refresh hook it put in.claude/settings.json(and nothing else there), and.multivac/ecosystem.json. Commit the removal.- What the vendors wrote stays until you remove it.
doctornames, per repo it may write in, graphify’sgraphify-out/,.graphifyignore, skills, door section and hooks, and codegraph’s.codegraph/andcodegraph.json, each with the vendor’s own removal; and the door warns, where graphify’s skill or hooks remain, that its graph is no longer refreshed. Those paths are not code, so the removal commits on any branch. graphify’shook-guardkeeps telling agents to ask a graph that only goes staler until you run its uninstall. - Doors are shorter: 747–1,156 bytes on a consumer door, 1,308–2,276 on a brain door, and each edit runs one hook where it ran two.
- What you lose: one-call transitive reach (
codegraph impact,graphify path); one-hop questions cost the same or less by grep. - Fourteen rows retired — twelve that were law and two never enacted: MV-50, MV-52, MV-58, MV-59, MV-61, MV-62, MV-90, MV-103, MV-131, MV-134, MV-139, MV-140, MV-148 and MV-149 (the last two proposed). Thirty-four rows amended.
- An old config loads.
- The SDD lives in the brain alone (MV-146). A top-level
sdd:used to reach every declared repo:repos syncinstalled the vendor into each code repo (spec-kit: 30 files, about 240 KB), each code repo’s door carried the SDD’s step block into every session,change planrefused until every code repo had written its own constitution, and the firstapplycommitted the vendor’s files onto each code repo’s branch — while the specs of every change were written in the brain. The SDD is now installed, gated and printed in the brain only. A code repo’s door carries one line saying where the brain’s SDD runs and where that repo’s code belongs.- A config can now refuse to load. A tool named in
repos.<key>.sdd, a top-levelsdd:the brain’s own entry contradicts, and an SDD name multivac does not know make every command in the brain exit 2, naming the key and the fix — exceptdoctor,doorsandinit, which report it and exit 1, as they do for any config they cannot load.repos.<key>.sddtakes onlynone, which exempts that repo’s code from the change gate. The config edit itself needs an open change (MV-97). A consumer whose mounted brain carries such a config is not refused:verifyandcountprint the refusal as one line, which gates only underverify --strict. - An SDD declared only on the brain==code entry now governs its siblings'
code. The code gate asks which SDD governs a repo’s code — the brain’s,
unless the repo says
sdd: none— so a sibling that resolved no SDD of its own, and so was never gated, is gated now (MV-137). - Code repos equipped by an earlier release keep their install, and the
carry committed
.specify/**on their change branches. Nothing removes it for you: while the brain declares an SDD,doctornames it as a leftover with its removal,repos checkappends it to that repo’s line, and neither fails over it. For spec-kit,git rm -r .specifyandspecify integration uninstall <key>for its skills; for OpenSpec, deleteopenspec/and theopenspec-*skills andopsxcommands its init wrote. Every known SDD’s install paths are not code in any repo, so the removal commits on any branch. - A change open across the upgrade whose specs sit in a code repo is
gated in the brain alone:
planandapplyname the file they found in the code repo and do not read it, andcloserefuses rather than pass over a task list it cannot read. Move the slug’s directory into the brain, or pass--no-sdd. - An older multivac keeps cascading the SDD against this config. Set
requires:to the release carrying this change to have it say so (MV-86).
- A config can now refuse to load. A tool named in
- opsx runs through openspec’s own CLI (MV-147). Its steps printed chat
commands spelled as only two harnesses spell them, and the scaffold installed
a command body per workflow per door — 19 files, about 270 KB, for
doors: [agents, claude]— whose propose body told the agent to stop after planning and whose archive body merged by hand. The steps are now openspec’s own terminal verbs, which every harness runs alike:openspec new change <slug> --jsonand thestatus/instructionsloop atnewandplan,openspec instructions apply --change <slug> --jsonatapply, andopenspec archive <slug> --jsonafter the merge. They need openspec 1.5.0 or later, the scaffold 1.7.0.- No command body is installed. The scaffold is
openspec init --tools none --no-animation ., which writesopenspec/config.yamland two.gitkeeps whatever the doors. In a brain scaffolded earlier,doctornames the bodies that init left, with thegit rm -rthat removes them; that removal is not code, so it commits with no change open. - The archive’s question goes to you. The printed archive carries no
--yes,--skip-specsor--no-validate, so on a change with spec deltas openspec answersarchive_confirmation_requiredand writes nothing; the guide printed under the step has the agent show you the deltas and offer openspec’s own answers. Each step’s line names the question the removed bodies asked on it, and a guide under it at its point carries the rest, never in the door. - A slug openspec refuses is refused first.
change new(exit 1) androadmap add(exit 2) refuse a slug outside openspec’s grammar — lowercase letters and digits in runs joined by single hyphens — or the reservedarchive, whateversdd_autoand--no-sddsay.Fix_Authused to open a change whose first printed command failed. - A land step’s proof is read in the brain checkout alone. An archive run
in the change’s worktree after the merge used to pass
change closeand never reach the brain; it is now refused by name. - Close stages a merged main spec only when it carries the merge. A spec
under
openspec/specs/is staged when it holds, whole and under the same name, every ADDED and MODIFIED requirement block of the archived delta, both read as openspec reads them, fenced examples included; one that does not — after an archive made without merging, or a draft of yours — is named dirty and left out of close’s commit. Onlyspec.mdis merged, so a file kept beside a delta never stages yours. - The ledger’s reason is true under
--json:--yesarchives over the tool’s own refusal, and under--jsonsays nothing.change applyprints openspec’s “Archive would refuse this delta” note from a passing validator, and still passes. - What the agent’s own openspec calls send and write is disclosed by
version. The entry’s opt-outs reach only the two commands multivac runs,
the validator and the scaffold; the calls the agent makes follow the
agent’s own environment, where
OPENSPEC_TELEMETRY=0orDO_NOT_TRACK=1stops them sending.
- No command body is installed. The scaffold is
- A claim is its row’s ID, and close checks what it cites (MV-150).
change newand the scaffold now writeclaims: [<ID>]; the row states the rule. A legacy{ id, statement }keeps working and is written back unchanged — do not convert a change in flight.- An older multivac reads an ID-only change as absent: its
changecommands refuse the file, and itsverify,roadmapanddoorsskip it without a word, so the change’s claims stop pending and a brain==code hook running it blocks the change’s own commits. Upgrade every consumer’s pinned and global multivac before the brain writes one, and setrequires:in the brain’s.multivac/config.ymlto the release carrying this change (MV-86), with or after that release. change closerefuses a claim that cites no stated row the change adds, touches or retires, a claim anchored only in its change file, an added row already in the law, and a proposed row it anchors but never claims — all in one run, before anything is written.--abandonrefuses a change whose own proposed row states a rule.verify’s finished line,landandplansay what close will refuse, and a row the brain’s channel states and this checkout lacks is named as a pull.- A key inside a claim other than
idandstatementis refused by every command that rewrites the change file, and named by every other reader. - Close’s reservation release reads the anchors
verifyparses; a fresh claude-door brain’s first unused reservation is now released.
- An older multivac reads an ID-only change as absent: its
Fixed
A run reads the checkout that holds where it was asked (MV-151).
verifytook its starting directory for the root. From a subdirectory of a brain or a consumer it exited 2 advisingmultivac init ., which would git-init a second brain there; from a brain change worktree’s subdirectory it judged the main checkout’s law and passed a row the worktree added; a monorepo subproject holding its own mount lost its verdict below its own directory, and a brain that declaresmount: docs/brainwas never found.verifynow resolves the root first — the nearest.multivac/config.ymlup to the git toplevel, asked with the ambientGIT_*dropped, else the consumer its change worktree or mount names, else the stale pin or the door — and a full report printed away from that root names it in onerootline.count,doctor,doorsandroadmaproot the same way, sodoctorno longer reports “config invalid” fromsrc/androadmapno longer lists “empty” there.- A brain change worktree reads the change’s own siblings. Each sibling is
read in the change’s own worktree for it, else where the main checkout
reads it; one missing both ways names the main checkout for
repos sync, which typed in the worktree cloned the ecosystem into.multivac/worktrees/.doctorreads them the same way. - A mount is judged as the brain it is. Every brain gate holds inside a
consumer’s mount; a sibling missing beside it names the host once and never
advises
repos sync, which typed there clones the ecosystem into the consumer. - Nothing is advised about another directory. Outside a work tree nothing
is walked; below a toplevel no brain governs the run says so instead of
advising
init; a submodule of a governed repository names it; a git refusal such as dubious ownership is quoted, exit 2, not read as “no repository”; and every command refused below a brain names that brain:no .multivac/config.yml in <dir> — it is inside the brain at <brain>; run this there. - The version floor reaches every run that reads the brain. The notice
reads the brain of the root the command reads, so a
requires:floor is heard from a subdirectory and from a consumer; a consumer hears only the floor, since the record’s fix,doors --adopt, runs in the brain.
- A brain change worktree reads the change’s own siblings. Each sibling is
read in the change’s own worktree for it, else where the main checkout
reads it; one missing both ways names the main checkout for
change closelands the brain’s specs whatever the flags say (MV-146, amending MV-144). Under--no-sddorsdd_auto: falsethe spec directory was left untracked; afteropenspec archivethe moved-fromopenspec/changes/<slug>/and the mergedopenspec/specs/<cap>/were left out of the commit and named dirty;--abandondropped the specs entirely. Close and abandon now stage the slug’s directories, deletions included, and the main specs an archive merged into.A slug inside a parent directory’s name no longer owns the parent (MV-146, amending MV-144). Slug
spectookspecs/and slugchangetookopenspec/changes/, so the carry andclosestaged every change’s directory. Close stages the file an archive merged into, never its capability’s whole directory, so an unrelated edit there stays named dirty.Two open changes no longer share spec-kit’s feature pointer (MV-146).
/speckit.planrun for one change wrote into the other’s directory, whose gate then passed.change planandchange applypoint.specify/feature.jsonat the slug’s directory and say so when it named another.A brain==code entry keyed other than
braingates its close (MV-146). Its proofs were looked for in a worktree named after the root’s scope, never its key, so a task list carried onto the branch was never found andclosepassed over an open task.A code repo’s
specs/is code (MV-146). The SDD’s artifact directories were not code in every repo that resolved it; they are not code in the brain alone now. Every known SDD’s vendor state stays not code everywhere.Under
sdd_auto: false, nothing claims a gate (MV-146). The door, flow.md anddoctorsaid the lifecycle refuses without the step artifacts, which it does not do with the automation off.A consumer door names the law at the path that repo can open (MV-143). It printed
.multivac/invariants.mdin repos where the law lives under the mounted brain.What the SDD writes in the brain lands with the change (MV-144). The commit
change closeprints staged the change file, the law, the graphs and the governance graph, and left the spec, the plan and the task list untracked — the very artifacts every gate in the lifecycle had demanded. It now also stages what the brain’s own status reports under the directories the closing slug owns, deletions included, so an archive that moves a directory lands in one commit. A dirty file the change did not write is named, never staged.change applyequips a repo it just made before carrying its artifacts (MV-144). The vendor’s init ran over a checkout whose files the carry had just taken out of it, and wrote a second copy. Cloning or creating, equipping, then branching and carrying are now three steps in that order.A fresh brain’s first commit passes its own gate whatever it declares (MV-142). The directories that are not code came from the doors multivac projects, so a brain declaring openspec and no grapher was refused over
.agents/**, a directory openspec’s own init had just been told to create. They now come from the declared scaffold’s integrations, each recording what it writes, measured per tool version.
Changed
One line when nothing is off (MV-151).
verify --quiet, orMULTIVAC_QUIET=1, prints one line — the summary first, then the header, each plain read with its ref, sha and age, theenactanswer and the change the code lands in — and the whole report, byte for byte and both streams in their order, the moment anything is off: a leg that is notok, a staged law, anyconfigline, a warning, an unparsable open change file, an anchor naming no row. A read that is not plain and a pin that does not gate print beneath the line. The git hooksdoorswrites exportMULTIVAC_QUIET=1, so a clean commit says one line; re-rundoorsto regenerate them. A binary older than this ignores the variable and prints in full. The claude harness hooks keep their commands byte for byte:verifyreads the harness’s own hook payload, so a session start is quiet and a post-edit run follows the edited file into the checkout that governs it — an edit in a change worktree from a session at the main checkout is now judged there. Those two harness behaviours were read from the harness binary with a simulated payload and await a live session.Cursor reads
AGENTS.md, so multivac projects nothing else for it (MV-143). The.cursor/rules/multivac.mdcstub was a second door that could disagree with the canonical one. Onedoorsrun removes multivac’s block from it and deletes the file when nothing but multivac’s own frontmatter is left; lines you added there survive, and the file with them stays.Fresh spec-kit installs get skeleton templates (MV-146). When the lifecycle’s scaffold installs spec-kit in the brain, multivac writes three short skeletons to
.specify/templates/overrides/, which every spec-kit template resolver reads first, so the specify, plan and tasks steps read about 4 KB of template per change instead of about 18 KB. Only into an absent directory, only from spec-kit 0.9.4 on, only where the installed core template still carries every heading the skeleton keeps, and never again after that run.doctornames an enabled preset those files outrank, with the override to delete. An existing install gets none.The constitution’s revisit says to commit no Sync Impact Report (MV-146). It said to prepend one, which spec-kit itself stopped saying at 1.0.6: from there the report is review scratch, removed before the amended constitution is committed. The amendment record is git and the change that amended it.
The instruction to run the chain through is printed once per lifecycle point (MV-146, amending MV-95), after the last step, instead of after every step. The brain door ends each step with its proof path or
[ungateable]and leaves the reason to the lifecycle,doctorand flow.md.The change body cites its spec (MV-146).
change newsays the why, the design and the tasks go into the SDD’s files.change closeappends one line,Specified in `<dir>/` (<sdd>)., unless the body already names the directory; nothing else in a body is written.change plansays where code goes (MV-146). For a change naming a code repo, it says the steps run from the brain checkout and code is written only in the change’s worktrees.The skill carries what no command prints (MV-152). Its body states the rhythm, where you are, seven rules and where to read what; each reference keeps the judgement no command prints and names what prints the rest — the brain door, the lifecycle’s lines and refusals,
verify’s lines,mvac help anchor,mvac help verify, the seed report. A change session loads 9,897 bytes of skill instead of 31,138, and the pack is 25,453 bytes instead of 56,542. A session in a code repo is told to write code in the worktreechange applyprinted and to runchange,roadmapandseedin the brain’s own checkout, never in the mount. The skill, the root documents and the pages no longer list two git shims of three, name a Cursor rules file as written, promise no install step to forget, sayclosecommits a code-less brain’s specs, or call the pre-commit hook the earlier and stricter check. Runmultivac doorsto re-project the skill.
0.14.1 — 2026-09-17
Fixed
- A change closed on its own branch passes
verify --range. Landing a change through one merge request puts its archive commit on the same branch, so at the head of the range the change file is archived, and 0.14.0 refused the whole request as code outside a change. A release is exactly that. The range reader now accepts a change archived at the head and not at the base. A change already archived at the base still admits no code. If you upgrade: a merge request refused this way passes once its pipeline runs 0.14.1. (MV-142) - A fresh brain’s first commit passes the code-in-change check. In a brain
initialised with an SDD and graphify, the commit
inittells you to make was refused over.gitignoreand.agents/..gitignore, and the harness directories that spec-kit, OpenSpec and graphify install into (.claude/,.agents/,.cursor/and the rest), are no longer code. In.github/, only the door file is exempt; workflows are still code. (MV-142)
Documentation
- The merge request job in the code-in-change reference now covers a branch that closes its own change. multivac’s own pipeline runs that job. (MV-142)
0.14.0 — 2026-09-16
Added
multivac repos check. It reports, per declared repo and offline, whether the path is the declared clone, and whether the repo’s declared SDD and graph are installed and committed, its project document written, and its mount in place. It exits 1 when any repo is not. It runs git and reads files, and never a vendor tool, so CI can runrepos sync --shallow && repos checkwith no tool installed. (MV-132)- Code reaches a repo only through a change. Where a repo declares an SDD
with automation on,
verifyrefuses a commit or a local merge that changes code outside the branch of an open change declaring that repo. multivac’s own files, the doors, and the SDD’s and grapher’s files are not code. A newpre-merge-commithook runs the same check on a local merge, andverify --strict --range <base>..<head> --branch <name>runs it over a merge request in CI, catching a commit made with--no-verify.doctorsays it binds only where the forge requires that pipeline and protects the default branch.--no-sddis recorded in the change file assdd_skipped. If you upgrade with an SDD declared: runmultivac doorsto install the new hook, then commit code on a change’s branch, not onmain. A release is a change too:CHANGELOG.mdandpackage.jsoncount as code. (MV-137) .multivac/ecosystem.json. The brain keeps a graph of its own declarations: repos, law rows (without their text), anchors and changes, and how they link. Nothing machine-dependent goes in it, so the same declarations render the same bytes.init,doorsand every lifecycle commit in the brain write it;verifysays when it is stale, without gating. graphify reads it with--graph .multivac/ecosystem.json, and the doors name it. (MV-139, MV-141)
Changed
repos syncand the lifecycle install what they declare.repos syncruns each repo’s SDD init and first graph build, and exits 1 naming a tool it would run and cannot find.change planandchange applydo the same after they clone or create a repo, andchange newrefuses a missing SDD before writing anything. If you upgrade: arepos syncin CI where a declared tool is not installed exited 0 and now exits 1; userepos sync --shallow, which writes nothing. (MV-129)- The SDD’s init follows your doors. spec-kit is initialised with the
integration for each declared harness door (
specify integration installfor the rest), and OpenSpec withopenspec init --toolsfor all of them. graphify’s own project install runs for each declared door, and an absolute path it writes into a hook is rewritten to the baregraphify. (MV-130, MV-131) change applycarries the SDD files onto the change branch. Files your SDD wrote beforeapply, such asspecs/<n>-<slug>/or an uncommitted.specify/, are copied into the worktree, committed there and removed from the checkout, so the merge no longer stops on an untracked copy. A modified tracked file, or an ignored one, is refused before anything moves. (MV-133)change landcommits the graph on the change branch. Before its push line,landrefreshes each ready repo’s shared graph in the change’s checkout and commits it there.change closerefreshes before it prints the archive commit and puts a changed brain graph in it. Building, installing, gating and refreshing graphs during a change now covers only the brain and the repos the change names;repos syncstill covers every repo. (MV-134)- A project document is judged against its tool’s own template. A
constitution is still the template when it matches the sha256 spec-kit
recorded, or still carries one of the template’s own tokens outside an HTML
comment; the refusal names the token. A written document that cites
[1]or[API]no longer fails.change newasks for an unwritten document, saying the principles come from you. OpenSpec’scontext:inopenspec/config.yamlis reported, never gated. The door says an active row outranks a project document. (MV-135) - Session zero, in order.
initends with the whole of session zero: declarerepos:before the first commit, sync, both flows with the one that fits marked, the project document, then the law,doorsandverify.seedreports each repo’s graph and project document. The skill runs sync, seed, the open questions, the project document, the law and then doors. (MV-136) verifyin a consumer’s change worktree takes the brain, the change and the repo from the worktree’s path, instead of exiting 2 over an empty mount. (MV-138)- The graph’s promises match what happens. The door says “refreshed after
your edits” only where a declared harness has a post-edit hook. That refresh
now runs in the repository of the file you edited, when it holds a graph.
flow.mdanddoctorsay that asking the graph cannot be checked. Where graphify’s own install covers a declared door, the door points to graphify’s section instead of repeating its verbs. (MV-140) reposanddoctorreport clone state. A repo iscloned,missingorinvalid(a plain directory, a directory inside another repository, a repository with no commit, or a different remote), notpresentbecause its path exists. If you upgrade: a script that parsedpresentfrommultivac reposordoctorneedscloned. (MV-141)- The documentation describes this flow end to end. (MV-141)
0.13.0 — 2026-09-16
Changed
initinstalls the SDD and grapher it declares.init --sdd speckit --grapher graphifyused to write both names into the config and the door and install neither: a fresh repo got no.specify/and nographify-out/.initnow runs the tool’s own init and the graph’s first build in the brain, the same waychangedoes, and runs neither where the tool is already installed. (MV-128)initrefuses a tool it would run and cannot find, before writing anything. It exits 1, beforegit init, naming the binary, the install line and the vendor’s repository. Nothing is required for a tool it would not run: one already installed,opsx, which has no init on record, or an SDD undersdd_auto: false. If you upgrade: a script or CI job that runsinit --sdd speckitorinit --grapher graphifywhere the tool is not installed exited 0 and now exits 1. (MV-128)- A graph’s first build adds its ignore lines first. For graphify that is
.graphifyignore, keeping.claude/,.multivac/,.specify/,specs/andopenspec/out of the graph, andgraphify-out/*with!graphify-out/graph.jsonin.gitignore. Missing lines are appended and yours are left alone; a rule of yours that still ignoresgraph.jsonis named. This applies wherever a first build runs,initorchange. Measured on graphify 0.9.29, a fresh brain’s first graph went from 223 KB, most of it spec-kit’s own files, to under 3 KB. (MV-128) - Step 0 of
initcommits only whatinitwrote. It printedgit add -A, which in a repo with code also committed your uncommitted work as “multivac init”. It now lists the pathsinitcreated or changed, leaving out the tools’ per-checkout outputs. (MV-128)
0.12.0 — 2026-09-16
Changed
repos syncmounts the brain in every repo it gates.doorsinstalls hooks that read the law through the brain mount, and until now nothing made that mount: it was a manualgit submodule addin each repo.repos syncnow checks the mount in every declared repo on disk, on every run. A repo with no mount gets the brain added as a submodule, and a directory that already holds a copy of the brain is adopted without downloading it. An empty mount is filled. A mount that holds something other than a brain, or records a different url, is reported and left alone. The mount is left staged: multivac never commits in your repos. A read-only repo is skipped, and a mount git refuses is quoted by its cause and exits 1 without stopping the rest.doctor,doorsand the consumer door now namemultivac repos syncas the fix;doctorsays when a mount is staged but not committed, anddoorslists the repos it gave hooks with no mount to read. (MV-127)brain_urlis a new config key: the address everyone clones the brain from.repos syncwrites it into each repo’s.gitmodules, so multivac never takes it from yourorigin, which may be an ssh alias only your machine knows.initwrites it commented out, with the origin it found as a suggestion. Without it,repos syncmakes no mount, and names the key once when a repo needs one. If you upgrade: addbrain_url, then runrepos sync; it stages a mount in every writable repo on disk that lacks one, for you to review and commit there. multivac 0.10.0 and 0.11.0 refuse a config that uses the key, and 0.9.0 and older ignore it. (MV-127)verifyno longer locks the commits of a repo whose brain it cannot reach. In a repo where multivac installed hooks and no brain is mounted, it exited 2 on every commit and advisedmultivac init ., which would create a second brain. It now warns that nothing was verified, namesmultivac repos sync, and exits 0, as the hooks already do on a machine with no multivac. It recognises the hooks by the header line multivac writes, including hooks it installed into another tool’s hooks directory. A repo without those hooks still exits 2, as does a stale.brainmount. If you upgrade: repos locked this way commit again as soon as the multivac their hooks run is 0.12.0; nothing needs re-projecting. (MV-127)- The documentation site names no law IDs. Its pages explain behaviour in plain words; IDs stay in the law table, the brain’s records and this changelog. The pages changed in 0.11.0, and this release makes it a rule. (MV-126)
0.11.0 — 2026-09-14
Changed
- A repo you do not own is read, never written.
repos.<key>.managed: falseis a new key, and a clone git reports shallow is treated the same way: multivac scaffolds, builds, refreshes and projects nothing there, and no gate looks for a file there;repos syncstill clones and fetches it.doors,doctorandreposmark it read-only, andrepos sync --shallowsays so as it clones.change planandchange applyrefuse a change that names one, with exit 1, before cloning, branching or bumping anything.managed:must be a boolean, and the brain’s own entry cannot sayfalse. If you upgrade: a shallow clone you already have is read-only now (git fetch --unshallowmakes it writable), a spec kept in one no longer proves a step, and a door or hook projected there earlier stays. multivac 0.10.0 refuses a config that uses the key, and 0.9.0 and older ignore it and write there. (MV-125) sdd:andgrapher:resolve the same way everywhere, andnonemeans none for both. A repo’s own value wins, then the top-level one, and the brain’s ownrepos:entry counts, so agrapher:there is honoured. If you upgrade: a repo’s ownsdd:now gateschange plan,applyandclosewith no top-levelsdd:, where they passed silently. Each adapter is judged in the repos that resolve to it and all must pass, including repos the change does not name (--no-sddskips one run); one whose repos are all uncloned refuses untilrepos sync. A spec in ansdd: nonerepo no longer proves a step,grapher: noneis no longer reported as unverified, andgraphers.noneis refused. (MV-122)- A vendor counts as initialised only when its own state file says so.
spec-kit needs
.specify/integration.jsonwithintegration_state_schema1 and a non-emptyinstalled_integrations, OpenSpecopenspec/config.yamlor.yml, graphify agraph.jsonthat parses as JSON, codegraph.codegraph/codegraph.db. A hand-made.specifyor a 0-byte graph no longer counts.doctorreportsinstalled,missing,partialorunevaluable, with the reason, where it saidartifact ok. A partial graph is rebuilt, andchange closerefuses while it stays partial; the SDD init is never run over a partial install, because a re-run can revert edited files. If you upgrade: a.specifywith no validintegration.json(spec-kit before 0.16.4 was never measured), or anopenspec/with no config file, reads partial and is warned about, not refused; commit before you run the vendor’s init there yourself. (MV-124) change closewants a shared graph committed, and never codegraph’s. If you upgrade: agraph.jsonthat is staged but not in HEAD no longer passes; the refusal names the add and the commit, anddoctorsaysNOT COMMITTEDwhere it saidUNTRACKED. codegraph’s database is local: built in each checkout, never asked for in HEAD, and the door says not to commit it. (MV-124)init --grapheris checked. A name that is neither a verified grapher nor declared undergraphers:in a readable config already at the target exits 2, listing the known names, before anything is created.--grapher noneis refused too: leave the flag out. (MV-122)- Vendor opt-outs are set on every run multivac makes. OpenSpec gets
DO_NOT_TRACK=1 OPENSPEC_TELEMETRY=0and codegraphDO_NOT_TRACK=1 CODEGRAPH_TELEMETRY=0 CODEGRAPH_NO_DOWNLOAD=1, in the scaffold, the validator, the build, the refresh and the post-edit hook; OpenSpec 1.13.0 otherwise sends telemetry from every command, the gates’openspec validateincluded. They override your shell, so aDO_NOT_TRACK=0you export does not reach them, and a codegraph missing its platform bundle is told not to download it; a run you type yourself is still yours. Re-rundoorsso Claude Code’s post-edit hook exports codegraph’s and reachesnode_modules/.bin, and so the projected skill describes this release’s gates. (MV-121, MV-123, MV-124)
Fixed
- An adapter’s binary is found the same way everywhere.
doctor,doorsandchange close’s graph checks looked on PATH alone, so a tool installed withnpm i -Dread as missing. Every surface now looks on PATH, then in that repo’snode_modules/.bin, and the post-edit hook reaches it too. On Windows it tries PATHEXT’s extensions, untested there. A missing binary is named per repo with its adapter, install line and vendor repository, where it saidis not on PATH. (MV-123) - The spec-kit scaffold no longer needs the
claudeCLI. It passes--ignore-agent-tools; without it and withoutclaude, spec-kit 1.0.6 exits 1 and writes nothing. (MV-123) - A failed vendor command is quoted by its cause. The scaffold, the validator and the refresh quoted the first three lines, which were spec-kit’s logo or a Python traceback header. They drop banner lines now and quote a traceback’s closing exception, else the lines naming an error, a refusal, a denial or something not found, else the last lines, at most three. (MV-123)
doctorno longer says the SDD init reaches the network. With the network denied, spec-kit 1.0.6’s and OpenSpec 1.13.0’s inits exit 0. The init stays out ofverify,doctoranddoorsbecause it writes the vendor’s files into the tree. (MV-121)- 0.10.0’s changelog entry and two site pages are corrected. The entry now
names MV-105 to MV-119, the rows that release made law, and the install guide
and hooks reference give the runner order MV-92 set: this repository’s own
build, then the multivac it declares, then
mvacon PATH. (MV-120)
0.10.0 — 2026-08-24
Changed
- A red
verifynow reaches the agent in Claude Code, and after an edit the agent has to read it.doorsnow writesmvac verify 2>&1 || trueat session start, where nothing may block, andmvac verify >&2 || exit 2after an edit, where a red law, a config the edit broke and a missing binary all come back to the model. This is a forced read in the same turn, not a revert. Re-runningdoorsrewrites an existing baremvac verifyentry in place, and amvac verify --strictyou wrote stays yours. Only theclaudetarget carries a hook config, and a brain that never re-runsdoorskeeps the old, mute command. (MV-112) - A commit that deletes the law is refused. A row that is
activeat HEAD and absent from the index is now refused with exit 1, and so is an index that removes the law file. Retiring a row stays allowed, and aproposedrow may still disappear. Where the question cannot be answered — no HEAD, an unreadable index, a consumer checkout — the check says so and does not gate. (MV-107) Retired rows are covered too: deleting one is refused. (MV-117) - An unknown config key or
--sddname is refused by name. A key multivac does not know is refused, naming the key it is near when only case or separators differ and listing the known keys otherwise;--sdd=exits 2 instead of 1. If you run more than one version: from this release on, a key a newer multivac knows is refused by an older one. Andinit --grapheris still not checked: an unknown name is written to the config, anddoorsanddoctorreport it as not verified. (MV-114) - Exit codes keep the documented contract in four more commands.
seed,repos,repos syncandroadmap syncexited 1 on a config that will not load; they exit 2 now.doorsanddoctorremain the documented exceptions;init, which now refuses a broken config, exits 1 too, though the reference does not list it; and bareroadmapstill exits 0 on a broken config because it never reads one. Baredoctornow exits 1 when an anchor in the law does not parse, and prints alawline naming them. (MV-118, amending MV-85)
Fixed
--flag=valueis accepted again. A long flag written with=is matched by its name now. A declared valued flag whose value is missing, or is itself a flag, is refused naming the flag. Andchangeandcountread the same refusal as every other command, sochange land <slug> apino longer exits 0 having recorded nothing. (MV-105)- The commit gates read the commit being made,
git commit -aincluded. Under-athey saw fewer paths than the commit contained, which walked the enactment and config checks straight past the most common way to commit; under a pathspec commit they saw more. The index git hands the hook is honoured now, for the repository the hook runs in and no other. (MV-106) - multivac no longer takes “it is there” to mean “it is ours”.
doorswrote a stub door such as.github/copilot-instructions.mdwhole; it writes only its managed block there now, and frontmatter only when it creates the file. The commit hook’s first choice of runner — the repository’s own build — now requires the repository’spackage.jsonto name multivac. A hook that mentions multivac only on a comment line no longer reads as wired; a mention inside a quoted string on a real line still reads as wired, by design. multivac’s own shim beside husky is recognised and rewritten, sostrict_pre_pusharms there. Andinit .no longer downgrades a strict pre-push shim to a plain one. A hand-edit to a hook multivac wrote is still lost when the hook is regenerated. (MV-108) - Hooks and projections survive the environments they land in. In a linked
worktree the repository’s own lefthook or hand-written gate never ran; the
shim looks under
--git-common-dirnow. A declared grapher refresh ran through a shell after an edit but was split on spaces atchange close; both run it through a shell. One mangled managed file no longer ends a multi-repo run: that file gets a notice naming it, and a file with two marker pairs is refused rather than half updated.doctorreads a shim instead of checking that it exists. (MV-115) - A broken config no longer disarms a strict gate through
init, and a commentedrequires:floor is read.initnow refuses a config it cannot load, with exit 1, though it may already have rungit initin a directory that was not a repository. Andrequires: ">=0.4.0" # floor for CIwas ignored; a trailing comment no longer hides the floor, so a binary below it gets its notice, and a malformed floor with a comment is named in its own notice. Neither notice changes an exit code. (MV-114) - The anchor engine reads one way. A bare
[:digit:]outside a bracket expression is refused now — anabsentleg written that way was green forever. The dialect gate also refuses(?…), lazy quantifiers, backreferences,\0and alphabetic escapes with no ERE meaning. A line in a CRLF file is a line — a$-anchored or exact-line leg never matched there; a lone\ris still text.countreads the bytesverifyreads, so acount=Npinned from it agrees with the gate. (MV-109) - Self-heal stays inside the file kind a leg names, and a symlink is not file
text. A
presentleg whose pattern moved is healed by rewriting its glob, and a candidate must now share the include’s trailing extension; an include with no trailing extension, or ending in a brace group, keeps only the.multivac/fence. Tracked symlinks and gitlinks are listed by neither reader. A leg whose glob names only a symlink now matches no tracked file; point it at the link’s target. (MV-116) - An SDD proof names exactly one feature. The spec-kit proof was
specs/*<slug>*/spec.md. The artifact language has no wildcard now:<n>is one run of digits, so the proof isspecs/<n>-<slug>/spec.mdand an openspec archive is<n>-<n>-<n>-<slug>. When more than one directory proves one step, the gate refuses and names them. A directory of the right shape written by hand still proves the step. (MV-113) - The lifecycle commits what it wrote and reports what happened.
change land --landedcommits its status bump now.--abandonnames repos that already landed instead of printing nothing landed.change newrefuses a slug whose archive already exists.roadmap syncpassed--labeltogh issue edit, so every GitHub update failed; it passes--add-labelnow, and a failed update or close is printed as NOT updated or NOT closed, followed by the command that failed, and a failed update still prints the not found in the tracker line after it. A GitHub issue still accumulates status labels, andclose’s spec-kit ledger still passes when the artifact never existed. (MV-110) change closeloses nothing on the way to the archive. It verified claims against every anchor it could see, including anchors written inside the change file, then archived that file — which the parser never reads. It refuses now, naming the claim. Two branches closing the same slug could overwrite an archived record; the archive write itself refuses now. And a frontmatter key the lifecycle does not know, which its next rewrite of the file drops, is named whenever a command reads the change file, as a notice rather than a refusal.--abandonstill reports rather than refuses over landed repos, on purpose. (MV-117)- A law row whose statement contains a pipe is read correctly. The trailing columns are read from the end of the row now, and a row missing them claims nothing rather than borrowing a neighbour’s cell. (MV-119)
inittells you to commit what it wrote, and the skill stops teaching what the tool does not do. A fresh brain’s firstchange newis refused until the scaffold is committed;init’s closing steps now start with that commit. The shipped skill taught an arrow-edgelanding_orderthe parser refuses, told the interview to write into the brain door’s managed block, whichdoorsregenerates whole on every run, and saidchange applyre-projects doors, which it does not. All three are corrected. (MV-111) The site’s session-zero guide gave the same managed-block advice and is corrected too. (MV-118)- The brain door no longer implies the graph stays uncommitted. Its line for a declared grapher said “never staged or committed by multivac”; it now reads “refreshed after your edits; multivac never stages it, but you can track and commit it”. (MV-50)
0.9.0 — 2026-08-18
Changed
- The CLI is parsed by citty. Every command
declares what it takes once, as data; citty parses that declaration and the
refusal reads the same one, so adding a flag is one edit instead of two that
can drift. Nothing a user types behaves differently: same flags, same
positionals, same refusals, same exit codes, and the existing suite passes
unedited. The refusal is not delegated — measured, citty parses an
undeclared flag into a key nobody declared and hands it to the command, which
is the silence MV-85 exists to end, so the check still runs before the parser.
--helpstays this tool’s own. Third runtime dependency, named in the law and in the constitution before the package was added: citty is one package with zero dependencies of its own. (MV-104, amending MV-02 and MV-85)
0.8.0 — 2026-08-18
Added
- A declared grapher’s graph has to be in the repository, not just on disk.
change closerefuses while a declared, present root keeps its artifact untracked, naming each root, the path and thegit addthat ends it. An artifact matched by an ignore rule gets its own message, becausegit adddoes not fix that one. Existence was half the question: a graph that lives in one working tree passes the old gate and helps nobody who clones the repo, where the door still tells every agent to ask it. multivac stages nothing — the gate says, you commit — anddoctorreports the same state per root without gating on it. (MV-103, amending MV-90) multivac roadmap syncprojects the change files to a declared tracker.tracker: gitlabortracker: github, using that vendor’s own CLI so multivac holds no credentials and adds no dependency. One way, always: the change files are the source, and nothing the tracker says ever reaches them — closing an issue by hand closes nothing, and the next sync restores it. The identity is the issue number, recorded in the change file, so a title edit cannot duplicate anything. Only labels in multivac’s own namespace are written, and none it does not own is ever removed. An absent CLI refuses rather than reporting success, and it never runs fromverify,doctorordoors. One issue per change for now. (MV-99)- The ritual now arrives with candidates, all commented out.
initwrote it as a bare comment, and facing a blank page most people write nothing — so the closing step printed nothing forever. Candidates are drawn from what you declared and nothing is asserted on your behalf: a commented line never prints, so a fresh brain still prints nothing at close. Only things no check could decide are seeded; a declared grapher contributes none, because its work is automatic and already gated. (MV-98, amending MV-34) - Changing
.multivac/config.ymlnow needs an open change. It decides which repos exist, which adapters bind and which gates run — all as load-bearing as a law row, and all editable in a commit with no explanation and nobody noticing. A staged modification is refused while no change is open; creating one is free, because a brain has to start somewhere andinitis the only thing that writes it. Any open change satisfies it, including one opened for that edit. (MV-97) multivac doorsnow writes.multivac/flow.md— one page sorting what your declarations oblige into what happens unasked, what refuses, and what no tool can check. A fresh brain had no place answering “what did I just sign up for”: the law is dozens of rows, the ritual starts empty, and everything else scrolled past. Every row is rendered from the adapter registry and your config, so it cannot describe behaviour the tool does not have, and each unprovable step carries the adapter’s own reason verbatim. It cites no invariant identifier — those are per-brain, so a generated one would be wrong in every other ecosystem. Derived and rewritten whole; anything you write outside the markers survives; it binds nothing and says so. (MV-96)change applynow says what can be worked at once. Repos in one stage oflanding_orderhave no ordering dependency by the declaration’s own meaning, and apply hands back an isolated checkout for each — it knew both and said neither. The line carries its boundaries: never the same file twice at once, and never the law. And each printed SDD step now says to run the chain through rather than waiting to be asked, with the opt-out on the same line; stopping is for a question the tool itself raises, not for permission. Both are printed and never checked — no artifact proves either happened. (MV-95)change newandchange applynow say when the brain is behind. The pin each repo records for the mounted brain was already compared to its channel, offline, and reported byverify— but nothing said it at the moment work starts, which is the only moment “refresh before you start” is an instruction rather than a fact about the past. It reports and never refuses: offline a pin behind its channel means somebody landed work or nobody fetched, and refusing on the second reading would fail an ordinary morning.staleness: blockstill gates inverify, unchanged. (MV-94)- A door in a code repo now names the ecosystem it belongs to. It carried
the law and the mount and nothing else, while the brain’s door listed the
repos and every adapter — and the consumer door is the one most sessions start
from. It now carries the list of declared repos with the one you are in
marked,
brainnamed as the handle anchors use, the adapters that apply to that repo, and an optional one-lineroleper repo. The mount refresh is now the first instruction rather than the second of four bullets, and carries its reason: the pin stays where the last commit left it, so a present mount is not a current one. (MV-93, amending MV-61 and MV-87)
Fixed
- The door a fresh brain gets is the door the tool maintains.
initcarried its own copy of the brain door and the copy had drifted: the projected door gained the graph block and the ecosystem’s repo list, the copy gained neither. So the first file an agent reads — the only one it has before anybody runs a second command — never named the graph it tells you to ask before reading the tree. There is one rendering now:initwrites the bytesdoorswrites, and runningdoorsstraight afterinitchanges nothing. A config that will not load leaves the door alone rather than being written from nothing. (MV-102, amending MV-101 and MV-91) - The door no longer names an SDD the config does not declare. On a brain
whose config exists but declares no
sdd:,init --sdd speckitreported the flag as not in the config — correctly — and then wrote a door gating through speckit anyway, because the door resolved the flag as a fallback.doors, which reads the config alone, deleted that block on its next run: two commands, one repo, two different doors, with nothing on disk recording the choice or explaining the revert. The door now takes the config’s answer whenever a config exists, and a flag is the declaration exactly once — on the first run, which is what writes the config. The report, the refusal for a disagreeing flag and first-run behaviour are all unchanged. (MV-101, amending MV-91) - The commit hook now runs the multivac that governs the repository, not
whatever is installed on the machine. The shim tried
mvacon PATH first and the repository’s own build last; the order is now the exact inverse — this repository’s build, then the multivac it declares, then PATH. A global install a year behind was enforcing an older law table against a repo that pinned something else, silently. Nothing runnable still never blocks a commit, and the repository’s own gates still run first. (MV-92) pnpm testno longer runs tests whose sources are gone.tscdoes not delete output for a deleted source, so the compiled suite accumulated whatever any branch ever built: measured after a rebase, five failures from a file absent from that branch — and silently, a deleted test that keeps passing. The build clears its output before compiling, and a test asserts the property rather than the script, so it fails whatever the cause. (MV-92)- A pasted link to the site now renders as a card. The head served an empty
description, a whitespace
og:type, and noog:imageon any page — and a scraper that finds no image does not fall back to a favicon, it renders a bare URL. Documentation pages carriedtwitter:titleand noog:tags at all. Every page now carries a description and a 1200×630 card image, the Twitter card is the large format, androbots.txtnames the sitemap it already had. No template was written: the machinery existed and had never been given anything to render. (MV-100, amending MV-77) - And the rest of the discoverability surface. Sitemap entries now carry a
lastmodderived from git rather than from a date field nobody would maintain; the empty/categories/and/tags/pages are no longer generated or indexed;languageCodereplaces alocale:key Hugo never read; and the head gainsog:site_name,og:locale, the card’s dimensions and alt text, and JSON-LD saying what kind of thing each page is — a developer tool, not a shop — which the theme’s microdata could not express. Every added tag is one the theme does not emit, because a duplicate is two sources for one fact. - The site’s discoverability is now checked on every commit, not fixed once. The check walks the content tree recursively, so a section added later is covered without anybody remembering — a listed set would pass forever while the next section shipped without a description. It states its ceilings: it cannot judge whether a description is good, and cannot decide whether a given network renders the card.
0.7.0 — 2026-08-18
Fixed
- Re-running
initno longer writes a door that disagrees with the config it kept.init --sdd speckitfollowed byinit --sdd opsxleftsdd: speckitin.multivac/config.ymlwhileAGENTS.mdsaid the features gate through theopsxSDD, with nothing saying they disagreed — and the door is the first file an agent reads. The config is authoritative once it exists: a flag naming a different adapter is now refused, with both values named and both ways forward stated, and the refusal writes nothing at all. A flag that agrees is reported as redundant; a flag naming an adapter the config declares none of is reported with how to make it stick. Everything else about a re-run was already correct and is unchanged. (MV-91, amending MV-70)
Added
- A declared grapher now obliges a graph.
change closerefuses while any declared, present repo has no graph, naming every one of them in a single message with the command that builds one there. Declaringgrapher:used to oblige nothing — the tool ran where it could and every failure was a notice that kept going, so a change could close with four repos ungraphed and say nothing. The cost was invisible by design: the door tells agents to ask the graph before reading the tree, so a missing graph never failed, it degraded into agents grepping. A root whose binary is not on PATH refuses too, because a gate that cannot be evaluated must not pass. The gate asks whether a graph exists and never whether it is current.--no-grapherskips it for one run,grapher_auto: falseturns it off for good, and--abandonis exempt. (MV-90, amending MV-87) - The refresh at close reaches every declared repo, not only the ones the change named. A repo moved by another change, a merge or a sync was left describing a tree that was gone.
- The door projected into each declared repo now names the graph — the tool, its artifact and its own query verbs — resolved with the grapher that applies to that repo. Only the brain’s door carried this before, so an operator entering the ecosystem through a code repo got an agent that never learned a graph existed.
- A change can now exist before it starts.
plannedis a new change state in front ofopen, carried by the same.multivac/changes/<slug>.mdfile with the same schema, plus ahorizonofnow,nextorlater. That is the whole ordering model — no dates, no estimates, no rank, no dependencies between items. Keeping the roadmap in the change files rather than in a second list is the point: two lists describing the same work drift apart, and whichever one the tool does not read becomes fiction. (MV-89) multivac roadmaplists planned changes grouped by horizon, nearest first, and reports how many changes are actually in flight under its own label so intention is never read as progress.multivac roadmap add <slug> "<title>" [--horizon now|next|later]records one. It reserves no invariant id, opens no branch and creates no worktree.change newon a planned slug promotes the file that is already there — status flips toopen, the invariant id is reserved at that moment, and the body is carried across byte for byte, so the prose written when the idea was young survives into the change that implements it.change newon a slug nobody planned behaves exactly as before: the roadmap is never a precondition, and no command anywhere refuses an operation because its subject was not recorded first.plan,apply,landandcloserefuse a change that has not started, and namechange newas the step that comes first.
A roadmap of any length never delays a release: a planned change contributes no
pending claim and no landed repo, so verify --strict can never name it as
unclosed. Had it counted, the first entry recorded would have blocked every
release for as long as the roadmap was not empty.
0.6.0 — 2026-08-17
Packaging — read if you verify what you install
- This release and the ones after it carry no provenance attestation. npm
generates one by default under trusted publishing, and its registry verifies
a bundle only from a gitlab.com shared runner; this project’s shared-runner
minutes are exhausted and it publishes from a self-hosted one, so the release
stopped on that check with a
422after the tarball was already built and signed. Publishing is still trusted publishing over OIDC — the credential is short-lived and no long-lived token exists in the pipeline — but a tarball from here has no verifiable link back to the commit that built it. It comes back the day npm supports self-hosted runners, or the day this project runs on a shared runner again. (MV-88, amending MV-68)
Fixed
A declared adapter reached whichever repo answered first, and stopped there. Measured in an ecosystem of six: one sibling repo that somebody had run
specify initin by hand months earlier made the scaffold return before it touched anything — the brain included — because presence was asked of the whole repo list and answered by the first hit.doctorprintedspeckit: artifact okover five unequipped repos for the same reason, and the project-document check accepted that one repo’s constitution as the ecosystem’s, so the gate passed on a document five repos had never seen.One rule replaces the three reads: a root is the brain plus every declared repo present on disk, and one root’s artifact never answers for another’s. The scaffold runs the tool’s own init in every root that lacks it and stays silent in every root that has it, a root whose init fails is reported in the tool’s own words while the roots after it are still attempted,
doctorprints one line per root, and the project-document gate asks each root where the tool is installed and names every one that fails. (MV-87, amending MV-75 and MV-76)The mark showed the old colour in the terminal. The lamp in flight was still amber while the site had long since moved to the acid accent, and
lockup.svgstill carried the old hex — three copies of one identity disagreeing about its own colour. The accent is now defined once and gated twice: by the TTY check for ordinary output, by its own argument for the drawinginitprints. (MV-33)
Added
A declared repo that no change has touched gets its graph. The code graph was only ever built for repos a change explicitly touched, so a repo had to be worked on before it could be navigated — backwards for an agent that reads the graph in order to do the work. The lifecycle now builds it once in every declared, present repo that has none, using the adapter’s own
createwhere it declares one; refreshing an existing graph is unchanged. (MV-87)A repo can declare its own
sdd:, mirroring the per-repographer:the config already carried. The literalnonemeans that repo has no spec-driven flow: never scaffolded, never gated, never reported as lacking anything — out of scope, not a gap. (MV-87)initcloses on a call to action instead of a full stop. It used to end with “load the multivac skill”, leaving the reader to discover session zero — that there are two flows and which one is theirs — from the door. It now prints three numbered steps with the branch already decided: tracked source in the repo means discovery (multivac seed), an empty repo means the interview. On a terminal the scaffolding lines are dim and that closing line is acid; piped output andNO_COLORare byte-identical to before.
0.5.0 — 2026-08-17
Fixed
The site advertised a version nobody could install. It deploys on every merge to
mainand its badge was held equal topackage.json— but both of those sit at HEAD, so the pair agreed with each other while neither knew what the registry serves. The release sequence is bump → merge → the site deploys → tag → publish, so every release advertised an unpublished version for as long as that took; and a release abandoned after the bump merged would have advertised it forever, with the rule calling it correct.The site now states no version at all: the badge renders a parameter the release pipeline sets from the last git tag, which is a published version because publishing refuses a tag that disagrees with the manifest. What is not written cannot drift.
Deployment is unchanged — site-only corrections still reach readers on merge, without a release being cut — and the pipeline’s stages were reordered so a release’s site follows the publication it describes and does not run if that fails. (MV-77, tightening MV-84 to forbid version literals on the site outright)
change close --abandonno longer releases a reserved invariant ID that an anchor names. It archived the change first and then released against an empty anchor set, so the condition the law states — release only when no anchor names the ID — was never evaluated on that path. The ID could return to the pool with a live reference pointing at it, and the nextchange newwould hand it out: MV-26’s collision by another road. Both close paths now read the anchor set before archiving. (MV-45)No lifecycle command sweeps a tree with
git add -A.change applydid, when creating a greenfield repo — harmless in itself, a repo made seconds earlier holding one file, but the law said nowhere, and the leg meant to hold that claim matched only a comment saying “neveradd -A” while the real call spelled the flag differently and stayed invisible. (MV-46)
Documentation
- Eight claims an external audit found overstating their code are corrected, and
the ninth is recorded as examined and accurate. Two moved the code, three
withdrew a clause that described something removed, two gained the ceiling
they were missing, and two documents stopped contradicting the law:
CONTRIBUTING.mdtold contributors to add an entry marked unsupported, which MV-28 forbids, andDESIGN.mddescribed aripgrepmatching engine and a commit-sha-keyed cache that were designed and never built. (MV-21, MV-31, MV-51, MV-56, MV-57)
0.4.0 — 2026-08-17
Added
- A brain now records the version it was deliberately brought to, in
.multivac/projected.yml, and every command tells you when your binary disagrees — in colour, with the command that closes the gap. Upgrading the binary never upgraded a brain:npm i -g multivac@latestreplaces the projector, not the doors, skills, hook shims and harness settings it already wrote. Nothing is refused over a version; enforcement degrades, it never locks you out.mvac doors --adoptre-projects and records. Baremvac doorsre-projects and leaves the record alone on purpose, so the notice survives a run made for an unrelated reason rather than going quiet without the upgrade having been taken.requires: ">=X.Y.Z"in.multivac/config.yml, hand-authored, declares the minimum your team will trust. A binary below it gets the loudest notice. The tool never writes this field.- Existing brains have no record, which reads as an absence and not as an ancient version: you get the mildest notice, once, with the command to fix it. (MV-86, amending MV-29)
Changed — read before upgrading
A command now refuses an argument it does not declare and exits 2, where four of the nine used to accept one and carry on. This can refuse a command line that worked before, so read the list:
mvac doctor --sttrictused to run the report without the strict assertion and exit 0 — a pipeline going green over a gate nobody had asserted was armed. It now refuses and names the flag.mvac doctor <dir>andmvac doors <dir>used to discard the directory and act on the working one. Neither command declares a directory. They now refuse rather than answering about somewhere else.mvac seed --anythingused to ignore the flag;mvac init --badflagexited 1 where the reference documents 2.mvac count --anythingalready exited 2 but printed only its usage; it now names the argument it did not understand.
What each command takes is its
--help(MV-69), and the refusal is measured against exactly that. If a script of yours passes an argument a command never read, it was already having no effect — the difference is that you are told. (MV-85, strengthening MV-29)
0.3.0 — 2026-08-17
One behaviour changed in a way that can newly refuse a repository that was passing. Read the first item before upgrading.
Changed — read before upgrading
The anchor scanner skips a line only when that line carries a complete anchor comment — the opener the grammar defines, and the
-->that closes it. Until now it skipped any line containing the substring@anchoranywhere, soconst evade = "user.name"; // @anchorin your source was invisible to every leg: anabsenttombstone over that pattern reported green at exit 0, and the same line without the seven-character suffix broke it. Those lines are now scanned. If your repository has source, fixtures or docs mentioning@anchoroutside a real anchor comment, a tombstone or acount=Nratchet over them can start refusing — that is the defect being fixed, not a regression.mvac count '<the leg>'shows you the new match set before you decide.The ceiling, stated rather than implied: a line carrying both the opener and
-->still hides, anywhere on the line, in any file, whether or not it is a well-formed anchor. The scanner tests shape and never grammar, because the fixtures that quote whole anchors inside string literals must keep hiding and a forgery is byte-identical to them in shape. Closing that needs something which is not a test on one line’s shape. (MV-82)
Documentation
- The site sets its own type, and serves it from its own origin. Two variable
faces — one for human language, one for machine output — with the width axis
carrying the distinction, both stored in the repository with their licences.
No typeface is fetched from a third party at page load, which is the same
promise
verifymakes about the network. Nothing an installed tool does changes. (MV-83) - Twenty-one statements across the site,
DESIGN.mdand the shipped skill were found to contradict the code and were corrected. The install page told readers the binary prints1.0.0, that the package isprivate: trueand that it is unreleased — none of which has ever been true of a published multivac. Others miscounted the door registry, the command list, the door kinds and the leg states, or quoted output the tool does not print. (MV-84) - The site’s pages now carry exactly one version string, and a test holds
it equal to the manifest. The three pinned version sites — tag, manifest,
badge — were already held equal by MV-68 and MV-77; nothing covered a version
somebody typed into prose, which is how
1.0.0survived on the install page under a law table with 83 anchored rows. (MV-84)
0.2.0 — 2026-08-17
Five of these change behaviour for anyone already running the tool. Read this section before upgrading.
Changed — read before upgrading
verify --strictnow refuses a change that is finished but not closed — every declared claim resolving and every declared repo recorded landed — and names it with the command that fixes it. Until now such a change was indistinguishable from one opened seconds ago: both reportedpending, and pending never blocks. That grace hid fourteen claims in this repo for weeks. If you carry finished-but-unclosed changes,--strictwill start refusing them, in CI too.change close <slug>is the whole fix. A change declaring no claims is never finished, and staleness of any kind is untouched. (MV-80)verifynow refuses a commit that flips a law row toactivealongside the code that row anchors. A rule and its evidence arriving under one hand is a rule nobody reviewed on its own. Commit the law file alone, then the code. The check reads the index againstHEAD, so it answers only while a commit is being composed, and says so when it cannot answer rather than passing silently. (MV-81)change landnow reads whether the work landed from the channel ref instead of commit containment, which a squashing forge defeats every time, and reports the ref, its sha and how long ago it was fetched. It offers the conclusion; recording it stays yours. (MV-80)change plannow refuses while the SDD’s project-level document is missing, empty, or still the unfilled template its own tool shipped. A repo that declaressdd:and has never written its constitution will start being refused where it used to pass. Staleness stays a report: a document older than the law’s newest row is named, never gated. (MV-76, amending MV-57)The lifecycle now runs the SDD’s own init when its scaffold is absent — for spec-kit,
specify init --here --integration <harness> --force, which reaches the network. It runs only fromchange, never fromverify,doctorordoors, which stay offline (MV-01). This closes a deadlock: declaring an SDD in a repo where that tool has never run used to make the change that installs it unplannable. (MV-75)doorsnow deletes files under the skill directory it projects when the source no longer ships them — including one you put there yourself, because nothing on disk records who wrote it. The prune is bounded to multivac’s own skill directory and never its parent, so a sibling tool’s skills are untouched. (MV-73)
Fixed
core.hooksPathis read the way git reads it. An absolute or~-spelled value was joined onto the repo root, so the hook shims landed in a directory tree named after the machine’s filesystem whileinitprinted the real path as the place they went anddoctorcalled them missing from the directory they were sitting in. The enforcement gate was disarmed in silence for any repo with an absolute hooksPath — everygit worktreeinherits one. Every read now goes throughgit config --path, and directory identity is decided after resolution rather than by comparing text. (MV-79, amending MV-37)- The managed
.claude/settings.jsonmerge no longer replaces a hook entry it did not write. It claimed any entry whose command merely containedmvac verify, then overwrote that entry’s whole hooks array and its matcher — deleting a user’s own commands and flags. Ownership is now the individual hook, matched exactly; where that leaves an event ungated, multivac adds its own entry beside yours and says so. (MV-74, amending MV-52)
Added
- A changelog, on both surfaces. This file is the only copy; the documentation site mounts it rather than keeping a second one. (MV-78)
- The version the site advertises is pinned to the version the package declares. (MV-77)
- In multivac’s own repository, the committed copy of the skill its harness
reads,
.claude/skills/multivac/, is held to theskills/multivac/the package ships by a test — same files, same bytes. (MV-72) doctornow states, in its own report, that who enacts a law row cannot be checked by this tool — identity is not a fact on disk — and names where it is enforced instead. Declared ungateable with its reason rather than left absent from the law. (MV-81)AdoptionandCompositionin the docs: the arc frominitto steady state and which phase buys what, and why spec-driven tools and code graphers are built on rather than competed with.
0.1.1 — 2026-08-16
Fixed
- Enumeration counts each tracked file once. A tree mid-merge keeps three index
entries per conflicted path, so every match inside such a file was counted
three times: a
count=2leg reportedfound 6and advised ratcheting to 6 — advice that, followed, would have written a corrupted number into the law over an unrelated merge. Runs taken mid-merge now say so instead of judging a tree nobody will commit. (MV-71)
Added
--helpon any command prints that command’s own flags and arguments. Five of nine commands previously printed only a one-line description;init --helpsaid nothing about[dir],--provider,--sdd,--grapheror--quiet. Where a flag’s legal values come from the adapter registry they are rendered from it, so a new adapter cannot leave the help behind. (MV-69)init --provider <name>now writes that harness’s door, skill and hooks in the same run, instead of recording a name and leaving a second command to be discovered. (MV-70)
Removed
aideris no longer an adapter entry. It sat there marked unsupported, carrying a note explaining at length why none of it applied, and read as support to everyone who did not open it. An unknown name already gets the list of what is supported, which is the answer that helps. (MV-28)
Documentation
- The integrations reference says why
doorsis a list of doors to project and not a list of providers to choose from.
0.1.0 — 2026-08-16
Added
- First release.
npx multivac initscaffolds a brain, arms the git hooks, and writes the canonicalAGENTS.mddoor. - Published by trusted publishing (OIDC) on a
v<semver>tag only, and the job refuses unless the tag equals the version inpackage.json. No long-lived publish token exists to leak or rotate, and a release is a decision somebody makes rather than a side effect of a merge. The published tarball carriesdistandskillsby allowlist — this repo’s own brain, site and tests never ship. (MV-68)