Changelog
All notable changes to this project will be documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Added
- A pull request inherits its last green run's verdict when the delta is low-risk.
magus affected ci --planasks the wired CI provider for the newest green run of the same workflow on this branch -magus\ci.provider(github)wires it, and the github spell'slast_green_runanswers under Actions and null everywhere else, so a laptop plan is unchanged - then classifies every path changed since that run's head commit through the same classifier the local redundancy check uses: generated output, prose, comment-only edits. When nothing classifies as code the plan emits an empty shard matrix and aninheritblock, and ci.yaml's shard fan-out and advisor short-circuit on that one plan output; plan and report always run. The verdict is GREEN and never a silent skip: the run page carries a notice annotation, and the job summary a table naming the inherited run, its head commit, and every changed path with the declaration or mechanism that classified it. A merge anyone pushed into the range re-runs the fan-out - the provider's own synthetic pull-request head merge does not, since the classification already diffs the tree it produced - and every other unanswerable case (no provider, no green run, an unreadable range) plans exactly as it did before.gate_inherit = falseinmagus.projectturns it off workspace-wide. - A target can declare how long it may run.
magus.project's target policies take atimeout, a Go duration string spelled the waymagus.yaml's workspace-widetarget_timeoutalready is -"security": {"timeout": "15m"}. A target that outruns its ceiling has its process tree killed and FAILS with MGS3011, naming the target, the ceiling, how long it ran, and the log its output was captured to; a timeout is never a skip and never a pass. The deadline rides the context, so it also bounds every target reached throughctx.needs, and a composed target that declares its own tighter ceiling is bounded by that one first - so the failure names the target that hung rather than the gate that ran it. Nothing is inherited implicitly and nothing is inferred: an undeclared target is unbounded exactly as before, and where both a workspace-wide and a per-target figure exist the tighter one wins.magus doctorkeeps the declaration honest against the durations magus recorded (MGS1032), in both directions: a ceiling a real run has crowded is about to fail a build that was fine, and one sitting a hundred times above every run on record would hold a hung target's locks for most of a day. This workspace declares two:securityat 15m against a worst recorded run of 44s, andciat 45m against 6m8s. - A target can declare that its failures are volatile, and say why.
magus.project's target policies take aretry_on_volatilereason string -"integration": {"retry_on_volatile": "talks to a shared broker that drops a connection under load"}- which routes that target's failures through volatility detection, so one magus predicts is volatile is rerun once instead of failing the run. Previously this could only be set through the Go registration API, so no magusfile could reach it. A baretrueis a load error, for the reasonskip_cache's anddrift_reason's are: asking magus to rerun a red target until it goes green claims the target fails without the code being wrong, and a bare bool cannot tell a suite somebody measured from a bug they stopped chasing. The policy is visible wherever target policy already is -magus describe target, the BuzzTargetobject, and the JSON record. - A redundant ci gate defers to the pull request when the machine is loaded. When a
green gate is already recorded for this branch and everything changed since falls in
a low-risk class - generated output, prose (magus ships markdown globs;
magus.project'sgate_low_riskreplaces or empties them), or comment-only edits (every language the same way:mgs_getLanguagereturns a typedLanguagewhoseextensionsandcommentsdeclare the syntax, one string-aware stripper consumes it, directive comments are always code) - a saturated machine refuses the run with MGS3010 and exit 75, and an idle one prints the same finding and runs anyway. A deferral is never a success: the refusal names the green gate's run ref, classifies every changed path with the declaration that classified it, shows the pool state, and persists as a record;--no-redundancy-checkforces the run. A merge commit in the range always re-gates, and a language without a declaration is always code. The per-repository gate history lives in the sessions store, shared across worktrees.release-build:immutableandrelease-sign:immutablerefuse to replace an existing archive, checksum sidecar, or manifest signature instead of overwriting it, so a re-run of the release process cannot silently replace a published artifact. The default stays overwrite, which the publish job's re-run path relies on; the rule isreleaser\overwriteRefusal. - A stall watchdog aborts a run that stops making progress. An invocation holding
every selected project's lock is now watched from inside itself: if no target starts,
finishes, or writes a line of output for the stall window, the run fails with MGS3012
naming the last step that ran, how long it has been quiet, and its captured log. The
window is
stall_timeout(MAGUS_STALL_TIMEOUT,--stall-timeout), 15 minutes by default and off when negative. It is the complement of the ceilings above, not a duplicate: a ceiling bounds a target that runs long and only one whose author declared a bound, while this catches a process doing nothing at all, in work no target declared. Output counts as progress, so a slow target that is still printing never approaches the window, and the watchdog runs in-process because a stall with no daemon up is exactly the case where nobody else is watching.
Changed
- The release-index publish flow is a magusfile target.
.github/actions/release-indexhand-rolled the branch, commit, push, and pull-request sequence in workflow shell;magus run release-index:cd -- "<reason>"now owns the whole flow - build and sign the index, regenerate the changelog's derived files across the root and docs projects, answer "anything to publish?" through the vcs module, and open the pull request - so release.yaml's tail and release-index.yaml are each one invocation and the composite action is deleted. Withoutcdit rehearses locally, unsigned whenMAGUS_SIGNING_KEYis absent, and touches no branch.
Fixed
- A per-target policy the engine accepts no longer fails its own dry-run preview.
The dry-run host validated
magus.project'stargetspolicy against a hand-copied list of recognized keys that had drifted toskip_cache,exclusiveandslots, so a magusfile settingmemory_mb,cache,driftordrift_reasonloaded fine undermagus runand was rejected as an unknown option by the Playground, the editor diagnostics, and every other dry-run surface. Both the engine binding and the dry-run host now reject against one shared table,types.TargetPolicyKeys, beside themagus.projectoption table that got this treatment when the same drift bit the project-level keys. A key added to the table reaches both consumers at once, and a test per consumer reads the table and fails if either stops accepting a key in it. - A typo inside a
toolsversion window no longer passes the dry-run preview. The same divergence in the other direction: the dry-run host walkedmagus.project'stoolsmap not at all, so{"go": {"minn": "1.21"}}was green in the Playground and the dry-run preview and then failed the real run, where the engine has always rejected an unrecognized bound key. The recognized keys are now one shared table,types.ToolBoundKeys, and the dry-run host rejects against it entry by entry the way the engine does, naming the bin and the two valid keys. - One generate invocation settles a cross-project derivation chain. The scheduler
now derives target-granular ordering from the declarations it already holds: when
one target's
ctx.writesFilesglobs intersect another'sctx.readsFilesor sources and both run in one batch, the writer runs first, across projects and across target names, which project-leveldepends_oncannot express. Where no step order can honor an edge (two projects' generate chains writing into each other's read sets, previously fixable only by hand-wiring actx.needs), the stale reader re-runs once at the end of the batch, and only when its input's bytes actually moved, so a settled tree re-runs nothing. Only targets that declare writes are re-run this way, verifiers such as a security scan are not, and the settle pass is accounted and watched like the batch itself, so a stalled re-run fails the invocation loudly instead of holding the project locks in silence. A cycle among declared footprints is not an error either: every project's routing index declares that it writes its ownMAGUS.mdand reads its siblings', so the direction no schedule can honor is dropped and settled afterwards like any other unorderable edge. Before this, a changelog edit that was graph-visible needed a secondmagus run generateto land the edge in the committed graph, and the release-index publish opened a born-red PR from the gap. - Work that runs outside the batch is accounted like a step in it. The post-batch
settle pass dispatched its re-runs straight at the interpreter, so they held no
limiter slot, claimed nothing from the machine budget, joined no inflight set, and
produced no journal event:
magus statusreported 0 slots in use and nothing running while the invocation still held every project lock and the run journal went stale. Settle re-runs now go through the same admission a batch step passes - slots, the machine claimmagus statusnames by project and target, the inflight record a killed run is reported from, a captured log and a journal result event with its own output ref - so an off-batch re-run is visible to every observer that already watches the batch, and a stalled one is legible instead of silent. The hardcoded ten-minute ceiling the pass carried while it was invisible is gone with the invisibility: what it still bounded was a settle making steady progress, on a figure nobody declared, and a target that needs one now declarestimeout. - A rejected archive import no longer destroys the cache file it named.
magus config cache importextracted each tar member directly at its final cache path, so a concurrent process could replay a torn blob or manifest mid-import, and a corrupt archive first truncated an existing valid CAS blob and then deleted it on the hash mismatch, destroying a blob other manifests still referenced. Members now stage through a unique same-directory temp file and rename into place only after the size cap and content-hash check pass. - A daemon no longer adopts runs from a binary built from different sources. Every
modified-tree build of one commit stamps the same
-dirtyversion string, so the adoption identity gate matched byte-different binaries and a warm daemon executed adopted runs with the code it loaded at start rather than the code just built: generators then emitted stale output that the local drift gate blessed and CI rejected. A modified-tree build's identity is now its executable file (resolved path, size, mtime), captured at process start; a mismatch falls back to the caller's own fresh binary, and a pre-fix daemon refuses fixed clients rather than trusting them. - Concurrent cache imports can no longer swap staged files under each other's signatures. The remote-artifact import staged its manifest and extras through fixed temp names inside the machine-shared cache, so two processes importing the same entry contended for one path: an importer could overwrite bytes a peer had already verified, and the peer's rename then committed unverified content. Staging now goes through unique temp names with same-directory renames, and extras dropped by a trust decision are removed instead of accumulating.
- A composed target's declared in-place edit no longer trips its composer. A target
run through
ctx.needsexecutes inside the composing step's fingerprint window, and the window carried only the composer's ownctx.modifiesExistingFilesdeclarations, sogeneratefailed MGS4007 wheneverchangelog-generateactually changedCHANGELOG.mdand the workaround was to run the constituent standalone first. The composer now inherits chained update declarations the way it already inherits chained outputs. - The release-index pull request arrives with its derived files regenerated. The cut
rewrites
CHANGELOG.md, but the bot's commit carried neither the knowledge graph nor the docs changelog derived from it, so every index pull request opened failing its own drift gate; the publish flow now regenerates and commits them alongside. SHA256SUMSis hex again. Releases v0.1.0 through v0.3.0 shipped hex; an upstream-parity change then made Buzz'scrypto.hashreturn raw digest bytes, and the sidecar writer, unchanged since v0.1.0, shipped v0.4.2's manifest as those bytes. That broke every consumer at once:sha256sum -cper the verify docs, the install script's 64-hex-digit gate (so asetup-magusprebuiltpin fails its checksum and audit's compat job goes red), andmagus self update's manifest parser. The sidecar now writescrypto\sha256File's lowercase hex, and thedescribe modulehint claimingcrypto.hashcoverssha256_hexis gone - that claimed equivalence is what wrote the bug.- The signed manifest carries the
version:header self-update requires.ParseManifestrefuses a manifest without one, and no release had ever emitted it, somagus self updatecould not verify any published release, independent of the hex defect.release-signnow prependsversion: <tag>before signing; sha256sum(1) skips the line, so the documented verify procedure is unaffected. - The publish job survives being unable to open the release-index pull request. The
branch push is the deliverable and container images gate on the job, so a refused
gh pr create(repository Actions settings can forbid it, and did for v0.4.2) now warns with the branch URL instead of failing; the branch push is forced so a re-run survives its own earlier push.
[v0.4.2] - 2026-09-03
See the full changelog at https://github.com/egladman/magus/compare/v0.3.0...v0.4.2
Breaking
magus queryexits 1 when it could not answer. A search that matched nothing and reported theunknownverdict - a stale symbol index, or a lazily-loaded layer the lookup never consulted - now exits 1 where it exited 0.foundandabsentstill exit 0, which is the documented rule this keeps rather than the one it changes: an empty result set is a legitimate answer to a search. What changes is the case that was never an answer at all, where exit 0 with a caveat buried in the text reads as "not in the graph" and sends the caller back to a text search - and it is the one case a command fixes,magus graph build. The status is the same under every-o,querystill never exits 2, and a populated result still exits 0 whatever its verdict, because there the caveat rides rows that are already facts.magus query <terms> && next-stepagainst a workspace whose index is missing or stale now stops where it used to continue; a caller that wants the old behavior branches on.answer.verdictfrom-o jsoninstead.
Removed
- The four JSON run-browser routes are gone:
GET /api/v1/outputs,/api/v1/output?ref=,/api/v1/runsand/api/v1/run?inv=. The typedmagus.viewer.v1alpha1.ViewerServicereplaces them -ListOutputs,GetOutput,ListInvocationsandGetJournalread the same two stores, and the contract they speak is generated rather than hand-marshaled. The service had been defined since the log viewer shipped and was never mounted; the JSON routes were hand-written strings with no schema behind them. magus.FailOnDrift()is gone, with no replacement alias. It named the response rather than the decision, so it could not carry "warn" or "off", and what it checked - whether the working tree was dirty after the target - was disarmed by any unrelated uncommitted edit. Usemagus.Drift(policy, reason), or thedriftkey in a magusfile's target policy.
Changed
- Writing a handoff entry that already exists now writes only the fields you sent.
magus memory put, themagus_memorytool'sop=put, and the console'sUpdateMemoryused to replace the whole record, so refreshing a plan's status silently dropped the body, the refs and the excerpt beside it. The store keeps no history, and the result was indistinguishable from a first write. All three now follow AIP-134: an absent update mask means the fields the caller populated, and everything else keeps what is stored. The cost is the one AIP-134 names, and it is the reason a mask exists at all: an omitted field can no longer CLEAR a stored value, so delete the entry and create it again for that, or name the field in an explicitupdate_mask. Two contract points come with it. A record cannot be updated into another type, because the type is the axis a listing reports and it decides which fields the entry may carry; delete and recreate instead. Andallow_missingnow decides whether an absent name is created or refused, spelled--amendon the CLI, so a mistyped name is an error rather than a stray second entry.MemoryService.UpdateMemoryaccepts a partialupdate_maskwhere it used to reject one; an absent mask stays a full replace THERE, because its caller is a person editing a form that shows every field and an emptied box has to clear one. - One unreadable handoff entry no longer takes the readable ones down with it.
magus memoryused to fail a whole listing when any single record failed to parse or validate, which disabled the surface a person would use to find and delete that record. Reading now skips the bad entry and returns the rest, with the problem reported bymagus memory verify. An entry whosetypethis binary does not know is reported as a warning and skipped, so a journal written by a newer magus stays usable by an older one;verifystays green on it, since there is nothing to repair. Writing an unknown type is still an error: the tolerance is on the read path alone. This helps only binaries built from this release forward. A magus already shipped still refuses the whole listing when it meets aneliminationentry, so a journal shared with an older checkout needs that checkout upgraded. magus querygains operators:kind=spell(match),kind!=op(exclude),id=~build$(regex).=reads as a match over a structured graph the way kubectl selectors and PromQL matchers do, and!=carries negation without the flag collision the dash spelling had.kind=~"spell|op"ORs alternatives, and the filters read the same in every verb they appear in. The:grammar (kind:spell,-kind:op) still parses as a compat alias, so existing invocations keep working; new queries should use=/!=/=~.- The drift gate runs for every target that declares outputs, and no
declaration turns it on. Declaring an output is already the claim that those
bytes follow from the target's inputs, so magus checks the claim rather than
asking each workspace to opt in. It previously applied only to
preflightandgenerate, and only when a target declaredFailOnDrift. Turn it down with{"drift": "warn"}or off with{"drift": "off", "drift_reason": "..."}; a reason is required to switch it off, as it is forskip_cache. - The drift gate fails only for output this change made stale. Output that drifted with no source change behind it - a merge whose own CI never finished, a generator nobody re-ran - is reported and does not fail the run. Failing it billed whoever opened the next pull request for a decision they were not party to, and they could not fix it without committing bytes they did not produce. This is not configurable: there is no setting that restores the old behavior.
- The gate now hashes each target's declared outputs before and after the run, rather than asking the VCS what is dirty afterwards. Bytes that did not move are not drift however dirty the surrounding tree is, and detection no longer depends on the VCS at all - only attribution does.
- Agent guard templates are at version 9. Codex's hook commands now carry
GUARD_NO_ADVISE=1inhooks.json, and the notice a template prints when the magus it found cannot judge a command now names the evidence - which binary path it resolved, that binary's version, and the error it actually printed - rather than offering "too old forsession hook, or cannot load this workspace" as equal suspects. The second was never a cause: the deny rules need no workspace, and a current binary run from an empty directory still denies, so that half of the sentence sent readers to check something no evidence pointed at. Re-download the templates to pick both up; enforcement and the rendered verdict are unchanged.
Fixed
- The release asset guard no longer refuses a Windows build.
fs\joingives the runner's separator, so the windows job compareddist\magus_v0.4.1_windows_amd64_static.tar.gzagainstdist/magus_*.tar.gz, found no match, and failed the v0.4.1 release on a path actions/upload-artifact normalizes and collects. An asset glob is workflow vocabulary, so both the guard and the release preflight now compare throughreleaser\toSlash. The preflight had the same defect and would have called every asset an offender when rehearsed on Windows. - A Windows worktree is no longer dirty the moment magus loads it.
.gitattributesis the one tracked file the merge-driver refresh rewrites on every workspace load, and it wrote a hard-coded\n. Git for Windows shipscore.autocrlf=trueand this repository declares noeolattribute, so checkout smudges the LF blob to CRLF on disk; rewriting it as LF makes git report the file modified, andgit describe --dirtythen answers<tag>-dirty. Every v0.4.1 Windows artifact was stampedv0.4.1-dirtywhile the other four platforms were clean. The section is now emitted with the line ending the file already uses. Only this writer needed it: the sibling managed sections land in.git/config,.hg/hgrcand hook scripts, none of them tracked. - The admission daemon starts on Windows.
exec.Commandresolves an absolute path there throughlookExtensions, which requires a PATHEXT sibling to exist, and setup-magus installs an extensionlessmagus, so the running binary could not re-exec itself:exec: "C:\hostedtoolcache\windows\magus\bin\magus": executable file not found in %PATH%. Machine-wide admission failed open, so the release still built. The spawn now sets the command's path verbatim, which is what re-execingos.Executable()wanted in the first place; PATH and PATHEXT have no say in which binary this process is. release-buildworks on an untagged commit again. When HEAD carries no root release tag,version()falls back togit describe, and describe picked among every tag in the repository - so with four v0.4.0-era tags on one commit it answeredlibs/diagnostics/v0.1.0-1-g<sha>, a version with a/in it, andrelease-buildrefused. That made the target unusable on main, which is a large part of why the release path went two versions without being exercised. The fallback now constrains describe tov[0-9]*, which is the same shape checkisReleaseTagapplies: it excludes this repository'sverified-refinementstag by the digit and everylibs/<name>/v0.1.0by the leadingv. The magusfile's note claiming a namespaced base was something "no version resolution here can prevent" was simply wrong.vcs\tagsno longer renames a tag that shares its name with a branch. The git backend asked for%(refname:short), which abbreviates a ref only as far as it stays unambiguous - so a repository holding both av0.4.0branch and av0.4.0tag got the tag back astags/v0.4.0. That name matches nov*pattern and splits into the module prefixtags/, so the tag was invisible to every caller asking whether a release exists: thetaggedcharm declared a tagged HEAD untagged, andmagus run releasesurveyed the repository as still sitting on the previous version. The v0.4.0 release ran against a repository whose own release tag it could not see. The name is now the tag as written.- A release commit carrying several tags no longer names its assets after the
wrong one. magus's own build read its version from
git describe, which picks arbitrarily among the tags on one commit; cutting v0.4.0 tagged three nested library modules on the same commit and describe chose one of those. Every archive was then namedmagus_libs/diagnostics/v0.1.0_<os>_<arch>.tar.gz- a path, not a filename - so the builds landed in a nested directory the release workflow's upload glob never looked in, and the release published no downloadable assets while reporting success. The version is now the root release tag on HEAD when there is one, andrelease-buildrefuses outright, before it compiles anything, if the version it resolved would put a directory separator in an asset name. magus describe fileno longer classifies a path that is not in the workspace. It is pure glob matching, and**/*.gomatches an absolute path from another checkout as happily as a relative one - so a fabricated or mistyped path came backproject: .,role: source,declared: source . **/*.go, byte-identical to a real file beside it. A path that resolves outside the root is nowunclaimedwith a hint naming the workspace it is not in, and every entry carriesexists, printed asexists: falsein the text form and always present in-o json. Classification is deliberately still answered for a file that is not there yet - "where would this land" is a question worth asking - so this reports existence rather than turning a missing path into an error.magus describe filereads a path shape the waymagus queryandmagus wheredo, through the sharedfile.NormalizeWorkspacePath. Its own partial normalizer handled neither backslashes nor an absolute path rooted somewhere else, socmd\magus\guard_shell.gowas taken as a literal filename.magus vcs resolveno longer gives up when the merge left conflict markers in a magusfile. It reads the committed magusfile through the newtypes.RevisionFileReadercapability, settles the generated conflicts with those declarations, and leaves the hand-written one for you - rather than reporting the interpreter's verdict on a<<<<<<<line and settling nothing. It deliberately does NOT regenerate in that state: a merge that changes a generator would produce bytes matching neither side, so it says to runmagus run generate:rwonce the magusfile is resolved.magus vcs resolveno longer loses the whole resolution to one renamed path. It staged with a single call whose pathspecs included files the rename had removed, and one pathspec matching nothing aborts the call before staging anything - so regeneration completed and the index was left untouched. Paths that cannot be staged are now reported by name instead.docs:site-generatedeclares the bundles it needs rather than relying ongenerateto order them first. It was correct as a stage and broken alone, somagus run site-generate docs- andmagus vcs resolve, which invokes the target owning a conflicted output directly - failed the site's asset-integrity check on the playground's missing wasm glue.- Agent guard: Codex is no longer sent advisories it rejects. Its PreToolUse
treats
additionalContextas an error and then fails open, so each advisory was discarded and disarmed the guard for that call. It now declaresadvise=none. The two fail-open notices still carry the key. - Agent guard: the
relockcharm is now taught in the magus-run skill, and a gate keeps every advisory covered there. An advise injects on one host of four, so guidance carried only by a verdict never reached the rest.
Added
-
magus run releasepreflights the release before it creates a tag. Every named module's would-be tag now goes through checks that used to fire hours later in CI, on a tag that a pushed version number makes unreusable: the versionrelease-buildwould stamp once those tags sit on HEAD, and whether every asset name it produces matches the globrelease.yamluploads with; the two thingsmagus-utils cutrefuses on in the publish job, an existingreleases/v<version>.yamland an empty[Unreleased]section; and that the root tag matches the workflow's trigger while no module tag does. A branch sharing the would-be tag's name is reported as aNOTEand never refused:refs/headsandrefs/tagsare separate namespaces so the two names may coexist, and the practical residue is only that a bare name is ambiguous, so the tag wants itsrefs/tags/<name>form when pushed. The dry run prints every verdict and refuses nothing, so a rehearsal shows all the problems at once;release:cdrefuses on any failure and tags nothing. The dirty-tree refusal moved after the transcript for the same reason - it used to hide every check behind it. This is the v0.4.0 release stated as a gate:git describechoselibs/diagnostics/v0.1.0from among four tags on one commit, every asset was namedmagus_libs/diagnostics/v0.1.0_<os>_<arch>.tar.gz, and a glob star does not cross a/, sodist/magus_*.tar.gzmatched nothing and the release published zero assets while every build job passed. -
release-buildrefuses an asset name the release upload could not collect. The guard asksRELEASE_ASSET_GLOB, the same string the preflight pins torelease.yaml's upload step, instead offs\basename, so it also catches a rename of themagus_prefix or the.tar.gzsuffix on one side of the handoff alone. It runs before the first byte is written, so no workflow step re-checks the built files: every asset in a job shares one version and the remaining name parts are fixed tokens, so the glob selects all of a job's archives or none. -
buzz-testnow runsreleaser.buzz. Its test blocks had never been executed by any target - the root list namedbadge.buzzandcoverage.buzzand nothing else - so the rules deciding what a release may be versioned as were covered by tests nobody ran. -
The handoff journal records what an investigation ruled OUT as well as what it concluded. A fourth entry type,
elimination, names the dead hypothesis, carries the reason in--body, and requires--excerpt: the captured evidence that killed it, copied into the record. The excerpt is required because the ref beside it is not durable. An output blob lives under the checkout that produced it while the journal is keyed by repository, so a ref minted in an agent worktree stops resolving the moment that worktree is removed, which decays a ref-only record into a dangling pointer with a confident tone. The ref stays as a best-effort reopen handle, andmagus memory verifynow warns when one no longer resolves. A session that hits its limit used to take its whole elimination trail with it, leaving the next one to re-tread falsified branches. Available frommagus memory put, themagus_memoryMCP tool, and the console;magus notes promotecarries the excerpt into the note. Nothing is captured automatically and nothing gates on it. -
magus agent adoptionmeasures whether agents actually use the knowledge graph. It reads a corpus of shell commands (stdin or--commands) and reports how often the graph (query/refs/explain/path) was reached versus a raw text search, the graph-to-grep ratio, and the top repo-wide greps whose pattern is a real identifier - each with the graph command to try for it. That command is routed by the pattern's shape through the same translator the live guard suggests with, so a diagnostic code and a Buzz op readmagus queryrather thanmagus refs, which covers compiled-language symbols only and would miss them both.-o jsoncarries it asrunon every top-pattern entry. magus stays host-agnostic: it analyzes commands, never a host's session logs, and the help prints the recipe to extract them. It turns "query before grepping" from doctrine into a number you can watch move. -
The guard's search advisory hands back a command to run, scoped to the project you searched. A repo-wide content search (
grep -r,egrep -r,fgrep -r,rg,ag) or a file-find (find -name,fd) now carries a concrete suggestion rather than a principle to weigh: content routes by the pattern's shape tomagus queryormagus refs, and a file-find becomesmagus query kind=file id=~<re>built from the name or extension it asked for. When the search named a directory the knowledge manifest knows as a project, the suggestion carriesproject=~^<proj>(/|$). That is an anchored regex rather thanproject=<proj>because a node resolves to the LONGEST project owning it, so the exact form would drop every node under a nested project that the grep it replaces would have matched. A search spanning two projects abstains from scoping instead of emitting a filter that silently answers half of it. Suggestions are paste-ready shell: a pattern carrying$, a backtick, a backslash, a double quote, or a!is single-quoted, so pasting one cannot run a command substitution or trip history expansion in an interactive shell. -
The knowledge graph now indexes markdown by heading, so prose is retrievable a section at a time. Every heading in a tracked markdown file becomes a
docsectionnode whose id and source are<path>#<anchor>- the same fragment a link into the rendered page carries, computed with the site's own goldmark auto-heading-id so the two agree. A pagecontainsits sections and a section contains the headings nested under it. An agent looking for where something is explained runsmagus query "kind:docsection <terms>"and gets the passage, not the whole file; the guard advises the pattern when it sees acatorgrepof a.md, and the magus-query skill teaches it. Knowledge schema is now v10, which forces a rebuild so an on-disk v9 store picks up the section layer. -
magus diffreads the working tree's uncommitted changes, annotated and ordered by what they can break. A changeset is a set of consequences, not a list of files, and alphabetical order spends a reader's attention at random - it gives a regenerated lockfile the same weight as a signature twelve packages depend on. Each file carries the evidence behind its rank: how widely its changed symbols are referenced, whether any referent crosses a project or the module boundary (the question a version bump turns on), the coverage a prior run observed, how often the file has been changing, and which agent sessions wrote it. Declared target outputs are folded away by default, because reading a generated file is reading a machine's restatement of an edit made somewhere else;--generatedshows them anyway.--watchre-reads on every tree change, and a patch can be read from a file or-instead of the tree. None of it is a verdict: magus does not claim a change is breaking, since deciding that needs signature compatibility, a base-side index magus does not keep, and language semantics it does not model. It reports who can see the thing you changed. Alsomagus\diff()in Buzz,magus_diffover MCP, andGET /api/v1/diff(with/api/v1/diff/patchfor the raw patch), all ranking by the same definition so a terminal, an advisor, and an agent cannot disagree about what to read first. -
A person and an agent can pair on one diff session. The console's Diff surface (
/console/diff/),magus diff --tui, and themagus_diffMCP tool attach to the same session when a daemon is reachable, so a comment written in the browser renders inline in the terminal and an agent cites a hunk by index rather than guessing one. The session carries the digest of the patch it was computed from andop=staterecomputes when the tree has moved, rather than replaying whatever a browser last attached - an agent is never served a changeset that stopped existing, and a path or hunk the change does not contain is refused.POST /api/v1/diff/sessionis the human half. The surface also answers with no daemon at all:/console/diff/#demoruns the real surface over a fabricated changeset. -
magus vcs checkpointprints the working state's identity and writes nothing. No tag, no stash, no ref, no file - it reads the head revision, the branch carrying it, whether the tree is dirty, and a 32-hex digest of the uncommitted patch, so a checkpoint nobody kept has cost nothing but the probes. The digest is deliberately the same algorithm the diff session uses for its patch, so a checkpoint recorded when work was handed out and a session opened over the same tree produce the same string; either can be used to check the other.-o nameprints the one citable token for a ledger cell, andmagus_vcs_checkpointserves the same facts over MCP. magus emits the facts and decides nothing about what they mean. -
A delegation ledger, for recording what work was handed to whom. An orchestrating agent writes one row per delegated unit through the
magus_ledgerMCP tool (opoflist,put, orclear), in the vocabulary themagus-delegate-multi-agentskill defines;GET /api/v1/ledgeris the read door onto the same file and the console's Plan surface (/console/plan/, served alongsideGET /api/v1/plan) renders the units as a layered graph with the live run states overlaid from the pool. It records and never enforces: magus does not check that a worker stayed inside its owned paths, does not block a write outside them, and derives no verdict from a row. Aputmerges field-at-a-time under one lock rather than read-then-write, because an orchestrator advancing a unit's state while that unit's worker records its checkpoint would otherwise have the second write revert the first.clearreports how many rows it dropped, since clearing is both how a fresh plan starts and how one orchestrator silently erases another's. -
A Sapling backend (
sl), the fourth VCS magus drives. Sapling is a Mercurial fork, so most of the hg driver's shapes carry over, but every one was verified against a realslrather than inferred from that lineage - the places Sapling has diverged are exactly the places a transliterated hg driver fails silently.sl tagsis a deprecated no-op and git tags are invisible even in a git-backed clone, so tag lookup and describe report nothing rather than guessing;sl debugignoreanswers "not ignored", which CONTAINS "ignored", so hg's substring test would have reported every path in the tree as ignored; andsl mergeprompts wherehg mergedoes not, so the driver names a merge tool explicitly. A cross-backend parity suite now pins the behavior every backend has to share, rather than the behavior git happens to have. -
ctx.observes(name, value)declares an external fact a target's answer depends on. An image scan is keyed on the image and the tree, but its answer also depends on the scanner's vulnerability database, which moves daily and is not an input magus can see - so a cache hit reports yesterday's CVEs against today's image. The only control wasskip_cache, which forfeits caching forever to avoid the staleness. An observation joins the cache key instead, so a fact that moved is a miss and a fact that held still replays, and it keys as its own input class:magus describe target <t> --cacheshows anobsline, so "why did this rebuild" names the external fact rather than blaming a source file. Like every other footprint declaration it takes literal arguments on the target's ownctx; a computed value is rejected at load, because the key is minted before the target body runs and a value probed at key time cannot reach it. That restriction is why this does not yet convert the image-scan case it was built for. -
Third-party dependencies are graph nodes.
magus query kind:packagelists them;magus explain package:<manager> <name>shows a package's version, whether it is indirect or replaced, and which projects require it. Nodes are keyed by manager plus name so an npm and a Go package with one name never share a node, a version bump edits an attribute rather than renaming the node, and two projects pinned to different versions of one package share a node that flags the split. Go modules only in this release; other manifest readers follow. -
MGS1028 reports a changed file that seeds a project it does not key. Seeding and keying are separate mechanisms and this is the case where they disagree: directory containment selects a project, the root project catches whatever no directory claims, but only DECLARED sources enter a cache key. So touching an undeclared file selects the project, magus runs the target, the key has not moved, and the answer is the one already recorded - a config edit at the root of a monorepo can rebuild and retest everything and produce nothing new. The silent half is worse and is the same declaration missing: when that file genuinely does change what a target produces - a lint rule set, a toolchain pin, a formatter config - the cache does not know.
magus affected --impactand--explainemit it, naming the seed projects rather than the files, because both already mark each file inline andmagus describe fileexplains any one of them in full.magus doctorreports the standing set. -
magus describe filereports the individual declarations behind a path, and which ones cover more than one of them.claimscarries each declaration that names the path with the project and target that made it and the glob that matched, which is the unit that answers "which target rewrites this" where the existingoutput_of/source_ofsummary only answers "whose tree does it appear in". A cross-project write is attributed to the DECLARING project, since that is the only one that can regenerate it. The claim set is wider thanroleranks: it also carries the in-place edits ofctx.modifiesExistingFilesasupdate, a write nobody replays or cleans.overlapsgroups the declarations covering several of the requested paths, once per declaration with the paths it covers rather than once per pair - a hundred paths under one glob is a hundred rows rather than five thousand. It is a fact and not a verdict: one target rewriting two paths may be a collision between two authors or exactly what one author intends, and nothing here decides which.magus_describe_filecarries both. -
magus versionreports the daemon's version beside the client's. There can be two binaries - this one, and the daemon that has been serving the workspace since it was started - and a daemon outlives the CLI that started it, so upgrading magus leaves the older code running until it is restarted. That is the case this exists to show. The daemon line reads "not running" when nothing answers, and--clientskips the probe entirely for a script that wants the build stamp with no daemon I/O. -
A hook payload carrying a prompt is recorded as a delegation handoff and exempt from judgment. The guard reads what a host hands it by field shape rather than by tool name - a payload with a command is a command, one with a file path is a path, and one with a prompt is an orchestrator handing context to a sub-agent. There is nothing to judge in a prompt: no command, no path, only a context transfer to note, so no rule is evaluated against one and the guard never denies it. It is tested last on purpose, so adding the branch cannot change the verdict on any payload the guard already judged. The event records the sub-agent type where the host supplies one (it repeats across spawns, so it groups a delegation feed), falling back to the per-spawn description and then to the tool name. Every activity event also now carries the host's own transcript path as a POINTER, so a session id in the console leads somewhere; magus never reads the file. The console gains an activity drawer that renders every activity kind.
-
magus statusreports the concurrency a run actually gets, and every live pool.concurrencyalone could not be budgeted against, because its common value is 0, which means "nothing was configured" rather than "no build may run";concurrency_effectiveis that resolved through the default and the machine clamp. Each pool entry addsavailable(free slots, floored at zero) so the reader does not subtract, andsocketso entries can be told apart. Two live proc servers used to produce no pool section at all - a "use --socket to select one" error stood in for it, withholding exactly the capacity and in-use numbers the question was about - and are now enumerated underpools. -
The docs site announces a release from its own bar, and can hold a post back. The announcement strip links the blog post for the newest shipped release rather than only naming the version, and a post marked
draft: truein its frontmatter renders nowhere - no post page, no blog index entry, no feed item, no announcement link - so unfinished writing stays committed and reviewable in the repository. The gate lives in the one walk every renderer shares, because the draft test previously sat in exactly one of three and the front door hid a draft the blog index went on listing.
Changed
- A changed file now seeds every project that DECLARES it, not only the one whose
directory contains it. A project declaring a source outside its own tree was invisible
to
magus affecteduntil the containing directory happened to be a project too, so an edit to a file a target genuinely reads could select nothing. Containment still seeds, and the root project still catches whatever no directory claims - that last case is what MGS1028 above reports. - Churn follows a file through a rename. A rename used to split a file's history
across every name it ever had, so each fragment ranked as a separate, quieter file than
the one thing actually being rewritten. Each backend now reports what a commit did to a
path - added, modified, deleted, or renamed, with the previous name on a rename - and
attribution follows that lineage.
FileHotspotgainsmoves, the number of times a file changed address inside the history window: a file that keeps moving is churning architecturally rather than textually, which is a different thing to know than its edit count and is not derivable from its path. A backend that cannot detect a rename reports it as a delete and an add, which is the old behavior rather than a wrong answer.
Fixed
magus --root <path> ls targets .no longer reports that.escapes the workspace root. A relative project ref was measured from the caller's cwd rather than from the workspace--rootnames, sofilepath.Relanswered an outside cwd with a../-prefixed anchor and every relative ref inherited the escape:magus --root <ws> ls targets .failed withproject path "." escapes workspace root. Refs now anchor at the workspace when one was named explicitly. A ref that genuinely escapes is still rejected - this stops magus inventing an escape the caller did not write, it does not widen what resolves - and inside the workspace a dot-relative ref still anchors at the cwd.- hg and jj reported cwd-relative paths where git reports workspace-relative ones. The same question answered from a subdirectory produced paths that resolved against different roots depending on which VCS the workspace used, so churn, hotspots, and every consumer of a changed-file list were quietly wrong for anyone not standing at the root. Found by the cross-backend parity suite, which now pins this as behavior every backend shares rather than something git happens to do.
magus memory ls -o nameprintedunsupported formatinstead of the entry ids. It is the one-per-line form every other listing command answers, and the shared renderer does not implement it, so a command that offers it has to render it - reaching the default case read as a broken flag rather than a gap.magus affected --planlikewise emitted JSON whatever-oasked for, and now honors it.
Breaking
-
magus insightis removed. The lenses were never a daily verb - they are a reporting surface, reached from CI and from a magusfile - and a subcommand is the one place they cost every reader ofmagus --help. The survivor ismagus\insight(): the typed report, computed IN-PROCESS from the workspace magus already has open, plusmagus_insightover MCP for agents, which never went through the subcommand at all. There is no document renderer: presentation is the caller's job, built from the typed report.What this costs, in full:
- The report can no longer be computed from a bare
magus buzzscript with no workspace on the context: there is no longer a nested magus to fall back to. - The volatility lens has no standalone surface any more. It is a field of the
whole report, but
magus_insightnever carried it, so an agent cannot ask for it alone. - Per-project scoping is gone. The subcommand defaulted to the cwd's project and
widened with
--workspace; both survivors are workspace-wide only. - The standalone Mermaid renders (
-o mermaidfor hotspots and affinity, and the quadrant chart) are gone with the flags that selected them. The combined report now always emits the PORTABLE Mermaid subset - what is left writes INSIGHT.md into a repository or a CI step summary, and those are the renderers that subset targets. InsightReport.graphStatsis removed. The CLI populated it from the knowledge graph it loaded for itself; nothing else ever did, so keeping the field would have shipped a documented axis that is structurally always empty.magus graph statsis the structural axis and is unaffected.magus\insighttakes an options map ({commits, since}) rather than a list of CLI flag strings, which is the shape the subcommand imposed on it. An unknown key is now an error rather than a silent default.
- The report can no longer be computed from a bare
-
magus\diagnoseDriftnow returns aDriftResultobject, notDriftVerdict. A magusfile annotating the return type has to follow. The rename settles what the word means across the codebase: a VERDICT is the scalar judgment, and the thing carrying one is named for the question it answers.DriftVerdictwas a record of drift, not a judgment value, andStagingVerdict(internal) was four slices classifying paths. -
Knowledge-graph schema v9. The shard store invalidates and rebuilds on first run. Two bumps landed in this window and neither breaks a parser; both break a WARM STORE, which is what the version is for. v8 added symbol-to-symbol
callsedges, so a v7 consumer would read a symbol's edge set as complete when it is not. v9 addedsecret_refsto a target node, and its shards were extracted before the field existed from a magusfile that has not changed since - so nothing but the version would ever invalidate them. -
vcs\diffis nowvcs\changedFiles. It returns the file paths changed against a base ref, not a diff, and the name said otherwise. The confusion became concrete whenvcs\dirtyDiffarrived and read like a variant of it rather than a different question. The old name is retired rather than repurposed on purpose: host-module members are typed to the checker, so a magusfile still callingvcs\difffails at load with a clear error, where reusing the name for the new meaning would have silently passed a base ref where a path list is expected. -
magus agent install-agents-mdis removed; magus no longer writes yourAGENTS.mdat all. It managed a marker-delimited block inside the file - creating it when absent, replacing the block in place on re-run, never touching your bytes outside the markers. That is the careful version of an installer appending to your.bashrc, and the care is what makes the point rather than excusing it: the file belongs to the developer, merge logic like that is never as careful as it looks, and a re-run leaves bytes nobody wrote in a file nobody can easily audit. Instruct, do not mutate. Nothing replaces the subcommand -magus agent installnow PRINTS the block on stderr for you to paste, and only when yourAGENTS.mdis missing it or carrying a stale one, so a--forcereinstall does not dump 80 lines of Markdown at you every time.magus agent sampleprints the same block inside a whole starter file and is never gated. ReadingAGENTS.mdto grade the pasted block's stamp is untouched:magus graph verifystill reports it present, absent, or stale per location, because reporting is not writing. Two knock-on effects:magus agent samplenow emits its magus guidance BETWEEN the begin/end markers rather than unmarked, so a paste from it is gradeable exactly as a paste from install's offer is; and this repo's owngeneratetarget no longer rewritesAGENTS.md, which makes that file plain hand-authored prose instead of a hybrid of prose and generated block. -
fs\mkdirallis nowfs\mkdirAll. The descriptor's underlying name was one mashed word instead of the snake_case every other multi-wordfsmethod declares, so codegen produced an identifier inconsistent withfs\readFile,fs\removeAll,fs\copyFile,fs\listDir, andfs\appendFile. There is no alias: a magusfile callingfs\mkdirall(...)must be updated tofs\mkdirAll(...). -
has_charmis nowhasCharm, on both receivers.magus\hasCharm(...)andctx.hasCharm(...). It was the ONLY snake_case member on either surface, sitting beside camelCase neighbors (ctx.needs,ctx.readsFiles,magus\bustCache); the lock file now has no underscore in it at all. The name was pinned because the static charm extractor ininternal/describematches it literally to build the charm inventory - that matcher moved with it, and the existing tests for both receivers and both arms of a charm branch are what make the rename safe rather than silent. -
magus\graphis nowmagus\projectGraph. It returns the PROJECT dependency DAG, and sat besidemagus\targetGraph, which returns a different graph entirely. NamedgraphandtargetGraph, the second read as a variant of the first; they are siblings, so each is now named for what it contains. It also settles the surface's one inconsistent qualifier: every other pair suffixes (describe/describeFile,affected/affectedImpact) while this one prefixed. -
magus\modules()andmagus\module(name)are now onemagus\describeModule(name?). Omit the name for every module; pass one to detail it. Either way the return is a[Module], so detailing one readsmagus\describeModule("fs")[0]. The pair was a list/get CRUD split this surface uses nowhere else.magus describe <noun> [<name>]is ONE command - its own usage says "singular and plural are interchangeable; pass a name to detail one entity" - and the Go API underneath (hostmodules.Describe) already took an optional name and returned a slice. The Buzz surface now mirrors the CLI one method per command form, the waymagus\describeFilemirrorsmagus describe file. -
Logging moved to
magus\log, andmagus\normalizeis nowmagus\canonicalName.magus\info(...)becomesmagus\log.info(...), and the same fordebug,warn,errorandhint.magus\fatalandmagus\raisedeliberately did NOT move. The line is what a member DOES, not what it looks like: everything inlogemits a message and returns, whilefatalandraiseend the run. Grouping the two together would letmagus\log.fatal(...)read as one more level, which is the confusion the split exists to prevent. It also settles the surface's odd asymmetry -magus\secretandmagus\cachewere grouped while five logging members sat loose beside them.normalizewas renamed because it named neither its input nor its output. It canonicalizes a magus ENTITY NAME - a target, charm, or spell op - and the doc's own word for the result was already "canonical":build2gains a-you did not type,HTTPServerbreaks before its last letter. Both fail at LOAD, not at run time, because host-module members are typed to the checker. Seeinternal/interp/bindings/testdata/magus-api.lockfor the full surface before and after. -
os\exec,os\shellandos\whichmoved to a newprocmodule. Importprocand callproc\exec(...),proc\shell(...),proc\which(...); there is no alias inos. The three were the only members ofosthat start a CHILD PROCESS, and everything left behind (env,platform,exit,sleep,hostname,executable,retry) reads or affects the CURRENT one. That is two different capabilities under one import, and the split is what lets a reader see which magusfiles spawn anything at all.proc\shellis also where the Windows branch belongs: it pickscmd /coversh -cper platform, which is a fact about running a shell, not about the operating system a script is asking questions of. A magusfile still calling the old spelling fails at LOAD, because host-module members are typed to the checker - but note the error names the missing member rather than pointing atproc, since nothing maps retired members to their new home outside theremovedtable ininternal/interp/bindings/modules_test.go. If a third such rename lands, that table is the thing to promote into a real migration diagnostic.
Added
-
magus describe filereports amaintainedrole. It sits betweensourceandunclaimedfor a path magus's own core writes outside every target's declared globs -.gitattributesis the only one today. Both halves of magus already knew this and disagreed out loud:magus vcs addreported it as a file "magus itself maintains", whiledescribe filecalled it unclaimed and advised checking the ignore rules, for a file magus had just written and needs tracked. The advice was worse than cosmetic, because acting on it drops magus's own merge-driver registration.magus\describeFileandmagus_describe_filecarry the new value, and the PR advisor that lists unclaimed files stops naming it. It is a refinement ofunclaimed, never a rank abovesource: a workspace that genuinely declares one of these paths still reports it as declared. -
A built symbol index no longer changes the committed graph. A SCIP index is cache state - gitignored, per-worktree, present only where the
scipop has run - but two aggregate shards folded its paths into the DEFAULT graph:@dirsminted a dir node per symbol directory, and@iominted produces/consumes edges for symbol files. SoMAGUS.mdandgen/knowledge-graph.jsondiffered between a developer who had runmagus graph buildand CI, which never does, and the drift gate fired on the difference. The@iohalf was worse than nondeterministic: those edges sat in the default graph while their target file nodes did not, so the committed graph carried 138 references to nodes it does not contain. Both now stay in the per-project@symbolsshards, so an edge and its endpoint appear together or not at all. -
The knowledge graph now has a call graph. SCIP records an enclosing range for each definition, so a reference occurrence inside one was written in that definition's body and the enclosing symbol is the caller.
magus explain symbol:Xgains "calls" and "called by", andmagus query relation:callsreaches them. Nothing new is parsed and no indexer re-runs: the ranges were already in the indexes magus caches and were being discarded.Two restrictions keep the relation truthful rather than merely large. The callee must be callable - an enclosing range spans the whole declaration, signature included, so measured over this repo's own index only 26.4% of the occurrences inside one are calls and the rest are struct fields, types, and packages. Callability comes from the moniker's SCIP descriptor suffix rather than the optional
SymbolInformation.Kind, so it holds for any indexer: scip-typescript populates no kinds at all, and a kind-based rule would have produced no calls for TypeScript at all while looking like it worked. And the callee must be defined in this workspace, since a call into a dependency has no body to navigate to and its usage is already on the referencing file'sreferencesedge. Together those took the symbol shards from a projected 2.35x to a measured 1.18x. -
Every empty graph answer says which kind of empty it is.
magus query,magus explain, andmagus refscarry ananswer.verdict:absentis a fact magus verified,unknownnames the projects it could not search and the command that fixes them. Previously both printed the same thing, so a missing symbol index was indistinguishable from a symbol that does not exist - and an agent recorded the blind spot as a fact.refsandexplainnow exit 1 onunknownand keep 2 forabsent;magus querystill exits 0 either way, because an empty result set is a legitimate answer to a search. -
Symbols carry a language even when the indexer does not report one. SCIP makes
Document.Languageoptional and scip-typescript sets it on nothing, so every TypeScript symbol landed unlabeled andmagus query language:typescriptreturned 0 whilelanguage:goreturned 22,245. magus now falls back to the language the producing spell declares - the same declaration that made the project symbol-capable in the first place. A document that names its own language still wins, since one index may span several. -
magus insight unreferenced, a sixth lens: code symbols the workspace defines and nothing in it names. It reads the knowledge graph rather than git, so it takes no history window. The output carries the coverage verdict above, which is the point - a project whose symbol index was never built contributes no symbols, so without it the lens would be most reassuring exactly where it knows least. Candidates for review, never a delete list: reflection, interface dispatch, build tags, and consumers outside the workspace are all invisible to a static index. -
vcs\dirtyDiff([paths])returns the working tree's uncommitted changes as text, on every backend. Drift gates previously branched onvcs\name() == "git"and shelled out throughvcs\cmd, so an hg or jj user got filenames with no diff. git, hg, and jj now each implement it and every one of those branches is gone. -
Markdown is formatted by dprint. The workspace had no markdown formatter after prettier was dropped. Each project carries its own
dprint.jsonextending a shared base, the waybiome.jsonalready does, and the markdown spell exposes adprintop. -
A GitHub Actions secret provider, as a third contract on the existing
spells/github/actionsspell alongside the cache backend and the CI provider. Select it withmagus\secret.provider(github)under Actions. It exists because an Actions secret is write-only - nothing inside a job can fetch one - so it does the two things the platform-neutral built-in provider structurally cannot. Anoidc:<audience>reference mints a short-lived token from the runner's own endpoint, which is the only credential on a runner that is genuinely fetched rather than injected and is what lets a repository hold no long-lived cloud key; it requirespermissions: id-token: writeand says so when that is missing. A bare reference reads the injected variable, registers the value with the runner via::add-mask::so every later step's log is masked too, and on a miss prints the exactenv:block to paste instead of reporting an unset variable. The OIDC request refuses a non-HTTPS endpoint, because an earlier step can rewrite a job's environment through$GITHUB_ENVand the request token is sent to whatever the endpoint names. -
Man pages for the three knowledge-graph verbs.
query,explain, andpathhad no entry in the man-page registry, so none of them appeared indocs/reference/manpage/, in the "See Also" list every other page carries, or in the man pages the binary ships.pathin particular - the verb that answers "how are these two things related at all", which neither of the others can - was effectively undiscoverable unless you already knew it existed. -
magus query invocation <id>, a reader for the run journal. magus recorded asecretevent for every credential a run resolved - reference and provider, never the value - anddocs/concepts/secrets.mdoffered that as the answer to "which credentials did this run touch". Nothing could read it: the invocation id magus prints asinv:resolved tomatches: 0against the graph grammar, so the audit trail was a claim with no reader.--secretsnarrows the stream to the credential reads;-o jsonemits the record. Pasting a bare invocation id intomagus querynow says which command reads it instead of reporting an empty search. -
Failure advice on a command op. A spell declares
hints = [Hint{contains = "...", advise = "..."}]; when the command exits non-zero andcontainsappears in its output, magus prints the advice through the run's own stderr, so it lands in the log and the output ref beside the tool's error rather than only on a terminal. Each stream is matched independently and only a real non-zero exit qualifies - a cancelled run advises nothing. The bundled spells declare it where a tool's message names a symptom rather than the fix:docker-buildxfor the registry-auth and daemon phrasings of BOTH docker and podman (podman commonly arrives through thepodman-dockershim, and the registry messages come from the OCI registry rather than from either tool, so those are shared); thegobuild/run/test ops formissing go.sum entryandupdates to go.mod needed; every JS op for a stale install, covering pnpm, npm, yarn and bun because each invented its own phrasing for the same fact; andcargo-clippyfor a missing rustup component. Hints are refused on service ops, where they could never fire. -
MGS1026: a cacheable target that reads a credential. A resolved credential is deliberately not part of the cache key, so rotating or revoking one invalidates nothing - and an authentication target, whose sources almost never change, then becomes a permanent cache hit that never contacts the provider and still reports success. The push that follows fails with the registry's own 401, far from the cause. A doctor check now reports the combination;
skip_cachewith a reason is the fix, and magus's ownimage-loginalready declares it.
Changed
- The
-loginconvention is demoted from "the convention" to a specific tool. It was presented as the way to authenticate, and it is a target with no inputs, no output, and no cacheability - a mode switch rather than a unit of work, and the shape that produced the MGS1026 hazard above. The documented default is now to authenticate when the tool says to, since the re-run replays from cache and costs seconds. A-logintarget remains the right answer for several registries at once, and for unattended runners where there is no human to read a failure.
Fixed
-
A shared cache dir no longer merges two workspaces' project locks. An absolute
cache.dir(orMAGUS_CACHE_DIR) resolves to the same path for every root - that is the point, one cache - but the lock tree hung off it directly, so every workspace's project.was the same lock file. An unrelated checkout then blocked on this one, and because the holder is a legitimate live process it presented as an indefinite wait rather than an error. Locks now live under<cacheDir>/locks/<workspace>/, andmagus statusreports only the current workspace's holders instead of prefixing every project path with the workspace segment. -
A re-entrant lock in a library caller hangs instead of reporting MGS3007. The diagnostic exists for exactly this - a lock held by one of your own ancestors can never be released - but invocation ancestry was stamped only at the CLI and daemon entry points. A Go test driving magus in-process had none, so the check could not fire and the acquire waited forever with the ancestry env var sitting unread in its own environment. The lock boundary now reads it when nothing upstream supplied one.
-
The root
formattarget no longer writes into descendant projects. dprint discovers a nesteddprint.jsonand formats that subtree under its own config, and neither the parent'sincludesnor an explicit--configprunes it: a bare run from the workspace root reached 164 files, 143 of them underdocs/. That is a target writing outside its project, which magus itself rejects as MGS3001, and it stayed invisible while every child project's markdown happened to already be formatted. Passing paths as argv scopes it (10 files, none in a child project), so the root call now does.dprint.jsonhad recorded this as unfixable; only the config route was ever tested. -
Breaking, and silent until now: Buzz's
str.replacesubstituted only the FIRST occurrence. Upstream Buzz replaces every one -src/builtin/str.zighands the whole string to Zig'sstd.mem.replaceOwned- so this was a gopherbuzz conformance bug, not the upstream parity it was documented as. It was invisible because the callers that care are escapers, which produce output that is merely wrong rather than failing: the docs Atom feed and HTML escapers encoded only the first&of a document, the GitHub Actions workflow-command escaper encoded only the first newline and let the runner end the command at the second, andslugifyProjectkept every separator after the first. A test comment and a project note both asserted the old behavior was correct, which is why it outlived review.libs/gopherbuzz/testdata/66_str_replace_all.buzznow pins the upstream shape. Do not add astr.replaceAll- upstream has no such method, and adding one would make gopherbuzz a superset whose scripts fail upstream silently. (pat.replaceandpat.replaceAllremain a correct pair and are unchanged.) -
Escaper tests now use multi-occurrence fixtures. Every escaper in the tree was tested with exactly one of each character, which is the shape that hid the bug above - thoroughness was being measured across characters rather than across repetitions.
-
magus run buzz-testnow runs the spell and docs-library test blocks. 142 in-filetest "..." {}blocks now execute in CI, up from 21. The rest were written, passing, and invoked by nothing.
Changed
-
Generated-file drift is measured by content, not by asking whether the tree is clean. Every gate hashed nothing and instead required a clean tree, which disarmed it at exactly the moment it was reached: the documented pre-push check runs with uncommitted work in the tree, so it printed "skipped" and exited 0. Gates now hash their paths before and after the generators run, so an unrelated edit no longer hides drift, and an uncommitted-but-current generated file still passes because its bytes do not move.
-
Go formatting is gated by golangci-lint's
formatterssection.gofmt -lreports on stdout and exits 0, and magus reads an op's verdict from the exit code, so unformatted Go passed green. Theformattarget keepsgofmt -las the local reporter. -
MGS4003 fails the run instead of warning. Determinism is what drift gating, cache replay, and regenerate-to-resolve merges all rest on, so there is no useful "warned about it" state.
--race=replayis now run weekly byaudit.yaml, renamed fromnightly.yamlbecause a workflow should be named for its purpose rather than its cadence. -
MAGUS.md's routing anchors no longer depend on local run history. The@runtimeshard records which diagnostics a machine happened to trip, and those edges fed the degree ranking, so a committed generated file differed between a developer's machine and CI. -
magus-delegate-ultracan now be reached by asking for it in plain words. It had two triggers: its own literal name, and "explicitly requests graph-planned parallel delegation" - a phrase nobody says out loud, so in practice it had one. It now also matches how people actually ask ("fan this out", "run these in parallel", "use several subagents") while keeping the opt-in sharp: wanting the work faster, sooner, or more thorough is explicitly NOT that request, because those are asks about the outcome and this skill is a choice about the method, with a real cost. The name is unchanged deliberately - it is the trigger string, and-ultraalready means "expensive, explicitly requested" across this toolchain. -
--simplenow sheds enumeration and keeps judgment, having previously done the reverse. It described itself as "the imperative steps without the rationale, for a reader that infers the why" - which is backwards for who actually installs it. The short permutation is for the most capable readers, and those are precisely the readers that can re-derive a step from-hormagus describebut cannot re-derive which failures are SILENT. It was handing its strongest reader the half it could have reconstructed and taking away the half it could not. Twelve skill bodies were re-cut against the new axis, so the short permutation now carries, in compressed form, the reasons a whole-tree revert destroys a concurrent agent's work, that a merge driver cannot finish a conflict alone, that a silent fallback hides the gap worth reporting, and that a partial inventory is a wrong answer wearing a right answer's shape. Several load-bearing imperatives turned out to be marked full-only and were reaching only half the readers; they are unconditional now. The authoring skill and the flag's own help carry the corrected framing. Skill contract v25. The old key named the absence of a behavior, so answering "can this run write?" meant parsing a double negative, and the documented CI snippet read inverted from its own intent:MAGUS_CACHE_IMMUTABLE: ${{ github.event_name == 'pull_request' }}becomesMAGUS_CACHE_WRITE_ENABLED: ${{ github.event_name != 'pull_request' }}. It gates the local snapshot and the remote push alike; restoring is still ungated, so a pull request replays the shared cache at full speed while publishing nothing to it. -
The host platform now keys the cache as separate
os:andarch:lines, each controlled bycache.include.os.enabledandcache.include.arch.enabled. They vary independently - a container image built on linux/amd64 differs from linux/arm64 by arch alone, a shell suite differs between macOS and linux by OS alone - so one combined switch made a workspace that cared about one pay for both. This replaces the per-targetplatformpolicy.
Fixed
-
A too-new tool reported MGS3005, "older than this spell supports". The version gate took a single constraint string, so one error covered every way of violating it. Two named bounds make the direction structural: below the minimum is MGS3005, at or above the ceiling is MGS3006.
-
A copied hook template can now be checked for staleness. Every shipped template carries a
magus-guard-templateversion line, and the guide says how to compare it against your own copy. It fills the one gap in the agent surface where a fix could not reach its users: an installed skill is generated and regraded bymagus graph verify, but a hook template is copied into a host's config and owned by its reader from then on, so the exit-code fix below would have been documented within the hour and absent from every installed copy indefinitely. A version rather than a checksum, because these files are explicitly yours to edit and a checksum would flag your own changes as drift. A bump is total: a test fails until every template is re-stamped, so no file is left claiming a version whose behavior it does not have. -
The shipped guard templates mishandled a denied command's non-zero exit. A deny now exits 2 with the verdict still on stdout, which the templates read as "this binary rejected the attribution flags": the two generic templates silently judged every blocked command a second time, unattributed and recorded twice in the activity trail, and the Cursor script let the blocking status escape as its own, which Cursor reads as a crashed hook and fails open on - turning every block into an allow. They now retry only when a call produced no verdict at all, and the Cursor script prints its JSON and exits 0, because Cursor's channel is that JSON rather than the status. Found by the new transport cases on the first run after the exit-code change landed; nothing had executed these files before.
-
The OpenCode guard plugin never obtained a verdict, so OpenCode sessions ran entirely unguarded. It invoked
magus agent hook, a subcommand that stopped existing when the guard moved to the top-levelmagus hook, and passed the command or path as a positional argument whenhookreads its input from stdin and rejects positionals. Both were invisible: the plugin ignores the child's stderr, so the usage text went nowhere, an empty stdout failed to parse, and its fail-open arm logged "verdict was not JSON; allowing" once per tool call and allowed everything. It now callshookand writes the input to the child's stdin. The parity check that found this one deliberately does not cover it - a glue that handles verdicts correctly but never receives one is a transport failure, and nothing executes the templates against a real event yet. -
A defined type over a basic kind crossed into Buzz as
null. Now guarded by a test that crosses every runtime boundary type, with the list generated from the same registry that emits the Buzz mirrors - so a new boundary type is covered when it is declared rather than when someone remembers. A type switch matches on type identity, not underlying type, so a field typedtypes.DoctorCheckStatusortypes.TargetRunStatematched no case and arrived as null -doctor().checks[0].statusread null rather than"ok", while the SDK guidance told callers to branch on exactly that field instead of grepping console text. Handled reflectively now, so the next defined type does not reintroduce it.
Added
-
A supported version window per tool, checked against the binary that actually ran. A spell declares what its ops need with
supported = VersionBounds{min = "1.21"}, a workspace declares its own policy withmagus.project({"tools": {"node": {"min": "22", "below": "25"}}}), and the two intersect so neither can loosen the other. Outside the window fails before the run does any work, as MGS3005 (below the minimum) or MGS3006 (at or above the ceiling); enforcement follows the declaration, so a project whose targets shell out instead of dispatching a spell op is held to its window all the same. The version already fed the cache key, so the probe was running on every build regardless; this compares its result against something you declared.minis inclusive andbelowis exclusive, both plain versions rather than a constraint range, because a range language puts a syntax between you and the two cases that matter. magus never learns which versions exist upstream and never selects one. -
magus describe tool[s], and a Toolchain tile in the console. Until now you could only see the window a build is held to by failing one, and the diagnostic named the tool that broke the rule without listing the rest. Both surfaces read the same state - the probed version, the spell'ssupported, the project'stoolskey - and report the verdict the CLI would raise for the same pair, so a page and a terminal cannot disagree. The spell's window and the workspace's stay separate columns: the first question about a failing bound is who set it, and the intersection has already thrown that away by the time a diagnostic exists. The console reads it over a newmagus.tool.v1service. A probe forks a process, so the daemon caches each reading for a minute and every row carries its age instead of implying it is live. magus still never learns which versions exist upstream, never selects one, and carries no end-of-life data. -
opts.quietonproc\exec,proc\shell, andvcs\cmd. Captures output without echoing it, matching whatmagus\cmdand friends already accepted. Read in the one path all three share, so they cannot drift into different option sets. -
A doctor check that the declared
required_versioncovers the magusfile keys in use. An unknownmagus.projectkey aborts workspace load, which takes down every command including the one that would build a binary new enough to read the file.required_versionconverts that into MGS1021, but only if somebody remembers to raise it; this asserts it instead. An unrecognized key with no near match now also suggestsmagus self update, for the binaries too old to evaluate a floor at all. -
A workspace can carry its own magus rules, in a skill magus does not ship. The installed skills teach the tool and are identical in every repo, so a rule that is true only here had nowhere to live: editing an installed copy reads as drift to
magus graph verifyand is erased by the nextmagus agent install --force, and nothing said so at the moment of the edit. A local skill beside the installed set (magus-local-developmentby convention) was already safe by construction - install writes only the names it ships and verify grades only those - so this release makes the convention discoverable rather than building a mechanism: a newmagus-adaptskill carrying the method and the per-rule stamp format (evidence, and the condition that retires the rule), the name reserved against a future shipped skill, and amagus hook --pathadvisory that fires when an agent is about to edit a stamped install. Skill contract v24. -
The reserved local-skill name is
magus-local-development, notmagus-local. The convention had not shipped in a release yet, so there was no compatibility burden to carry:LocalSkillName, themagus-adaptskill body, the.gitignoreexception, and this workspace's own.claude/skills/magus-local-development/moved together, outright, with no alias. Skill contract v27. -
A
magus-buzz-reviewskill, three lenses over a magusfile, spell, or standalone.buzzscript - idiom/style, skeptic/correctness, and upstream-Buzz conformance - fanned out in parallel and merged, the same shape go-review-ultra already uses for Go. magus-buzz teaches how to write Buzz; nothing taught how to review it, so a gopherbuzz-only behavior (namespace access accepting a dot as well as a backslash, a bareascast coercing instead of statically checking, a compound assignment double-evaluating its target) had no home to be flagged from, and a strict-mode rule applied to a magusfile - which is always parsed embedded, unconditionally - read as a real finding when it was a false one. Every finding carries one of three authority labels (UPSTREAM, GOPHERBUZZ, PORTABILITY) naming which of those three questions it answers. It does not cover magusfile/target/spell contracts; magus-buzz still owns those. Skill contract v28. -
magus-buzzno longer tells an agent thattestis a reserved word. The shipped skill's "reserved words" table conflated parse-time reservation with runtime shadowing, and was wrong on two entries:testis not in gopherbuzz'sreservedIdents, deliberately, because every magus target set definesexport fun test(...), the canonical test target - the old text told an agent that target was illegal.mapwas never reserved either; naming a local or a fieldmapshadows the builtin.map()method, which is a runtime hazard, not a parse error, and is why it fails later with a confusingnull is not callable. The passage now states the real reserved list, states plainly thattestis usable, and moves the shadowing hazard to its own line. Skill contract v29. -
magus-buzzandmagus-buzz-reviewnow teach the magusfile testing boundary.docs/guides/testing.mdalready said not to write tests for a magusfile - it is declarative configuration, and wanting one is the signal to move that logic into a spell or a sibling module - but the shipped skill never said so, so an agent following it would happily test a magusfile. magus-buzz now states the boundary and the--embeddedflag a magusfile's own imported module needs when tested (its imports parse embedded, not strict); the review skill's idiom lens adds the one-line finding and points back rather than restating it. Skill contract v30. -
magus-buzz-reviewnow records that a declared!>error set is unenforced. Upstream Buzz treats!> ErrTypeas a real error set; gopherbuzz's parser consumes the annotation and calls skipType, and no AST node stores it, so a function declaring a raise that throws compiles clean when called with no try/catch from a function declaring none. The upstream-conformance lens now says to read a!>as documentation rather than a checked contract, and not to take its absence as proof a call cannot raise. Skill contract v31. -
Host parity is now a build gate rather than a table nobody re-reads. Each guard template declares, per guard surface, how much of a verdict it can carry (
magus-guard-coverage), and the guard's own vocabulary moved into an importable contract. Adding a decision kind or a guard surface without wiring every host now failsgo test, as does a declaration that disagrees with the parity table in the agents guide. A declaration can also be sincere and wrong, so the templates are now EXECUTED as well: a testscript corpus runs the three POSIX sh templates against real host events with a real binary, and the OpenCode plugin's transport cases run under node withBun.spawnsupplied by the test, leaving the shipped artifact untouched. Both are tied back to the contract - a new decision or surface fails until an executed case covers it, or the file says in writing why that cell is unreachable. The honest remaining limit: the recorded event shapes come from each host's documentation, so a host renaming a field is still invisible until someone runs it. -
Tool readiness probes. A spell can declare
mgs_getReadinessProbes, keyed by tool, and magus checks it before dispatching an op that runs that tool.docker --versionis client-only and succeeds with no daemon, so a stopped daemon used to surface as a build failure on a project with nothing wrong with it; it now fails as MGS3004 before the op forks. Readiness never enters a cache key - it is a precondition, not an input.
Changed
-
semver\comparenow orders instead of testing a relation. It wascompare(a, op, b) > bool, answering whether a relation held. Every other library spellscompareas three-way ordering returning an integer - Go'scmp.Compareandstrings.Compare,x/mod/semver.Compare, Masterminds, node-semver - so the old signature was a trap that compiled: an author expecting an ordering got a boolean. It is nowcompare(a, b) > int, returning -1, 0, or 1. The relation form moves to the newsemver\satisfies(v, constraint), which also accepts ranges the operator form could not express, sosemver\compare(v, ">=", floor)becomessemver\satisfies(v, ">= " + floor). -
An output reference is now derived from the step's cache key, so the same inputs mint the same ref on every machine: an inspect line pasted from CI or a teammate's terminal resolves in your checkout. Ref equality becomes input equality, which is what makes the works-on-my-machine question answerable at all - if CI prints one ref and your laptop prints another for the same target, your inputs differ, and magus can now say which ones. A ref is
outplus 12 hex (out9c92fef96e60) where it wasoutplus 8, so a script, fixture, or pattern that pinned the old width needs updating. Execution identity moved down a level to per-run ATTEMPT ids, which keep the 8-hex shape - a volatile target's recent failures each stay independently addressable, and an id printed by an older magus still resolves.
Added
magus query output <ref> --attemptslists the executions stored behind one ref, newest first, and--metashows that run's identity rather than its output: descriptor, invocation lineage, cache key, and one digest per key component class.magus describe target <target> --cachecomputes the key a run would mint right now, without running anything, and--against <ref>diffs it against a stored run's key to name the exact source file, environment variable, or tool version that drifted. The verdict is key equality rather than the line list, and a mismatch exits non-zero so a script can gate on it; pass--no-default-charmswhen comparing against a CI ref, since CI runs that way. Env values never reach the store or the terminal - a key input's value is replaced by a short digest that still changes when the value does.magus query output <ref> --publishuploads a failing run's output to the remote cache as a signed bundle, so a teammate can resolve the same ref. Failures are never cached and never pushed, which is backwards from what people actually want to share, so this is an explicit act. A bundle carries no manifest and no artifact blobs, so a published failure can never replay as someone's cache hit. Passing runs still travel automatically, and their artifact now carries the run's descriptor and key inputs as well.- An unresolvable
magus query output <ref>(MGS8001) is no longer a dead end. magus sweeps every candidate target in the workspace, keys each exactly as a run would, and compares against the ref - the same predictiondescribe target --cachealready does for one target on demand. One match prints the exactmagus run <target> [project]that would reproduce it; no match says plainly that the run which printed it had different inputs (a different commit, an uncommitted change, or an environment), which is a finding, not a failed lookup.--baseplays no part in either case - it scopes which targetsaffectedtreats as changed, not what a target hashes to.--metaalso gains arev:line: the VCS revision the run's inputs were read at, with a(dirty: ...)note and arecorded at X, you are on Y.callout when it differs from HEAD - the key pins a tree state, never a commit, so this is provenance, not something to check out. A ref minted by a run that forwarded extra arguments after--still cannot be predicted, since those arguments are part of the key and a prediction has none.
Security
- The remote cache artifact's signature covers every member instead of the manifest alone: the build log and the portable-ref sidecars are authenticated, imported extras are staged until the signature clears so a rejected artifact leaves nothing behind, and a signature is bound both to the KIND of object it was made over and to the (project, cache key) it is served for. Without that binding a signed output bundle could be re-tarred as a cache artifact and replay as a successful entry, turning a published failing run into a teammate's cached pass. Artifacts signed by an older magus still verify; the extras their signature never covered are dropped rather than trusted.
Added
-
Host module calls are typechecked. Every method a host module declares now ships a Buzz
externsignature alongside its implementation, somagus\affectedImpact(base)types asImpactat the call site instead of as an unknown, and reading a field the return does not carry is a load-time error rather than a runtime surprise. The declarations are generated from the samestd.Moduledescriptors the runtime binds, so a signature cannot drift from what executes. Two methods stay untyped and say so in the generated output:fs\joinandfmt\sprintfare variadic, and Buzz has no variadic parameter to declare. -
A magusfile can read a credential through a declared provider.
magus\secret.provider("<spell>")selects the backend andmagus\secret.read("<ref>")reads one reference. Where a secret comes from is a spell's problem, so 1Password, Vault, or AWS Secrets Manager are anproc\execaway and magus grows no per-provider code; with no provider declared, the built-in one treats a reference as an environment variable name. A value is a secret because it was read through the resolver, never because its name looked credential-shaped, so magus can keep it out of what it persists: the captured output, the raw log, the output store, the journal, and every log format are redacted at their write boundary. See docs/concepts/secrets, which is also explicit that this reduces blast radius and dwell time versus a.envfile and does not make anything "secure". -
Container images are published with an SBOM and provenance, to two registries in one build. Each variant now carries an SPDX SBOM and max-mode SLSA provenance as in-toto attestations, and a single buildx invocation per variant pushes to GHCR and Docker Hub together - not one build per registry, which on a cold CI runner would rebuild every layer. Merges to main publish a per-commit snapshot image.
image-registriesreports the registry table the active charms resolve to, andimage-loginauthenticates against it. -
magus.projectaccepts"no_language", a REASON string explaining why a project binds no toolchain spell. It silences doctor's language-coverage check for a project that is legitimately polyglot (theevalsharness is the in-repo case) without inviting the check to be switched off wholesale. A baretrueis rejected: the reason is the point. -
MGS1020 reports a generated file claimed as an output by more than one target, and documents the one-owner rule for generated files.
-
magus doctorfindings come at two levels, and the split is a correction.[fail]is a workspace that is wrong however you like to work: a dependency cycle, an unparsable magusfile, two targets claiming one output.[advice]is a convention magus recommends - target naming, language coverage, spell doc comments - which is reported and exits zero, becauseciis the one target magus reserves and the rest of the layout belongs to whoever wrote it. Previously a convention check could only fail or not exist, so each one grew its own escape hatch (no_language, and brieflyallow_bespoke_name): the config surface was accumulating one key per opinion, and taking magus's advice was mandatory unless you wrote a paragraph explaining yourself. There is deliberately no flag that promotes advice back to failure; that would be the same imposition with an opt-in label. -
A workspace can declare the oldest magus that can run it.
required_versioninmagus.yamltakes a semver constraint, is checked before any magusfile is evaluated, and reports MGS1021 naming both fixes (upgrade the binary, or raise the pinned version in CI). It has to be declared rather than derived because the binary that hits the problem is the OLD one: it cannot look up which release added the module it is missing, having never heard of that release. Without it, a too-old magus fails from wherever the magusfile first touched something it lacks -import "xml": module not found, which reads like a typo. See docs/concepts/compatibility, which states what magus promises across versions and why there is no plan for a 2.0.
Fixed
- The console's service worker stops serving a stale bundle indefinitely. Its
BUILD_IDnamed the cache and was hand-written, though the comment beside it claimed the build bumped it, so a rebuilt console produced a byte-identicalsw.js, the browser found no update, and every client stayed pinned to the shell it first cached. The refresh prompt downstream was never reached because nothing upstream ever fired.BUILD_IDis now a digest of the bytes it precaches, and a tab re-checks for a new worker on boot and every 15 minutes, so an unattended display is not left on a build from days ago. - A
magus doctorfinding names the file it found. Details rendered their path withfilepath.Relagainst the runner's root and discarded the error, but the root is empty on the path the daemon takes - soRelfailed and the detail printed an empty path, reporting a target name as wrong without saying which magusfile declared it. It now falls back to the workspace root, then to the absolute path. magus doctorreports a bespoke phase-fragment target name (MGS1003) once per project instead of once per name. Collapsing every project onto the first magusfile scanned meant a workspace with three of them showed one, and fixing that one surfaced the next - the check could not say how much work was left.- The container images build again. Both Dockerfiles copied only
go.modandgo.sumbeforego mod download, but the rootgo.modreplaces two in-repo modules and the download reads each replacement's owngo.modto build the module graph. It failed withreading libs/<name>/go.mod: no such file or directory, which meant no container image had ever been published: the failure only fires on av*tag, so every release attempt died at the same step. The manifests are now copied before the download, which keeps that layer cacheable on the manifests alone. - The
_staticrelease archives and thelatestcontainer image are now actually static. Buzz's FFI provider reachesdlopenthrough purego's//go:cgo_import_dynamic, which gives the binary aPT_INTERPand alibc/libdl/libpthreaddependency even underCGO_ENABLED=0. The archive advertised as static therefore needed a dynamic loader and would not run on a musl or scratch host, and thedistroless/staticimage could not exec its own binary at all, reporting onlyexec /magus: no such file or directory. Both now build with-tags noffi(see Changed). - The sandbox no longer denies a write into a directory the run has yet to create.
A non-existent write target is normalized by resolving its parent, but when that
parent was missing too the whole path stayed lexical, so a symlink anywhere above
it went unresolved and could never match a rule path (which IS resolved). Any
workspace under a symlinked prefix - on macOS that is every path under
/varor/tmp- had nested creates denied. It now walks up to the nearest ancestor that exists and re-attaches the missing tail. - magus no longer panics mid-run on a target that fans out.
captureRunputs one pair of output taps on the context for a whole target body, andctx.needs(lint, test)- the shape of everycitarget - runs its children concurrently, so several goroutines reached the same tap. Its line buffer was unguarded, so two writers tore the slice header and the process died withslice bounds out of rangeinsidelineTap.Write. The panic killed the writer goroutine, after which the child process reported its broken output pipe asexit -1- surfacing as an unrelated-looking tool failure rather than as a crash. The shared log sink beside it already had the equivalent guard.
Changed
-
magus status -o jsonspells thebuild_infokeys in lowercase (version,commit,date) rather than capitalized. The struct carried no tags, so it was the one object in an otherwise snake_case payload that echoed Go field names. A script reading.build_info.Versionmust read.build_info.version. YAML output is unchanged, and the console reads this over protobuf rather than JSON, so it is unaffected. -
magus affected --impact -o jsonalways emitscoverageon a changed symbol, andmagus insight report -o jsonalways emitsvolatility. Both were pointers that disappeared when absent; they are values now, so a magusfile readssym.coverage.ratioandreport.volatility.targetswithout a nil guard and the Buzz mirror can declare them non-optional. A consumer testing for key presence should test the counts instead:total_stmtsof 0 means no coverage was observed, an emptytargetsmeans no run-outcome history. -
Breaking:
vcs.shortHash,vcs.hash,vcs.branch,vcs.commitDateandvcs.commitnow RAISE when no VCS is resolved or its metadata cannot be read. They used to swallow the failure and hand back""(or, forcommit, an object with every field empty), and the module reference told you to testc.date == ""to find out.That is not how a Buzz function reports a problem - upstream declares the error in the signature and the caller writes try/catch - and the sentinel could not even be trusted:
""is a value a branch name or a subject line can legitimately hold, so the check could not distinguish "no answer" from "the answer is empty". It also made the check optional, and a magusfile that forgot it interpolated an empty commit into a version string or an image tag with nothing to surface the mistake.Migration, where a missing VCS is a real case (building from a release tarball or a container context):
// before final c = vcs\shortHash(); if (c == "") { return "unknown"; } return c; // after try { return vcs\shortHash(); } catch (e) { return "unknown"; }vcs.name()still returns""when nothing is resolved, and remains the way to TEST for a VCS before asking it anything - the same split asos.envandos.lookupEnv. -
Breaking: container images are signed with cosign v3, so verifying one needs a v3 client. A v3 client reads both formats; a v2 client cannot read a v3 signature and reports the image as unverified, which is indistinguishable from a bad signature. Run
cosign versionbefore treating a failure as a compromised image.Taken now, deliberately, rather than announced later: no release has been published yet, so nobody is verifying these images with a pinned v2 client. Doing it after a release would have flipped
latestunder readers who never opted in, and the guide tells them a verification failure means "not an official build - do not run it".It also unblocks the toolchain. cosign's own 2.x releases do not publish the
cosign_checksums.txt.sigstore.jsonthat aqua verifies against, so no 2.x version could be installed through the pinned toolchain at all -mise installfailed outright, in every CI job that runs it rather than only the signing one. -
Breaking:
skip_cachenow requires a REASON string; the baretrueform no longer loads."skip_cache": truewas a flag that recorded a decision and threw away why it was made, so a target opted out of caching in 2025 looked identical to one opted out by accident, and six stale opt-outs survived in this repo alone because nobody could tell which were still load-bearing. Write the reason instead:"targets": { "release-sign": {"skip_cache": "signs the manifest per invocation; a replayed signature would cover different bytes"}, },A magusfile with the old form fails to load and names the target. This is a per-target policy about a target that must never replay; it is NOT the way to skip the cache for one run -
--no-cacheon the command line is a session-level judgment and stays where it is.docs/concepts/cache.mdcovers the distinction and the mapping from Nx'scache: false. -
Breaking: the release archives now name the static build with a
_staticsuffix and the dynamic build with the bare name, and the dynamic archives are no longer published by default - a release carries them only when the workflow is dispatched withinclude_dynamic_builds. v0.3.0 shippedmagus_<version>_<os>_<arch>-static.tar.gzbeside a bare-named dynamic build; the hyphen made<arch>-staticparse as an architecture, so the suffix is now an underscore field, and it marks the exception the waybusybox-staticdoes.This breaks a pinned URL twice over. A pin to
..._<os>_<arch>-static.tar.gzno longer resolves - use..._<os>_<arch>_static.tar.gz- and a pin to the bare name, which used to fetch the dynamic build, now fetches nothing on a default release. Everything that fetches an archive by name asks for_staticexplicitly: the install script, the download guides, andmagus self update. -
Breaking: Buzz FFI (
zdef()) is unavailable in the_staticrelease archives and in theghcr.io/egladman/magus:latestcontainer image. FFI opens a shared library at runtime, and that capability is what made those builds non-static (see Fixed); a build carrying it cannot also be loader-free. In those two artifactszdef()now reports FFI as unsupported, the same graceful degradation an unsupported OS/arch already got, rather than failing at the call.Nothing else changes. Default builds,
go build,go install, the dynamic release archives, and the-dynamicimages all keep FFI. If a magusfile callszdef(), use a-dynamicimage or a dynamic archive. In the static image the capability was unusable regardless: it ships no shared libraries fordlopento open.
Removed
- Breaking:
magusfileis no longer a spell.import "magus/spell/magusfile"and amagusfileentry in a project's"spells"list now fail with MGS1017 and the one-line fix: delete both. Neither did anything already - magus binds that driver to every project it discovers, because it is what makes a magusfile's own targets runnable rather than a toolchain an author opts into. Leaving the declarations accepted kept teaching readers thatmagusfilewas a spell likegoorbuf, which the spell reference has never listed it as. Consequences:magus describe spellsno longer lists it, andmagus lsreports the toolchain a project actually binds (or none) instead of answeringmagusfilefor almost every project - a fact true by construction, since having a magusfile is how a project is discovered at all. - Breaking:
magus memory listandmagus config mcp connector listare now... ls, matchingmagus lsandmagus run ls. The old spelling errors with a message naming the new one. - Breaking: the three vendor spells register canonical, vendor-qualified names -
actionsis nowgithub-actions,s3-cacheis nowaws-s3, and the GitLab CI provider'sciis nowgitlab-ci. A registered name is what identifies a spell in every listing and diagnostic, with no directory around it to supply context, so it has to stand alone:actionsnamed no product, andcicollided outright with theciTARGET thatmagus affected cianchors on. Source paths are unchanged (spells/github/actions,spells/aws/s3-cache,spells/gitlab/ci), so the path imports in magusfiles keep working; only the registered name moved. The reasoning is written down in CONTRIBUTING. - Breaking:
magus tailis gone. It streamed the most recent cached log for the project in the current directory - a viewmagus query output <ref>already gives from the reference every run prints. A whole subcommand, flag surface, and man page for a narrower path to the same bytes. Its retired URL is listed indocs/retired.urls.lock; no successor page, because the capability did not move. - Breaking (library callers):
magus.WithTargetNameNormalizerand thetypes.TargetNameNormalizerinterface are gone, along withtypes.DefaultTargetNameNormalizerandtypes.NormalizeCharmName. The interface had exactly one implementation and the option had zero callers anywhere in the tree, including tests, sorun.Normalizerwas always nil and the seam only ever installed the same kebab-casing sixteen other call sites reached for directly. Usetypes.Normalizefor every entity name - target, charm, or spell op. - The bundled PGO profile (
libs/gopherbuzz/default.pgo, thecmd/magus/default.pgosymlink, and thepgo-generatetarget) is gone. A profile that has to be regenerated by hand after hot-path changes is stale more often than not, and it madego buildandgo testdisagree about how the same package was compiled. - The
assume_interactiveconfig key (MAGUS_ASSUME_INTERACTIVE,--assume-interactive) is gone. It existed to lift the TTY gate onmagus tailandmagus x, and did not earn its place on either. Forxit never reached a working state: past the outer gate the picker hit its own TTY check and failed anyway, so the escape hatch only moved the error later. Fortailit was a workaround for a gate that was too broad, andmagus tailhas since been removed outright (see above). Nothing replaces it; if you set it inmagus.yamlit is now inert.
Changed
- Breaking: built-in spells are named for what they adapt.
tsis nowtypescript,rsisrust,pyispython,mdismarkdown.gois unchanged - Go's name is Go. Updateimport "magus/spell/<name>"and thespells:list inmagus\project; the handle an import binds changes with it, sots["tsc"]becomestypescript["tsc"]. Op names are untouched (cargo-build,pytest,markdownlintalready named their real tool). An unknown import still suggests the right spell, and the alias table now holds only genuine synonyms -javascript,js,node,nodejs,cargo,python3- rather than apologizing for abbreviations. - Breaking: spell op names are normalized when the spell is decoded. Op keys are
validated against a charset that admits
_and uppercase, but every request arriving at dispatch has already been kebab-normalized, and dispatch is a map lookup - so an op authoredgo_buildwas stored undergo_build, looked up asgo-build, missed, and swallowed as a fan-out skip at debug level. Declared, and reachable by nothing. Every built-in already used kebab keys, so bundled spells are unaffected; a workspace-local spell with acamelCaseorsnake_caseop now works instead of silently never running. - Breaking (library callers): the
types.Describermethods return slices instead of a{definition, count, items}envelope.DescribeSpells,DescribeCharms,DescribeTargets,DescribeFiles,DescribeWorkspacesandDescribeTargetnow hand back[]SpellEntry,[]CharmEntry,[]TargetEntry,[]FileEntry,[]WorkspaceEntryand[]EvaluatedTargetEntry.Definitionwas a package constant andCountwaslen(), so every call site that filtered had to reassignCountby hand - a denormalization one forgotten line shipped as a wrong count. The JSON shape ofmagus describe ... -o jsonis unchanged; the envelope is rebuilt at the render edge.DescribeProjectsandDescribeEvaluatedProjectskeep a struct, because both carry a realWorkspacefield.host.ModulesOutputis nowhost.Modules. magus describe spellreports how to reach a spell and what it adapts: animportline you can paste (import "magus/spell/go";) and the source language it adapts.SpellEntrycarries the import path asbuzz_importin-o json. It is a path, not a handle: spell imports are read statically to build the target graph, so a spell reached any other way would lose its edge without failing.magus describeover MCP serves every noun the CLI does.charms,graphandmoduleswere CLI-only, so an agent could not discover what charms exist, could not see the target graph, and could not introspect the Buzz stdlib at all.- A non-canonical target or charm spelling now prints a one-time hint naming the
canonical form (
magus run goBuild->target "goBuild" is canonically "go-build"). Silent when you already wrote the canonical form. - Suggestions are case-insensitive.
magus run build APImissed projectapiand got no suggestion at all, becauseAPI->apiscored three edits against a threshold of two. Project paths still resolve exactly - they are filesystem paths. - The sandbox passes every
GO*variable through (sandbox.env.passthrough: ["GO*"]in this repo'smagus.yaml). A variable that shapes compilation but does not reach the compiler does not get ignored, it splits the build cache:GOEXPERIMENTreaching onegoinvocation and not another produced a linkerfingerprint mismatchthat looked unrelated to anything. - Knowledge-graph schema v7. No node or edge shape changed: the bump is because shard fingerprints are now computed by streaming fields into SHA256 rather than by hashing marshaled JSON, so every fingerprint VALUE differs from a v6 store's. The manifest check treats a version mismatch as a full rebuild, which is the whole migration. The old approach marshaled each shard purely to hash the bytes, putting an encode on the hot path of every magus command (fingerprinting all shards costs 757 ms at 50k projects) and coupling the fingerprint to the storage format, so a future format change would have silently invalidated every cached shard. Measured: -44% sec/op, -23% B/op, -41% allocs/op.
- Host methods that return a record now say so in their signature:
magus\cmd(args, [opts]) -> ExecResultwhere it previously readmap[string]any. Nineteen methods across nine modules were affected. Annotating the named type (final r: ExecResult = magus\cmd(...)) makes the checker verify field access, turning a typo from a runtime nil into a load error; that already worked and was simply undiscoverable.
Added
-
Agent skill version 22. Skills now render their full and simple permutations with standard-library
text/templatebranches ({{if .Full}},{{else}},{{if .Simple}}) instead of private HTML-comment markers. Installation now fails loudly for malformed template syntax, while a parse-tree guard keeps bodies limited to deterministic wording branches. -
magus\normalize(name)canonicalizes any entity name from a magusfile - the same function targets, charms and spell ops resolve through. It is also live in the browser playground, so the name-normalization docs run their examples rather than asserting the rule. -
The playground says when a
test "..." {}block was not run. Buzz test bodies execute only undermagus buzz -t, so evaluating one in the playground was a silent no-op: a deliberately failing assertion looked exactly like a passing one. -
Agent skill version 19.
magus-architecturenow surveys for what is too THIN to justify a boundary, not only what is too big. Every existing lens (god nodes, hotspots, affinity, ownership) detects something central, hot, or heavily coupled; none detect over-abstraction, which is the more common failure early on. The skill names the cost no metric records: in Go every package boundary forces an export, so splitting files into packages to organize them widens the public surface you were trying to keep small. It flags three shapes - imported only from inside its own subtree, one importer with nothing encapsulated, single file with a single exported symbol - and says explicitly that SIZE is not one of them, because a small package that hides four helpers behind one function is earning its keep.Known gap this does not close: the graph has no
packagekind, so it cannot answer "who imports this Go package" directly. Its finest structural rung isproject(a magusfile-bearing directory) and the next isfile; Go's unit of encapsulation sits between them and is unmodelled. Mintingpackagewithimportsedges would make the first shape above a one-line query instead of a manual read. -
ctx.updates(...), a third per-target footprint declaration besidectx.inputsandctx.outputs, for a file a target EDITS rather than produces: a hand-written page with a generated region between markers, a manifest a tool rewrites in place. magus never deletes an update (magus cleanskips it) and never replays one from a cache snapshot, because the bytes it produced are only part of the file. It folds into the cache key like an input, so editing the prose around a generated region invalidates the target that maintains that region - which declaring the file an output could not do, since an output is excluded from its own source hash. It infers no ordering edge in either direction; declarectx.needsif you need one.This closes a real data-loss path.
docs/concepts/spells.mdanddocs/concepts/knowledge.mdare 355- and 570-line hand-written pages carrying a small generated region, and both were declared inctx.outputs:magus clean docsdeleted them whole, and the nextcontent-generatedied withinject spell list: open concepts/spells.md: no such file or directory. Only git made that recoverable. Both are now declared withctx.updates.magus clean's help no longer describes what it removes as "regenerable build artifacts" either - that was the declaration's claim, not something clean verified. -
magus agent install --simpleinstalls a shorter permutation of every agent skill: the imperative steps with the rationale withheld, for a capable model that infers the why and would rather spend the context on the task. Both permutations are hand-authored from ONE source body (an author brackets the withheld spans), so they cannot describe different behavior and they share one content digest -magus graph verifyreports staleness the same way whichever is installed, and the file's stamp recordsskill-variant. Across the eight skills the short form is 14% smaller. The docs site now reproduces every skill in both forms with a size comparison (docs/reference/skills/), generated from the embedded bodies so it cannot drift from what install writes. -
The
magus-changesskill now serves three outputs rather than one: the evidence-backed brief it already wrote, aCHANGELOG.mdentry in this file's existing Keep a Changelog shape, and per-question granular diff commands - all answered through magus surfaces (graph diff,describe file,affected --impact/--explain) rather than a raw diff. -
Shell completion now offers the target names this workspace actually declares, read from
magus describe targets, instead of eight names baked into each script; zsh and fish also show each target's kind (canonical, or the spell providing it). Falls back to the built-in set outside a workspace, wheredescribecannot answer. -
magus buzz --workspacegained a line editor: arrow-key history, line editing, and Tab completion drawn from magus's own surfaces - meta commands, the session's user globals, host modules and their methods (fs.writeF<TAB>), and the workspace's targets and projects. A piped session is unchanged. It also pins a one-row status footer showing the active language, the working directory, and the parser's continuation depth. -
File authorship is now first-class in the graph (schema v6): an
authornode per git contributor withauthorededges to the files they touched, soexplain author:<name>shows what someone maintains and it can be set against a file's declared CODEOWNERS owner (the emergent maintainer vs the owner of record). The edges are uncapped - bounded only by theknowledge.vcs.max_commitshistory window, not an arbitrary per-author limit - so a solo maintainer's full authorship is a fact the graph teaches, not a summary it hides. Extracted from the same git-history scan (author facts in the graph; aggregate analytics stay in insight). Setknowledge.vcs.authorship: false(envMAGUS_KNOWLEDGE_VCS_AUTHORSHIP) to keep only the per-filevcs_*attrs and omit the author node/edge layer; on by default. -
File nodes now carry
vcs_last_author(the last commit's author) alongside the existingvcs_last_commit/vcs_last_modified/vcs_commits, so a file's EMERGENT maintainer (who actually edits it) can be set against its DECLARED CODEOWNERS owner - a gap a pure code-graph cannot see. Captured from the commit history magus already scans. -
Knowledge graph indexes the build I/O layer and authored markdown (schema v5). Each target's declared
magus.outputs/magus.inputsbecomes aproduces/consumesedge to the file and doc nodes it matches, so a generated file is self-labeled by its producing target (explain doc:docs/spells/go.mdshows "produced by content-generate") and you can walk a target to exactly what it writes; a per-glob fan-out cap keeps a broad declaration from turning a target into a god node. Separately, every authored markdown file workspace-wide (README, AGENTS.md/CLAUDE.md, CHANGELOG, SKILL.md, ...) is now adocnode carrying aroleattr from a universal filename convention and acontainsedge from its project, soquery "kind:doc role:agent"finds the agent-instruction files in any repo. -
Knowledge graph gains build and runtime dimensions: each spell op now carries the base argv it runs (an
argvattr) anduses atoolnode for the program it runs, soexplain tool:golists every op that runs go andkind:toolis the workspace's toolchain inventory - a target reaches its tool via its existingtarget --uses--> opedge. Plus testcoveragewith atest_refscount folded onto file and symbol nodes from the coverage profile magus already produces, andmagus refsnow returns the definition'sfile:line. Query recipes: the knowledge graph. -
daemon.enabled(flag--daemon-enabled, envMAGUS_DAEMON_ENABLED, default true): set false to run each invocation self-contained in its own per-process pool instead of discovering and adopting the sharedmagus server startdaemon - handy for a worktree that should not touch a shared daemon. Recursivemaguscalls still forward over a per-process socket to share the concurrency budget; only the shared daemon is opted out of. -
Self-documenting output templates: bare
-o template(no body) lists the command's output fields - the json keys usable in-o jsonand-o template, with each field's type, drilling into nested types. Works for every structured command (the field list is reflected from the output value, no per-type registration). Previously an empty template was an error. No new command or format: it rides the existing-o templatesurface. -
Spell authoring kit:
magus init spellscaffolds a spell,magus buzz -truns a spell's in-file test blocks, andmagus buzz lspserves diagnostics and completion to an editor over stdio. -
buf-breakingop in the buf spell: gates a proto schema against a baseline branch, composable into alinttarget. See Breaking changes. -
describe target --explainprints the charm trace behind a target's resolved command, so a stacked argv patch is inspectable before a run. -
Silent-failure diagnostics: an invalid charm patch (MGS6001), a
has_charmtypo, a spell that binds zero ops, and an unknown project name now report a coded, actionable error instead of failing quietly. -
Interspersed global flags:
magus <command> --verboseandmagus --verbose <command>now parse the same way. -
magus describe charm[s]inverts the charm index: it lists every target that declares a charm and the argv edit it makes, marking the reserved built-ins and workspace defaults. -
Charm conflict detection: when two active charms edit the same argument, one silently overrides the other (the winner decided by name order), so magus warns that the losing charm has no effect at run time and flags it in
magus describe target ...:a,bbefore a run. Disjoint edits never trip it. -
magus describe targetdescribes a service op before it runs: its readiness probe, stop command, idle window, whether it is shared, and its dedup fingerprint. -
magus graphis the home of the workspace's graphs as objects:graph depsemits the project dependency DAG (the standalone form ofrun --graph/affected --graph, which remain),graph exportemits the merged knowledge graph (-o jsonnode-link, or the new-o graphmlfor external graph viewers), andgraph statsreports its shape (god nodes, orphans, doc coverage;--kindto scope). Thequery/explain/pathretrieval verbs are unchanged.
Fixed
magus.Open(ctx, root)works again for library callers. The literal-argument rule overctx.inputs/outputs/updateswas scoped on a per-targetskip_cachepolicy, but policies are only populated once the interpreter has evaluatedmagus\project()- which a bare library caller never does. So every target read as cacheable and a magusfile the CLI loads fine was rejected. The rule now splits by declaration kind: footprint declarations stay a hard error, execution overrides (ctx.withEnv,ctx.withCwd) do not.magus run --dry-run <target>:<charm>takes the same charm branches as the real run. The tracer normalized the target half of atarget:charmreference but not the charms, and comparedhas_charmraw - solint:no_cachetraced un-charmed while the reallint:no_cacheran charmed. The tracer's whole premise is fidelity to the run it predicts.- The docs site no longer walks into a nested
node_modules. Generated directories were skipped only as exact children of the docs root, so once a sub-project underdocs/had its dependencies installed, the render began emitting every dependency'sREADME.mdas a page - an unbounded render that also wrote into a descendant project (MGS3001). - Forwarding to a daemon of a different build no longer warns. A version/protocol
mismatch means the daemon is alive but will not adopt a mismatched client, so the
command now falls back to local execution quietly (a debug line, not a
[warn] proc forward failedline). This is routine when multiple worktrees run different builds against one shared per-user daemon. - A workspace-local Buzz spell could not declare a service op: the host-registered
magus/targetmodule omitted theServicetype (present only on the dry-run host), soService{...}failed to compile. Both hosts now register it.
Changed
- The knowledge graph's git-history (
@vcs) scan is now cached through the standard shard store - keyed by an input fingerprint (HEAD + window + schema) recorded in the manifest - instead of a bespokevcs-inputs.jsonsidecar. The expensive scan runs only when HEAD or the window actually moves; an unchanged tree reuses the shard from disk with no extra serialization. The window (knowledge.vcs.max_commits, default 1000) bounds the scan so it never walks a whole monorepo's history. magus explainandmagus pathnow render as compact natural-language text by default, for both the CLI and the MCP tools: an edge's direction is folded into a verb (used by,depends on,part of,required by), edges are grouped by that verb with a count before any multi-item list, and full node IDs are listed - so one rendering serves humans, agents that read, and the docs. This replaces the<--uses-- op:go:go-build [op]adjacency notation, which made the reader invert the arrow, and the verbose JSON the MCP tools returned (roughly 4x the size).-o jsonremains the structured form for agents that parse.- Breaking:
-o template=<go-template>now renders against the JSON-normalized value, so template field names are the json-tag keys ({{range .projects}}{{.path}}{{end}}), identical to what-o jsonemits, instead of the PascalCase Go struct fields ({{.Projects}}/{{.Path}}) it exposed before. This makes-o jsona faithful reference for authoring templates. Numbers arrive as float64 (coerce withintbefore numeric comparison);joinnow accepts any list, not just[]string. - Breaking:
magus describe knowledgeis nowmagus graph export, andmagus insight structureis nowmagus graph stats; the old spellings error with a pointer to the new home.insight reportstill embeds the graph-stats section, renamed fromstructuretograph_statsin its-o json/yamloutput (theKnowledgeStatsschema itself is unchanged). magus buzz lspreplaces the top-levelmagus lsp.- Local spell imports resolve workspace-root-first with walk-up accrual; a name
collision between an ancestor and a descendant spell is flagged (MGS1002) and
suppressed only with an acknowledged
spells.allow_shadowreason.
[v0.3.0] - 2026-07-25
See the full changelog at https://github.com/egladman/magus/compare/v0.2.1...v0.3.0
[v0.2.1] - 2026-07-19
See the full changelog at https://github.com/egladman/magus/compare/v0.2.0...v0.2.1
[v0.2.0] - 2026-07-18
See the full changelog at https://github.com/egladman/magus/compare/v0.1.0...v0.2.0
[v0.1.0] - 2026-07-05
Added
- Playground: an in-browser CodeMirror editor with live diagnostics, module and symbol autocompletion, hover docs, and call-signature help, backed by the WebAssembly interpreter; a collapsible notice lists the host modules the browser cannot run.
- Docs site: first-class
/blogsubsystem with reverse-chronological listing, breadcrumb root, per-post edit links, and Blog nav item. - Docs site: two Atom 1.0 feeds —
/public/atom/blog.atom.xml(posts) and/public/atom/releases.atom.xml(releases, derived from this file). - Docs site: nested Apache-
mod_autoindex-styled/public/tree with an autoindex helper — hub at/public/, feeds at/public/atom/, release artifacts at/public/release/.
Changed
- Docs site: extensionless URLs everywhere (
/documentation/,/modules/fs/); the authoreddocs/manpage/gen/path segment is flattened out of public URLs. - Docs site: nine flat client scripts collapsed into a two-file esbuild bundle
(
theme.jshead-critical,main.jsdeferred module). - Docs site: nav link "GitHub" moved to the footer, relabeled "Source Code".
Fixed
- Docs site: mobile TOC becomes a slide-up bottom-sheet instead of stacking above the article; page toolbar reflows so search fills its row and "Suggest an edit" drops below.