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]
See the unreleased changes at https://github.com/egladman/magus/compare/v0.2.1...main
Changed
cache.immutableis nowcache.write.enabled, inverted. 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 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
- 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 anos\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 WITHOUT a suffix, and the cgo build with
-cgo.magus_<version>_<os>_<arch>.tar.gzused to be the cgo build, and the static build carried-static; the container tags said the opposite (lateststatic,latest-cgocgo), so one release described the same default two opposite ways. Both now follow the image convention.This renames what people already download rather than changing which build they get: the install script has always defaulted to
VARIANT=staticand the download guides have always linked the static asset. It does break a pinned URL. A pin to..._<os>_<arch>-static.tar.gzno longer resolves - drop the suffix - and a pin to the bare name now yields the static build instead of the cgo one.VARIANT=cgoselects the glibc build from the install script. -
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 cgo release archives, and thelatest-cgoimage all keep FFI. If a magusfile callszdef(), use the cgo image or a non--staticarchive. 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 (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.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.