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.
Entries for the next release wait as one file each under changes/unreleased/.
[Unreleased]
Added
magus affected cisizes its gate to the change. Every changed file gets a tier (trivial, mechanical, scoped, full) with its evidence. Below full the gate runs only the drift check and lint, the targets that declare a changed doc, ortestwithgo-testnarrowed throughgo list. A trivial change exits 0.--no-redundancy-checkruns the full gate.AncestryReporteranswers whether one revision reaches another. All four backends implementIsAncestor.- The agent guard refuses a backtick command substitution. Inside double quotes a
backtick runs a command, so a literal backtick in a pattern pairs with the next one and
swallows everything between, file operands included. The deny names the fixes:
$(...)for a substitution, single quotes for a literal backtick. - A target holding two or more slots is a GNU make jobserver.
make, cargo, and other clients of the protocol it runs share the target's slots instead of choosing a width of their own. A target that declares noslotsis unchanged. broker: required | best-effort | offdecides what a run does without a broker.requiredrefuses a step (MGS3022, exit 69),best-effort(the default) runs unarbitrated and says so once, andoffnever starts or contacts one. Also--brokerandMAGUS_BROKER; Go callers passmagus.WithBrokerormagus.WithBrokerPolicy.- The broker can run under systemd or launchd.
magus broker unitsprints the units; magus never installs them. Under systemd the broker serves the socket the supervisor hands over and refuses a malformed one or one bound anywhere butbroker.sock.magus broker --idle-exit 0keeps it up for a supervisor. - BZZ1008: a redundant import alias is refused in magusfiles and embedded Buzz.
import "path" as alias;errors whenaliasrepeats the default binding, forspells/,project/,magus/spell/<name>andbuzz:imports; a file import's alias isolates it, so it is exempt, as isas _. The root magusfile and built-in harness spells dropped their redundantas codex,as cursor,as opencode. - The cache refuses an entry whose hashed inputs moved while the target ran. The run stays green; only the entry is skipped, locally and remotely.
- The cache is two tiers under standard two-tier semantics. Reads go local, then
remote; a remote hit is verified and promoted into the local tier; a build is stored in
both, each under its own gate.
cache.remote.write.enabledgates the remote tier: unset, it is written when a signing key is held;truemakes remote writes required. - Claude Code MCP tool calls reach the guard. A
mcp__magus__.*PreToolUseentry forwards the call envelope. No rule judges MCP calls yet. - A command rule sees where a line runs and which binary judges it. The request
carries
dirandworkspace, each command itspathwhen the line names the program by one, andmagus\guard.binary()returns the hook binary'spathand thestampits build linked in. concurrency_profilesets build width relative to the machine.conservative(half the cores),balanced(min(cores, 8), the default) oraggressive(every core), also as--concurrency-profileandMAGUS_CONCURRENCY_PROFILE. An explicitconcurrencyoverrides it.ctx.narrowed()tells a target body a sized gate narrowed its tests. A check over the whole suite, such as a coverage floor, can stand down when only the packages a change reaches ran.magus run --stdinruns a savedaffected --plan;--shard <id>runs one shard. The plan now carries atargetkey. A malformed plan, an unknown shard, another target or a disagreeing--n-shardsexits 2 (MGS3029). Under--dry-runit runs nothing and renders the plan through-o. Without--stdin,--shardstays a label.- The doc-section advisory carries the reader's own query instead of a
<terms>placeholder. - Each spell's install op is named binary + capability, one per binary.
typescript's
pnpm-installandnpm-ci, go'sgo-mod-download, python'suv-sync, rust'scargo-fetch. A project composes the one it needs into its own top-levelinstalltarget, whichbuild/test/lintneed;:updaterewrites the lockfile as before. An unchanged install replays without forking the package manager. macOS seedsnode_modulesfrom a sibling checkout. - Every host delivers
adviseverdicts; three rehydrate after compaction. Codex receives advisories and wiresSessionStartoncompact, Cursor's write guard moves topreToolUse, and OpenCode joins advisories bycallID. Guard template v12: re-copy installed copies. - Every record says which OS account wrote it, and through which entry point. Trail
events, session records and job rows carry
user,uidandentry_point(cli,hook,mcp,rpc,daemon), read by magus itself.magus sessiongains a USER column. A job row'sregistered_byis anOrigin. - Every wait and kick-back carries a code. Verdicts, events and
kick_backname one of a closed set (WAIT_NOT_APPROVED,KICK_CONFLICT, ...) beside the files at issue, and the GitHub provider writes them under its comment for a workflow to read. - The
extendedcharm, for tests that need more of the host than the gate does. A function target reads it withctx.hasCharm("extended"). This repository's cgo compression and shell completion tests now run only under it. - A failing remote tier degrades the run to the local tier. The first failure is reported and counted as failed, never missed, and the run stops asking.
- The agent guard refuses a filter that nothing feeds. A
grep,sed,jq,head,tror similar with no file operand, no pipe into it and no input redirect reads the harness's stdin, which can hang past the tool timeout. Each tool's flags are modeled, and a call the guard cannot classify passes. - A
flagshost module andmagus buzz --check.flags\parsereturns{values, positionals, unknown}.--checkparses and type-checks without running; add--embeddedfor magusfile code. - Generated bytes reach a branch only from the base's own regeneration. Apply runs it where the build tool proves a change touches none of the generator's code, and kicks the change back otherwise. Generated means a declared output, and a review covers a merge of the base only when that regeneration reproduces it.
- Git hooks written in Buzz, kept under version control.
spells/git/hooks.buzzinstalls a shim for each<hook>.buzzin a directory you name, honoringcore.hooksPathand linked worktrees;removedeletes only its own. It writes only underrw, and a hook it did not write is an error. - File nodes carry
linesandbytes. Buzz sources and the files a SCIP index defines symbols in are sized from the read that already indexes them, somagus explain file:<path>says how long a file is.explainon afile:ordir:ID now loads the symbol shards. Knowledge-graph schema v15. magus refs <symbol> --definitionprints where a body starts and ends. Each definition reads aspath:start-end, checked against the file:verifiedwhen it predates its index,changed(exit 1) when the name left the start line.--sourceadds the lines. A missing end line is said, never guessed.- The guard advises on reads outside the session's focus. Focus is the working
project, its dependencies and nested projects. Under a bound lease it denies, and the job
row's
read_pathsgrants reads without writes.magus describe filereportsfocus. - The guard advises on a split
magus run(split-run). One target run on two project sets, on one line or in two calls within ten minutes, gets the combined form, once per session. A charm is part of the target, solintandlint:rwnever combine. - The guard refuses a spawn brief that teaches a denied command (
brief-command). Each fenced shell block and inline code span in an Agent or SendMessage brief is graded like a shell line; a line naming a command to forbid it is passed over. - The guard denies a line that ends by printing an exit status (
exit-status-echo).cmd; echo "rc=$?",printfforms,rc=$?; echo $rc,cmd || echo "failed $?"and an echoed${PIPESTATUS[...]}are refused: the echo exits 0, masking the failure.exit $?,[ $? -ne 0 ],&&chains and redirected echoes pass. - The guard denies a retired or misspelled MAGUS_ variable (
unknown-env).* ANAME=valueprefix,env NAME=value,env -u NAMEorexportof a name magus's own startup would refuse (MGS1046) is denied with the same message, naming the replacement or the closest real name. search-translationdenies a search a graph query provably answers. A pattern matching only MGS codes, every Markdown heading searched, or only magusfile target declarations is checked against the graph's ids and names the exact query. The guard reads those ids fromguard.idx, written bymagus graph build, within 150 ms.- A write path can claim one declaration of a file.
run.go#executeStageslets two jobs start on one file: claims on different declarations do not overlap,magus ls jobslists the pair asclaims: disjoint, and dropping a claim releases that declaration's own lines.magus job forkrefuses a claim nothing can grade (MGS3031). - The agent guard refuses an edit into another job's claimed declaration. When the
hook payload carries the replacement, the guard applies it in memory and places the
changed lines as the job footprint does;
claimed-declarationdenies a leased edit landing in a declaration another live job claims. A whole-file write stays graded by path. - Diff drivers in the managed
.gitattributesblock, with funcname patterns for TypeScript and Buzz.magus doctorreports a missing one. - A job's footprint.
magus job waitnames the declaration each changed line lands in, found with git's own funcname patterns and measured from the merge base, andmagus ls jobscompares footprints when it reports overlapping write paths. Lines above a file's first declaration land in its(preamble). - Job rows and results carry
schema_version. An unknown version is rejected by name rather than field by field. Rows recordregistered_by. magus job waitgrades a job's footprint against its declaration claims. For a file the job claims only by declaration, each declaration its diff touched that no claim names is a violation. A doc comment now belongs to the declaration below it, and a Govar,constortypeis a declaration of its own.- A lease's declared boundary is enforced. The guard denies writes outside
write_pathsor insidedeny_paths, any write by aread_onlyrow, andciwhenchecknames a narrower target. magus affected --planprintsaffectedandunbounded_by. The merge queue partitions by them.magus describe jobderives a holder's terms;magus job waitgrades the result. Terms add reached projects, output globs, sibling holds and hidden coupling.waitchecks paths, descendants and a passing run of the row'scheck, exiting 1 on a rejection and 2 when it cannot answer;magus job exit --schemaprints the result contract.magus diffbrings the symbol index current before it reads it. Each touched project'ssciptarget replays or rebuilds; when it cannot (a missing indexer, cache writes off), the review carriesMGS7003in place of conformance findings, and the PR section fails its step instead of reading as clean.magus diffreports where a changed symbol departs from how the workspace declares the same kind of thing: a missed naming pattern, a name a target already has, a rename's leftover old name, or reversed parameters. Each is aCheckonDiffSymbol.checksstating the workspace's own counts, gated by--conformance-min-cohortand--conformance-min-share.magus doctorreports whether a bound lease is enforced (bound-lease).magus\guard.commandregisters a workspace shell-command rule. One Buzz function sees every agent shell command the built-ins pass, as parsed programs, and may deny or advise, never lift a built-in deny. It fails open and is held to the committed copy asmagus\guard.spawnis. Both rules now returnGuardVerdict(wasSpawnVerdict) and shareonce/count.magus\guard.spawnregisters a workspace spawn rule. One Buzz function sees every subagent spawn and continuation and may deny or advise, never lift a built-in deny. An uncommitted loosening waits for a commit. Policy changes land on the trail asguard_policy; MGS1045 refuses a bad registration. magus ships no rule.magus\guard.writeregisters a workspace file-write rule. One Buzz function sees every file an agent writes through its host's edit tools, with the text the host says it writes, on the same strengthen-only, fail-open, committed-copy terms asmagus\guard.command. A command rule judginggit pushalso gets the checkout's branch and its remotes' branches, read through the VCS driver.magus job forkdeclares a job from the terminal. Flags cover one row (--criteria,--write-paths,--read-paths,--check,--model,--read-only);--stdintakes a full record and--schemaprints its contract.magus mcpserves MCP over stdio. An agent host registers{"command": "magus", "args": ["mcp"]}and gets every tool for the workspace it launches in, with no daemon and no token. Each call carries astdiocredential holdingmcp=write, and every MCP tool call, over either transport, is refused below that with MGS9015.- The magus-multi-agent skill adds a coalescing rule and a typed brief and report.
- A
pipeBuzz module makesmagus buzz <script>a stage of a magus pipe. A script reads the records of the run before it withpipe\more,pipe\nextandpipe\all, andpipe\emitwrites records for the stage after it, somagus run test . | magus buzz failures.buzzreplaces| grep FAIL. Its exit status counts like any stage's. - Magus stages in a pipe trade typed records, and projects flow forward. A run
whose stdout another magus reads writes its
-o jsonlrecords there and its prose on stderr, somagus run format libs/x | magus run lint | magus run testruns all three on libs/x. Named projects still win; an explicit-okeeps its format. magus queue describeprints theghcommands that finish setting the queue up.--app <slug>adds the steps for the queue's own GitHub App, whichsetup-magusturns into a token. magus runs none of it.applyrefuses a status pinned to another integration than its token's (MGS3019).magus queue describeprints what the provider supports. That includes its queue label and who commits the queue's update commits. The newmerge-queuepull request advisor, off by default, reads it to name the label that queues an approved pull request.magus queueis a merge queue;magus vcs queueis gone. Itsls,plan,validateandapplyread JSON and report JSONL. Validation runs changes' code with read access only; apply rebuilds each candidate and merges it with the change's own method. The code isinternal/queue, its contract and mocks in itstypespackage;--factsserves other build tools.magus runandmagus affectedtake--preflight <target>[,<target>...]. The named targets run first across every selected project; a failure stops everything, exits 3 (MGS3020) and names the target, projects and fix. A green pass is not repeated. A name outside the invoked target'sctx.needsclosure is refused, exit 2 (MGS3021). Withaffected --plana red pass prints no plan.magus session --briefrestores a session's state after history loss. Branch, unpushed commits, classified dirty tree, live leases and last failing targets, also as-o json. Themagus-rehydrate.shtemplate wires it to session start.magus session hintsreports uptake per suggestion id.magus session lsand--briefshow each provider's published prompt-cache window.magus\skills(opts)returns the skills a workspace offers an agent. Each entry carries its name, description, source (shippedorlocal), form, the body an agent loads, and whether the installed copy is current.opts.nameselects one skill andopts.formpicksshortorfull, so a script can hand a worker its skills from magus rather than from pasted files.magus_buzzMCP tool runs a Buzz program. It forksmagus buzzwith inlinescriptor a workspacepath, plusargsandstdin, and returns stdout, stderr and the exit status, with JSON stdout parsed. A compile or runtime error is a tool error. Every call needswrite=true:magus buzzhas no read-only mode.- MCP and the Connect APIs on
server.sock, no token needed. A same-user peer holds thesocket-peercredential,mcp=writeandconsole=write, each path held to its loopback need. Every MCP tool call, on any transport, checksmcp=writeagain (MGS9015). - A merge's kept generated files regenerate after it finishes. The merge driver records
the owed target in the git dir, and
post-merge,post-rewriteandpost-commitsubmit aregenerate-owedjob that runs each once, deepest project first, and stages the result; it prints the amend command and never amends.magus doctorreports an unsettled record (owed-regeneration). - The merge queue merges stacked changes. A change carrying another queued or merged
change's head is stacked on it: it merges after it, its own delta measured from that
head, and waits without blame when the one beneath is kicked back. On GitHub a
merge-queue: <method>label on a stack's top queues the stack. MergeStarter.StartMergetakes the identity to act as. A merge started for a machine no longer depends on the box's configured git identity.- MGS1021 from a binary built from the checkout it cannot load names the way out. Such a binary cannot rebuild itself, so the error names the source link to run instead.
- MGS1028 surfaces where it costs. The run that reruns an undeclared seeding file names
it before starting (
undeclared_seedsin-o json), and an undeclared build input rings the console's notification center. - MGS1037: a tool's observation keyed as its version.
magus doctorrefuses one command declared as both version and observation probe without a narrowingkey. - MGS1038: a removed
magus.projectoption stops the load. It names the key and the fix. An unrecognized key is still only ignored. - MGS3013: a slot pool that cannot free a slot is refused within seconds. The refusal names every holder and what it waits on.
- MGS3014: a newer gate supersedes an older one on the same tree. The earlier
cirun cancels and exits 75 (EX_TEMPFAIL). Sibling worktrees and non-ciruns are refused immediately instead, like any other contention. - MGS4008: an unschedulable composed step is refused before it runs. Two targets in one
step, one writing what the other reads with no
ctx.needspath between them, fail at derivation.magus doctorchecks every composed target. - The merge queue and the merge driver settle low-risk conflicts. A conflicted file
settles when every region both sides changed does, and magus's change classifier
classes the edit low risk, or
merge_low_riskopts its code in. Each region is namedpath#declaration. git, hg and Sapling run the driver while merging; jj throughmagus vcs resolve. magus queue validate --remote-cache-readgives hooks the signed remote cache. The queue serves the GitHub Actions cache service to hooks through a loopback proxy that forwards lookups with the runner's token and refuses every write. Hooks get a stand-in token, the base's trusted keys and remote writes off; without the runner's credentials or a trusted key, validate refuses to start.- The merge queue leaves a change whose own code regenerates its generated files to a
person. It kicks it back with
KICK_REGENERATION, labels itmerge-queue: needs regeneration, and says to regenerate and merge it by hand. - The merge queue labels pull requests with at most one status:
merge-queue: queued,merge-queue: kicked backormerge-queue: needs regeneration. A merge the queue sees removes everymerge-queue:label, and the next apply run clears a closed pull request still carrying one. Label creation GitHub refuses as invalid is an error. --scratch-env NAME=DIRonmagus queue validateandmagus queue applypoints a variable at a directory inside each candidate's scratch space, so cache isolation lives on the queue's flags rather than on the hook's command line. It may repeat.- A kick-back comment reproduces the failure. Each kick-back is a new comment with the run's link and the command that failed, runnable as written.
- A merge-queue provider's
describecan decline with{refused: {reason, url, app}}.magus queue describeprints what is missing and where, then the command to run next with the provider'sappin place of--app. The queue still passes--appto the provider unread. - The queue acts on the base's other required checks. Apply reads them at the head
(the provider's optional
required_checks): running ones wait withWAIT_CHECKS, red ones on a head carrying the base's tip kick back withKICK_RED, and red ones from an older base get one update commit merging the base in, which runs them again. - A
merge-queue: <method>label queues any pull request. On a pull request outside a stack, applied by someone with write access, it is merge intent like auto-merge, which GitHub will not enable on a pull request it says conflicts. The queue then resolves the conflict or kicks it back naming the files. - Results carry structured
nextsuggestions.query,explain,describe file, affected listings and failing results carry up to three{id, command, argv, why}entries, filtered by the acting lease's role and journaled per session. - A run held back by its width suggests
concurrency_profile: aggressive. It fires once per session, only after 15s or more queued for slots with cores idle. It stays silent under an explicitconcurrency, an already aggressive profile, or a wait on the machine budget. Uptake is counted bymagus session hintsasconcurrency-profile. - A run that may write the remote tier backfills it. A local hit whose key the remote
tier lacks is uploaded in the background, after a
has_artifactlookup; the cache contract gains that optional function. - A spawn rule sees its continue target's facts and the job store.
targetcarries the spawn'sdescription,modeland last observedcontextTokens;magus\job\list()reads the guard's rows. A spawn titled<parent>/<role> <job>grades the child under that job's lease and records its base. - Spells can be imported from a registry by path.
import "ghcr.io/team/spells/lint";is declared with a tag inmagus.yamland pinned inmagus.lock; only theupdatecharm, throughmagus spell lock --update, resolves a tag. Apath:entry replaces an embedded or remote spell. MGS1041 through MGS1044 cover undeclared, stale, mismatched and invalid.magus spell build|push|pull|lspublish. - A subagent's shell and edit calls are graded under its job.
magus shell --agenttakes the host's subagent id, and the command and path glue forward it (HOST_AGENT_PATH, defaultagent_id). Guard templates are at version 17; re-install them. - A target whose inputs have not moved since it failed says so before rerunning.
Nothing is skipped (
hint_id: unchanged-failure). testkit.Environ,IsolateandMaingive a test an environment built from an allowlist. The sandbox's allowlist plus the Go toolchain's settings survive; HOME and the XDG base directories move under a temp root; the broker and server are pinned off; everything else is dropped. A package names any extra variables in its TestMain.- A text filter over a backgrounded run's capture is denied like a pipe over magus.
Use
-o jsonl --tee <file>for a capture a tool may consume. - Tools declare
observeprobes; ops declare external effects. An observation keys only the targets that drive the tool (obs:). Ops markreads-externalormutates-external, and MGS1033 fails a cacheable target composing one with neither an observation norskip_cache. The docker spell gainstrivy-image. - URLs in comments and docs become graph edges. A URL resolves to a local page,
heading or file where it can, otherwise to a
linknode (kind=link). Nothing is fetched. Knowledge-graph schema v14. - VCS capabilities that combine revisions without a working copy.
TreeReporter,TreeMerger,CommitWriter,GeneratedPathReporter,CheckoutProvisioner,RevisionFetcher,PusherandBundlerjointypes.VCSDriver, withRangeFilesandRangeCommitsonRangeReporter. git implements every one; none runs a hook or signs. - A write that opens an unrelated unit of work draws an advisory pointing at the magus-multi-agent skill.
Changed
- The agent guard hook no longer loads the workspace.
magus shellreads its rules from the root magusfile alone, and loads the committed copy only while a file that load read is uncommitted. Measured here: about 120ms per call, down from about 1.6s. - The agent surface stops promising a checkpoint restore. A checkpoint records a digest, not the patch; magus-vcs-hygiene covers recovering work.
aggressivenow claims memory, not just cores.balancedandconservativestill reserve a quarter of memory for everything else on the machine;aggressivetakes every usable megabyte down to a fixed 512 MiB floor for the kernel.- An ambiguous output ref prints a count and three examples.
- A
BAGGAGElease claim ranks below every record.magus shell --leaseno longer defaults to it, so the subagent's spawn record and the checkout'smagus job execbinding answer first, for magusfile job writes and attention requests too. Verdicts, activity events, target results and attention requests recordlease_from:flag,agent,marker,envorcontested; the console's session lineage shows it. - BREAKING: the activity wire's
actoris a rendered label, andactorsfilters by field.actorwas a kind (agent,operator); it is now the origin rendered for a row head. Anactorsentry matches one origin field exactly (user, host, agent, credential, entry point), never the label. The review-remark telemetry labelhumanis nowunattributed. - Breaking for SDK callers: a shared service crosses the broker wire as a
broker.ServiceSpec.Client.AcquireServiceandServiceHost.Acquiretake one instead ofspells.Service, so a spell schema change never changes the protocol;broker.NewServiceSpecresolves a spell's service to one. A service acquire with no command is refused asmalformed, andClient.Shutdownreturns once the broker has hung up. - Breaking: a retired or misspelled
MAGUS_*variable stops every command. SettingMAGUS_NO_WAIT, aMAGUS_DAEMON_*name, or a near miss such asMAGUS_CACHE_DIRRfails with MGS1046 before any work, naming the fix;magus shelldenies instead. Other unknown names run, andmagus doctorreports them. - Breaking (Go API): the cache's tiers share one shape.
cache.WithMutableisWithLocalWrite,WithRemoteStatsisContextWithRemoteStats,Cache.Remote()isRemoteNamespace(ns), andRemoteBackendtakes(namespace, key), answersErrRemoteMissandErrRemoteExistsinstead of(nil, nil), and gainsHasArtifact.cache.Openreads no environment. - Breaking:
magus queue applyrequires--base, and--workflowwith a run source. Verdicts no longer carrymessage, and a provider'slist_artifactsreturns the run's origin asrun; a provider script without it is refused. - Breaking: the GitHub merge queue requires its own GitHub App.
magus queue describe --status-contextandapplyrefuse to run without--app, and nothing falls back to the job's Actions token: a run or merge that token makes starts no workflow, so the queue validated changes and merged none. Register the app with the linkdescribeprints. - BREAKING: spells declare what their tools need from the sandbox. The core grants no
toolchain; each spell's
mgs_getSandbox()does, and a spell op's child gets only its project's spells. Drop Go variables fromsandbox.env.passthrough: the go spell passes them. Grant mise with asandbox.allowentry. A target's newsandboxpolicy key takes the same shape. - Breaking (SDK): every
types.VCSDriverimplements every capability. A backend without one returns*types.VCSUnsupportedErrornaming itself and aVCSCapability, matchingErrVCSUnsupportedanderrors.ErrUnsupported.RemoteURLtakes a remote name,RangeDiffReporterisRangeReporter, andBisectmoves toBisector. - BREAKING: a token holds a grant, and every route names what it needs. A grant is
none,readorwriteper surface (tokens,mcp,console); a valid token below a route's need gets 403 MGS9015. No door mints a token wider than its minter's grant. Doctor'smcp-tokenscheck is nowtokens. - BREAKING: tokens carry their class and always expire; older ones are refused.
mgo_is the operator,mgs_a stored token (now intokens.d),mgl_a share link, which the loopback daemon refuses. Stored tokens live at most 366 days and share links 24 hours; longer, ornever, is MGS9018. An old operator file is MGS9016, anything inconnectors.dMGS9017. - Breaking: the
daemonblock inmagus.yamlis nowserver. Every key moves as is:daemon.enabled,daemon.address,daemon.idle_ttl,daemon.workspacesanddaemon.maintenance.*becomeserver.enabled,server.address,server.idle_ttl,server.workspacesandserver.maintenance.*. The flags follow,--daemon-*to--server-*. Adaemonkey is refused with an error naming its replacement. - Breaking:
MAGUS_DAEMON_*variables are nowMAGUS_SERVER_*.MAGUS_DAEMON_ENABLED,MAGUS_DAEMON_ADDRESS,MAGUS_DAEMON_IDLE_TTL,MAGUS_DAEMON_WORKSPACESand theMAGUS_DAEMON_MAINTENANCE_*family keep their suffix underMAGUS_SERVER_; setting an old name is an error that names the new one. The pool pointer magus exports to its children,MAGUS_DAEMON_SOCKET, is nowMAGUS_PROC_SOCKET. - Breaking for SDK callers:
magus.Openrefuses a broker it would never use.WithBrokergiven a nil client, or a client whileWithBrokerPolicyor the workspace'sbrokersetting resolves tooff, is an error fromOpenrather than a client silently ignored. Pass one or the other. - Breaking for SDK callers: the daemon names are gone from the Go API.
magus.Daemon,SetDaemonandServeDaemonareServer,SetServerandServe;types.EntryPointDaemon,HolderDaemon,DaemonRequiredandDaemonSocketWithheldareEntryPointServer,HolderServer,ServerRequiredandProcSocketWithheld. Theinternal/daemonpackage isinternal/server. - Breaking: the wire says server where it said daemon.
Pool.daemon_versionis reserved and replaced byowner_version;JOB_HOLDER_DAEMONis reserved and replaced byJOB_HOLDER_SERVER; the entry point recorded on an event isserver, notdaemon; and the doctor checkdaemon-versionisserver-version. - Breaking: sockets and logs moved.
$XDG_RUNTIME_DIR/magus/broker.sockandserver.sockreplacemagus-daemon.sock, and a detached broker or server logs to$XDG_STATE_HOME/magus/, which survives logout, instead of beside its socket. The detached server runs asmagus server --foreground. - Breaking for SDK callers: status types name the broker and the server.
types.StatusSnapshotgainsBroker,ServerandBrokerPolicyand dropsMachineandServices;types.StatusOutputdropsModeand renamesDaemonVersiontoVersion. The newbrokerpackage is the client and server, andworkspace.WithMachineAdmitteris gone. - Breaking:
magus statusreports the broker and the server one fact per row.-o jsongainsbroker,serverandbroker_policy, movesmachineandservicesunderbroker, and dropspool.mode;pool.daemon_versionispool.version.magus version -o jsonreportsserverfordaemon.magus broker statusexits non-zero when none is running. - Breaking: the daemon is two processes,
magus brokerandmagus server. A run starts the broker, which holds host capacity and shared services on a unix socket only and exits after ten minutes holding nothing. Onlymagus server startstarts the server, which serves MCP, the console and jobs.server stop --servicesis nowbroker stop --services. - Unreleased changelog entries are fragments under
changes/unreleased/. One file per entry, so concurrent pull requests never edit one shared section;CHANGELOG.mdkeeps an empty[Unreleased]and the docs changelog page renders the fragments. A malformed fragment or an unknown group failspr-changelog. - CI asks for the whole machine explicitly. Every
magusinvocation in this repo's own.github/workflows/*.yamlthat runs a build, test, lint, or generate target now passes--concurrency-profile aggressiveon the command line, the same flag any other caller would use - not an environment variable read by magus itself. - A step claims the memory it has been measured to use. Machine admission claims the
smaller of a target's
memory_mband 1.25 times its highest measured peak over at least three successful runs, so a declaration sized for the worst case no longer refuses peers that would fit.magus statusand MGS3009 say whether a figure is declared or measured. - Claude Code, Codex and OpenCode harnesses are Buzz spells under
spells/harness/, wired withmagus\harness.provider. Adapt one by forking it and changing the import path. - Console links carry a one-time code, never a token.
#code=lives a minute and works once; the console trades it atPOST /api/v1/token/exchangefor a 12-hour console token. The guard denies agents the commands that mint or revoke tokens (credential-verb) and the token files (token-state). A non-loopbackmcp.addressneedsmcp.insecure_bind: true, and the operator token is refused off loopback. - The daemon's guards refuse a request as
google.rpc.StatusJSON in the route's protocol. A Connect service answers Connect's envelope; every other route,/mcpincluded, AIP-193's{"error":{"code","message","status","details"}}. Both carry the MGS code as agoogle.rpc.ErrorInforeason and agoogle.rpc.Helplink. A missing bearer token is now MGS9011, split from a rejected one (MGS9001). Host, loopback, share-device and console-file refusals gain MGS9007-9010. - A daemon whose workspace fails to load keeps serving and says why. The console,
/mcpand status stay up; workspace calls answer MGS3016 (FAILED_PRECONDITION, onePreconditionFailureviolation per diagnostic), or MGS3017 while reloading.StatusServicereportsWorkspace.stateand agoogle.rpc.Statuserror. A failed workspace reloads when a.buzzfile ormagus.yamlchanges. - A declared
timeoutbounds the target's own time. A body parks while itsctx.needsdependencies run and gets a fresh deadline per stretch of its own work. - Breaking:
magus queue validatereads its plan from stdin with--stdin;--plan <file>is gone. Runmagus queue validate --stdin ... < plan.json, the same input idiom asaffected --stdinandrun --stdin. Without--stdinvalidate exits 2. - A deny on a multi-command line says nothing on it ran.
- Every Connect procedure and
/api/route names its own need. The daemon refuses to start on a missing or empty one, and an unloaded daemon holds the same needs. Graph reads needconsole=read. TokenService takes aGrantand lists each token's class;TokenScopeis gone. A malformed share body is MGS9020, an impossible mint MGS9021. - The redundancy check, CI verdict inheritance and job completion skip only a trivial
change. A comment-only edit, and markdown a package embeds or a gate target reads,
no longer skip the gate. A job's completion gate on
cipasses on a greencigate when the change since it is trivial. - The GitHub-hosted 4-core hard-code is removed. A larger runner gets its real core
count. magus reads no environment variable to guess it runs in CI, so the same command
behaves the same everywhere, and
concurrency_profilestaysbalanced(min(cores, 8)) unless something asks otherwise. - The guard denies every channel a bound lease could use to rewrite its row (
magus job exec <other>,job wait,job fork,op=clear). Reads and--schemapass. - The guard denies process-table polling (
pgrep,pidof,ps). Usemagus status --watch. - The guard hook asks version control only about the files its policy load read. An
allowed shell command no longer waits on a whole-tree status or on resolving the
repository root.
magus.ApprovedSpawnRuleAt,ApprovedCommandRuleAtandApprovedWriteRuleAtare renamedLoadApprovedSpawnRule,LoadApprovedCommandRuleandLoadApprovedWriteRule;ApprovedPolicy.Pendingtakes a scope. - The guard judges a script by its content.
bash x.sh,python3 x.pyor./x.sh, and a write of such a file, get the busy-wait, scripted-rewrite, cd, output-pipe, output-redirect, capture-filter and unknown-env verdicts the script's lines would get typed inline. The refusal says the script was read. - The owned-path lease advisory speaks once per session per lease (
leased-path). It repeated on every write into a running lease's paths, 52% of all advisories served in one audit. - A
forloop that sleeps between passes is a polling loop. Thebusy-waitrule deniesfor i in $(seq 1 60); do sleep 10; done, and a workspace command rule sees each program inside such a loop withrepeatsset, as it already did forwhileanduntil. Aforloop over a list with nosleepstill repeats nothing. - The guard routes unbounded source dumps to
magus refs. symbol-searchdenies alternations, definition lookups and diagnostic codes. A recursive search whose every alternative (A\|B,-e A -e B,func X,type X) is an indexed symbol routes to onemagus refs <name> --occurrenceseach; a registered MGS code routes tomagus explain diagnostic:<code>.- "Handoff journal" is renamed to memory across the command, docs and manpage.
- Breaking: a holder's rendered terms carry commands.
footeris replaced bybootstrap, a list of{run, why}, and the rules and skills blocks are gone. - Breaking: hook glue names its host on the command (guard template 18). Each glue
command carries
--agent-name <host>, rendered from the harness spell's name.__MAGUS_AGENT_NAME, theclaude-codedefault and Codex detection fromturn_idare gone; a hook with no name is refused (MGS3024). Re-runmagus agent harness apply. installprobes only its own tools and skips dependency order. A project's install no longer waits for its dependencies' installs, andrun installprobes node and pnpm, not tsc.- Breaking for SDK callers:
types.RegionReportergainsRegionsBetweenandDrivers.RegionsBetweenplaces the lines two in-memory versions of a file differ in, andDriversnames each path's diff driver. A backend implementingtypes.VCSDriveradds both, or declines with*types.VCSUnsupportedError. - Breaking: job rows are
schema_version11. A#in a write path now claims a declaration, which an older magus would match against no file and stop grading, so it refuses these rows instead. Records sent tomagus job fork --stdinandmagus_jobcarry 11. A magus that writes 10, which ends dead jobs, refuses them too. - The job store refuses a write to another holder's row. A leased session may record
its base, shrink its own
write_paths, end its own row, and fork inside its paths.cleararchives what it drops. - Breaking: MGS3018: a job's write paths name files.
magus job fork,magus_jobandmagus\job.putrefuse a write path that is an existing directory, unless it is a project root the job owns whole or does not exist yet. A glob ending in wildcards (dir/**) is judged as its directory. - Breaking: magus ends dead jobs itself, and job rows are
schema_version10. A store read ends a live job asno_returnwhen an ancestor ended, its checkout is gone, or nobody took it withinjobs.stale_after(default 2h,0never). Older magus binaries refuse the store once this one writes it: restart the daemon and rebuild./magusafter upgrading. - A lease binding that does not read is an error. A marker holding anything but a lease
id no longer reads as unbound: the guard denies with the path, the CLI commands that
resolve a lease fail, and
magus job exec --vacateclears it. The guard lets that command and help through, so an agent can recover. - Breaking:
log.format: jsonlis refused frommagus.yaml,MAGUS_LOG_FORMATand--log-format. It withheld per-target results with no stream to carry them;-o jsonlis the one way to select it. magus agent harness apply|install|removeis refused under a lease from any source. The commands refuse under the checkout's binding as well as aBAGGAGEclaim, and the guard refuses them for a worker attributed by its spawn or its session.magus doctorchecks a freshly built knowledge graph.graph-boundsbuilt nothing and passed whengen/knowledge-graph.jsonwas absent; it now builds the graph in process and fails when the build does. The graph JSON is no longer committed.magus doctor'srecurring-guard-denialscheck reports facts only. Rule, surface, denial count, session count, and followed rate; the retired advice layer's destination and confidence labels are gone. A human reads the evidence and decides.- MAGUS.md is formatted with the repo's other Markdown.
- Breaking:
magusmust be imported. A magusfile, spell or script callingmagus\needsimport "magus";; without it the load fails with MGS1039, which names the fix. - magus never waits on another magus invocation. A workspace lock or machine budget
held by another invocation refuses immediately (exit 75), naming the holder.
MAGUS_NO_WAITis removed. Invocations in one process (the daemon's) queue for each other, as do the nested runs of one root invocation.--watchretries on the next change. magus query output --publishis refused when remote writes are off.magus query outputreads trail payloads.grd(guard verdicts) andmcprefs resolve besideoutrun outputs.- Breaking:
magus status -o jsonnests concurrency.config.concurrencyis an object ofconfigured,profileandeffective;config.concurrency_effectiveis gone. - Breaking: magus is told where it runs; it never detects it. The CI provider spells
stop checking
GITHUB_ACTIONS/GITLAB_CI; wire them under a setting the workflow sets.TestNoEnvironmentSniffingenforces it. - The MCP tool catalog is generated from the
std.Magusdescriptor. - Breaking:
server.sockspeaks HTTP. Forwarded runs, jobs, status, reload and stop are/proc/v1/paths on it, and a client meeting a server started by an older magus gets MGS3025 naming the restart. On Linux and macOS it admits only processes running as the server's user (MGS9022 otherwise). - The merge queue needs no bypass actor. Apply posts
successright before a merge, once main is still at the predicted tip, and GitHub's auto-merge merges; apply merges itself after a minute. A success it cannot follow through goes back topending. Breaking for providers:list_greenis required, andmerge_changereportsby_provider, whichmergedevents carry. - MGS3010 defers a redundant
cigate regardless of load.--no-redundancy-checkruns it anyway. -o jsonlruns emit only records. Headers, progress, summaries, race diagnostics and lock decisions are typed events on stdout besiderun.target.result; notices, other log lines and output printed outside a target arerun.noticerecords on stderr.magus x,affected --stdinand--detach(run.detach) do the same. No record is dropped; the schema is now 5.- Per-session guard state is keyed per host. Facts a rule reads, such as skill loads
and projects written, key on
<host>/<session>; fire-once notices and deny explanations key on<host>/<transport>/<session>, each part escaped.magus shell --transportnames the hook form; the shipped sh and Buzz command and path hooks passshandbuzz. - A pipe of magus runs fails at its first failed stage, without
set -o pipefail. A run starts nothing once a magus stage upstream of it has failed, and the last stage exits with that stage's status (MGS3030), somagus run generate:rw . | magus run test .is a chain whose exit status can be trusted. - A planted token record cannot outrank a minted one. The store skips, with MGS9019,
a record holding
tokens=write, outliving 366 days or naming another file, and keeps the rest;magus doctorfails on it. Revoke takes an exact id or name, within the caller's grant. Every mint is audited, and a revoked token ends its open streams. - The PR advice comment leads with files no project claims.
- Breaking: a merge queue hook is a command and its arguments, not a shell line.
The queue appends the change's affected projects (
/when unproven) instead of settingMERGEQUEUE_*variables, so a gate becomesmagus run ci --no-default-charms;--factsgets the fact asked for. Shell syntax is refused with MGS3026: point the flag at a script. - The queue keeps a change whose generated files its merge leaves alone. It kicks
back with
KICK_REGENERATIONonly when the merge needs them regenerated (a conflict, or main changed them) by code the change touches; otherwise the drift gate checks them. A kicked-back change merges main in, regenerates, and is queued again. - The merge queue kicks back stale generated files before its gate runs. A change that edits generator code must commit outputs that are current on top of the base. If they are stale, the kick-back names the stale files and says how to fix them. If they are current, the change merges.
- The queue lands a change whose generated files drifted. Planning no longer kicks
back with
KICK_REGENERATION; validation regenerates the candidate, gates it, and uploads it ascandidate.bundle. Apply runs none of the change's code: it takes that commit only once it checks its parent and that it changes declared outputs alone. - Raw package-manager installs are advised, not denied.
pnpm install,npm ci,uv sync,cargo fetchandgo mod downloadpoint atmagus run install; naming a package leaves the command alone. - A record with no host claim is unattributed, never "a person".
magus shelltyped at a terminal recordsentry_point: cli, no session, and no longeractor: "agent"; its terminal window keys fire-once notices but is not recorded as a session. The OS user says whose account acted. - Breaking: the
relockcharm is renamedupdate, with no alias. A run spellingrelockfails with MGS6002, which namesupdate; rename the suffix.cistripsupdateas it strippedrelock, andrwdoes not include it. - Breaking for SDK callers:
RenderedSkillstakes a form, not a variant. The*ForFormmethods are gone. - A repeated guard deny is one line and a ref. The first deny from a rule in a session
carries the full reason,
nothing ran (N commands)on a multi-command line, and the rule's page. Later ones name the rule and citemagus query output grd<hex>, counted bymagus session hintsasdeny-verdict. - The
run.remoterecord saysstored, notpublished, which names output bundles only. - Breaking: sandbox grants are explicit, and misconfiguration is an error. Exec
needs
mode: rxorrwx;roandrwno longer imply it. An unset$VAR, a mode typo or a passthrough prefix likeGO*fails with MGS2004. System,PATH, toolchain and tool-cache directories are granted by default; children get a privateTMPDIR. - Breaking:
sandbox.enabledis nowsandbox.mode: off | best-effort | required. AlsoMAGUS_SANDBOXand--sandbox=<mode>, replacingMAGUS_SANDBOX_ENABLEDand--sandbox-enabled.best-effortis the oldenabled: true;requiredrefuses to run (MGS2012) unless kernel landlock can confine its children. The old env var is an error naming its replacement. magus describe graph -o markdown <project>renders only that project. A scoped index drops the workspace-wide kind and project tables, so a change elsewhere cannot make it stale; the unscoped index keeps them. A project path that names no project is now an error instead of an empty index.- "Session" now means only the host's conversation; magus's per-process id is an
invocation.
magus sessionlists INVOCATION and SESSION columns;-o jsonkeys areinvocations,invocationandsession. The store is schema 2; a schema-1 line is counted and named as written before the rename (legacyin JSON), and pruning never deletes a file this build cannot read. - Breaking:
--skill-formtakesboth,shortorfull. Skills stampskill-variant: short, and a body brackets short wording with{{if .Short}}. magus spell lock --updatewrites no lock that pins nothing. With no remote spell declared it removesmagus.lockinstead of writing a header andversion: 1.- A target's
std\printis captured with its output. It is withheld, streamed and stored under the target's ref like a subprocess's output, instead of bypassing both. - The trail names the credential, not "operator", and drops
actor. Each event recordscredential(the verified token's class, id, name and grant, never its secret) and the MCP client ashost; the wire'sactoris a label rendered from them. Console review comments areunattributed, nothuman. types.DoctorCheckistypes.Check, andDoctorCheckStatusisCheckStatus(withCheckOK,CheckFailandCheckAdvice): a conformance finding is the same record. Go names only; JSON keys and proto messages are unchanged.magus\diffgainsopts.from, reading a saved review, somagus diff --impact's advisors share one diff.- Breaking: an unknown key in
magus.yamlfails the load. MGS1040 reports each asfile:linewith the nearest known key; a second YAML document in a file is rejected. - Upstream-wait messages back off (15s, 30s, 1m, 2m, …) and name both targets.
- Breaking for SDK implementers:
VCSDriver.Preserveis required. Every backend captures and restores the working copy, untracked files included. - A workspace load reports every broken magusfile and every shadowed spell at once.
ls,doctorand every other command name them all instead of stopping at the first. - Writes to host guard wiring are denied under a bound lease. Every verdict names the lease it was graded under; an unknown lease id is a deny.
Removed
- Breaking: the advice action's
hand-edited-generatedadvisor and input. It named generated files as hand edits whenever no declared input of their project changed, which was false for every output whose target opts out of the cache, such as aMAGUS.mdrendered from the whole graph. The drift gate already fails on a real hand edit. Delete the key fromwith:. - Breaking: the advice action's
fix-generated-drift,fix-merge-conflict,offer-fix-labelandfix-labelinputs. Both fixers pushed to the pull request's branch and neither ever ran: their consent label never matchedgh's JSON. The merge queue now settles drift and conflicts at merge time. Delete the keys fromwith:; the action needs onlypull-requests: write. - Breaking: the advice action's
pr-number,base-ref,head-sha,head-refandhead-repoinputs. The action reads the pull request from the triggering event and runs its advisors in one step; delete those keys fromwith:. An input switch reading anything buttrueorfalsenow fails the step, and one failing advisor no longer stops the rest. - Breaking: the
ci-shardtarget. A workflow publishes the plan itself: savemagus affected ci --planto a file, then redirectmagus run --stdin --dry-run -o 'template=...' < plan.jsoninto$GITHUB_OUTPUTand$GITHUB_STEP_SUMMARY. The GitHub Actions guide shows the three lines. A magusfile that copiedci_shardkeeps working. - Breaking: the
exclusivetarget and project option, with no replacement. A magusfile that sets it fails with MGS1038; delete the key.slotsandmemory_mbare the concurrency dials. The run-isolation gate goes with it. See docs/decisions/0001. - The
code-searchanddoc-searchguard advisories. Measured uptake was 0.9% over 6,075 servings and 2.5% over 812. A search the graph answers exactly is still refused bysymbol-search, and a one-name precedent hunt still advises. - Breaking:
harnesses/*.jsoncompat descriptors are removed. All four shipped hosts are Buzz spells underspells/harness/, wired withmagus\harness.provider(...). JSON descriptors underharnesses/,.magus/harnesses/and$XDG_CONFIG_HOME/magus/harnessesare no longer read, and--idnow resolves only a wired spell. Adapt a host by forking its spell's import path instead. - Breaking:
magus run --thenandmagus affected --then, with no alias. Pipe the run into amagus buzzscript instead:pipe\outputs,pipe\exportTo,pipe\history,pipe\diffandpipe\valueact on the records it reads, andfs\readFileandcrypto\sha256Filecovercontentsandhash. - The
magus_tail_logMCP tool.magus_outputreturns the same bytes by ref; the SDK keepsMagus.TailLog. - The
preflighttarget convention. The starter magusfile and the docs no longer declare one, and thetypescriptspell's no-oppreflightop is gone;--preflightreplaces the idea. A target you namedpreflightkeeps working. - Breaking for SDK callers:
ReportWriter,NewReportWriter,WithReport,WithReportWriter,Magus.LogScope,LogCharms,LogCacheandLogBase. Build oneSinkwithNewSink(format, stdout, stderr)for the invocation's-oformat, emit headers through it, pass it withWithSinkand close it after the run. - The
stale-binaryadvisory and thego -Cdeny into another checkout of magus. Both judged only magus's own repository, which now keeps them in its guard policy.
Fixed
- Breaking for SDK callers:
magus cleankeeps outputs the VCS tracks. It deleted committed generated files, with or without--cache, and left the tree dirty.CleanOutputsreturnsCleanedOutputs, listing removed and kept paths. - A broken working tree no longer switches off the approved spawn rule. The committed
magus\guard.spawnrule runs on every spawn whatever the working tree holds; resolving it too slowly denies. A skipped rule says what applied. A workspace advise joins a built-in one, and the idle clock follows the agent's id. - Status, doctor and the console see both background processes. The liveness probe
asks the configured server rather than whichever socket a run inherited; doctor's
socketscheck reportsbroker.sockandserver.sockby name; the compact status line names each; and the dashboard gains broker and server tiles.MAGUS_*settings now apply in a workspace with nomagus.yaml. - A claim released while a restarted broker was taking it back is released there. A step finishing during the moment its claim moved to the new broker left the claim counted, so the broker admitted less work until that process exited.
- A claim released while its holder reconnects no longer stays on the new broker. A release that landed after the new broker recorded the re-asserted claim, but before the client finished reconnecting, was dropped, so the broker counted that memory until the holder's process exited. The client now hands such a claim back as soon as it reconnects.
- A broker reply that arrives just before the connection closes is no longer lost.
magus broker stopcould reportconnection closed during shutdownafter the broker had answered and stopped. - A run killed outright releases its claims and services at once. Claims and service references ride the run's one broker connection, so the kernel closing it releases them; this replaces pid polling and the 24-hour cap. When the broker restarts under running steps, they re-assert their claims on the new one.
- The server keeps the knowledge graph and symbol indexes current with MCP off.
Graph watching and symbol indexing belonged to the MCP listener, so
mcp.enabled: falsequietly stopped both; they now run for as long as the server does. - "Cannot check byte-stability" is recorded. It fails the gate, and a
-o jsonlrun now carries it asrace.determinism_uncheckedwith its error. - Clones of an hg, jj or Sapling repository share one state store. Identity is read from each backend's config without running it. jj is covered when colocated with git.
- Concurrent fetches into one repository no longer fail. Two
git fetchruns read each other's refs mid-update and failed with "bad object"; magus now fetches into a repository one at a time. - Config values given as flags are validated.
--log-level bogusran with a value the same setting inmagus.yamlor the environment is refused for. - The console dashboard connects on the daemon's own origin. The token exchange sends an expiry the daemon accepts, and a page from an older console build asks for a reload.
- Console failures are always shown. Every failed daemon call, stream or undecodable frame raises a notification, and the console lint rejects a swallowed catch. A page with no token shows one sign-in state with the command that opens it signed in, and a 401 returns there.
- Console links carry a runnable command.
magus job fork,ls jobsand the other console hints printopen "<url>#token=$(magus config token print)", andmagus_console_presentreturns it asopen. The job hint names a daemon running a different build instead of claiming nothing serves the console. - A coverage record is no longer refused after a squash merge. Its commit is provenance only; per-file digests keep it honest.
affected --planadvice names--max-shards, the plan's own flag, instead of--ci-max-shards. The GitHub Actions guide no longer tells shard jobs to run a nonexistentaffected --shardwith amatrix.totalthe plan never emits; they runrun ci:gha ${{ matrix.projects }}.- A cache hit no longer restores a nested project's generated files. An output glob
such as
**/gen/mocks/*.gostops at a nested project's directory unless rooted there, in the snapshot, replay, drift gate and--race=replayalike. MGS3001 now judges a directly selected step's replay, catches a file replaced with its mtime kept, and names the likely writer. - Every git call magus makes is hardened the same way.
GIT_DIR,GIT_REPLACE_REF_BASE,GIT_ATTR_SOURCE, the shallow and pathspec variables and injected config never reach git, including the shallow-clone deepening fetch, which re-added them. git never prompts for credentials, and no signing, rerere or signature line changes what magus reads. - Generated docs examples read the same wherever they are regenerated. The
examples generator runs magus in
testkit.Environ, so noMAGUS_*variable, cache or state dir it inherits leaks run history into the capturedmagus explainoutput. - A command magus runs leaves no process behind. On Linux, macOS and the BSDs, whatever of a target's process group outlives its command is killed when the command exits, before it is reaped, so a background process it started stops there and no longer holds the run open for five seconds on its output.
- A failed remote-cache exchange names the step that failed.
- A failed spell import names its magusfile. A workspace failure located no file for
an import error, and an error built without a relative path rendered
magusfile: exec :. - A failing remote store reads as failed, not missed. Both shipped cache spells throw
on a failed request;
falsemeans not stored (get) or already stored (put). - A fresh magus checkout can build its first binary. The
raw-toolrule advisesgo build -o magus ./cmd/magusalone into a checkout root with nomagusyet, and denies it once one exists.go -C <dir> <verb>andgo <verb> -C <dir>reach one verdict, and a barecd <dir>no longer trips thecdrule. - Generated output no longer depends on the build. magus requires
GOEXPERIMENT=jsonv2and refuses to build without it. - The GitHub Actions remote tier stores what it uploads. The spell read the signed
URLs under their lowerCamel names while the service answers
signed_upload_url, and took the empty URL for an existing entry: every upload reported success, nothing was stored, and every lookup missed. It reads either name. - The GitHub provider no longer offers a merge method a ruleset refuses.
describeintersected repository settings alone; it now narrowsmethodsto what every active ruleset rule targeting the base branch also allows, dropsmergeunder a required linear history, and errors when nothing is left in common. go::go-cleanaccepts forwarded flags.-cache,-testcache,-modcache, and-fuzzcachealways landed alongside./..., which go refuses in any order. A newCommand.TrailingArgsslot drops the package pattern the moment any flag is forwarded; a barego::go-cleanstill cleans./....- The graph links a target to a workspace spell imported without an alias.
import "spells/acme";, the form BZZ1008 requires, produced no target-to-op edges, somagus pathandmagus explainmissed every op it runs. - Guard advisories no longer fire on paths outside the workspace. A command whose every path lies outside the root, or a write into a scratch directory, is advised nothing, and scripted-rewrite no longer refuses a script whose every named path is outside it.
- Imported cache files are 0644, and a running target's crash record survives the same target running twice at once. Stale inflight temp files and staging directories are collected.
- Installs of magus-managed git, hg and Sapling sections are atomic. They are serialized per repository and leave a hook executable. A torn section marker is an error.
- Knowledge shards on the remote tier are signed and verified, and a run that may not write the remote tier stores none.
log.levelis honored. It was overwritten at startup by the level-vand-qimply, solog.level: debuginmagus.yaml,MAGUS_LOG_LEVELand--log-levelleft the process atinfo. A verbosity flag still wins when given.magus affectedrebuilds the project a renamed file left. Underdiff.renames, git's default, only the new path was reported. Branch-change notices list both too.magus bisectnames the culprit on current git. Newer git writes# first 'bad' commit:in the bisect log, which the parser did not read.magus doctorreports an unregistered merge driver from an explicit boolean.magus doctorsees the checkpoint hook template again (template revision 11).MAGUS.mdrouting indexes are byte-identical on every machine. Example columns rank by the repository's own edges, not the binary's spell catalog; gitignored sources are skipped; and the drift gate now catches an output a composed step writes, which let five library indexes go stale.magus session disposerefuses without an interactive terminal and is denied to agents. Disposing an attention request records that a PERSON answered it. Outside a terminal the CLI exits 2 with the--acksentence, and the guard ruleagent-sign-off(widened fromread-ack) denies every spelling on every agent channel.- One magus can pipe into another that needs the same project, even through
jqortee. The reader, proven from the kernel on linux and macOS, waits until the upstream is done with its projects, draining the pipe; before, the stage that lost the race exited 75. Different projects still stream. A looping pipe is MGS3023. - A
MAGUS_*value that does not parse stops the load. A bad number or duration was ignored, andmagus.Openskipped validating the environment at all. magus vcs resolve --againstworks in a linked worktree, and paths stage literally. A conflicted merge there read as one that never started, and a file named*.txtstaged every.txtfile. A merge already underway is now refused.- Go ops key their cache on the platform they build for. The go spell's build, vet,
test and lint ops fold
GOOS,GOARCH,GOARMandGOAMD64into their cache keys, so a run for another platform no longer replays the host's result. - The
output-pipe/output-redirectexemption formagus query outputandmagus refs --textnow sees past a global flag. It anchored on the first argument aftermagus, somagus --root <dir> query output <ref> | grep xwas wrongly denied; the check now reads argv the same way the read-ack rule does, ignoring where a global flag sits. - The push gate grades the checkout a push runs in.
git -C <dir> push,hg -R,jj -Rand a leadingcd <dir> &&are graded by that checkout's revision and gate record, not the hook's, and a single refspec grades the revision it names. A directory only the shell can resolve keeps the advisory. - The queue merges a gated candidate whose only regeneration was a settled conflict.
When validation's regeneration wrote nothing, apply kicked the change with
KICK_REGENERATION; it now merges its own rebuild once that rebuild is the commit validation gated, still running none of the change's code. - The merge queue no longer kicks a change back for a red it inherited. A red
candidate whose base is red on the same projects waits with
WAIT_BASE_RED, keeps its place and gets no comment. The base is gated once a run, however many changes it reddens. - Queue candidates are dated from the commits they merge. They carried a fixed 2000-01-01 date, so a build that reads HEAD's date as "now" dropped every dated post. The plan records the newest date among its base and admitted heads, so the same queue still builds the same candidates.
magus queue describe --appworks for a private GitHub App. It reads the App ID from an organization's installations where an owner's token lists them; elsewhere it names the app's settings page and prints the rerun with--app <slug>:<id>. The printed key step reads no stdin and deletes the download even when storing fails.- The merge queue no longer asks the remote for blobs its own merges wrote. In a
partial clone, git fetched them as missing and the remote refused, failing the
candidate as a machine error.
queue validateandqueue applynow refuse a partial clone, and the queue workflows check out full clones. - Merge-queue hooks can run what they build in their scratch directory. A hook's
scratch directory, where its tool caches live, is now read, write and execute, so a
regeneration's
go runno longer fails withpermission deniedon the binary Go cached there. - A quiet
magus\cmdthat fails carries the child's stderr in its error. The Workflows passsecrets.GITHUB_TOKENasGITHUB_TOKEN, whichghand the github queue provider both read, in place ofGH_TOKEN. - A remote hit is one
cache.hitrecord, counted once its replay succeeds. A local replay that fails tries the remote tier before rebuilding. - A remote-tier miss is visible. Each prints
<project> not in the remote cache (out...)with the producing run's ref, the end-of-run line counts misses, and-vadds a digest per key-input class. - Replaying a fully cached run is fast again. Tool probes run concurrently and skip tools no selected target drives. Cache keys are unchanged.
--rootfrom another directory no longer loads that directory's modules. A magusfile's imports resolve against its project, then the workspace root.magus run test . -- -run Xruns only the selected tests. The workspace'stesttarget passed explicit arguments togo test, which replaced the forwarded ones, so every narrowed run executed the whole suite. It now appends its forwarded args and skips the coverage floor when narrowed; the spell docs show the+ argsidiom.- Args after
--reach only the target you name. A cached replay's gates,--preflightsteps and the settle step no longer receive them, somagus run test . -- -run Xstops failing in its generate step withflag provided but not defined: -run. - A run the machine's build budget refuses says so. It exited 75 with nothing after
the header; it now prints
[fail] <project> <target> (not started)with the MGS3009 cause naming the holder, and-o jsonlemits therun.target.resultandrun.diagnosticrecords. - Two runs of one step recorded in the same millisecond resolve to the later one.
A bare output ref and
--attemptsbroke that tie by a random attempt hash, so they could answer with the older run. Each record now stores when it was persisted, and that decides the tie. - A sandboxed tool can create its declared cache directory. The kernel sandbox skipped
a granted path that did not exist yet, so buf on a fresh machine was denied its cache. A
missing path a spell or
sandbox.allowdeclares writable is now created; a missing workspace is still reported. - Sandboxed
go generateandgo runwork again. The sandbox grants the Go build cache execute as well as read and write: since Go 1.24,go runandgo toolexec the binaries they cache there, so everygo rungenerator failed withpermission denied. - Share and wrong-method failures answer in the refusal shape.
/api/v1/shareand a wrong method on any/api/route send AIP-193 JSON (MGS9012-MGS9014); the console shows its message and Help link. Health reports down when every workspace failed, and the Windows sign-in line is PowerShell'sStart-Process. - A shared step no longer inherits one caller's timeout. It runs under the invocation's cancellation. MGS3012 lists what was still admitted.
- A step waiting on an upstream target no longer masks a stall. Only a moving step beats the heartbeat; MGS3013 and MGS3012 still catch a wedge.
- Three guard rules match their catalog entries.
cdfires only ahead of a magus command.cache-dir-writegrades only write targets, sorsync --exclude .magusand an interpreter's quoted data pass.stage-all's description now names-u,.and the long forms its matcher already covered. - The unchanged-inputs hint no longer reads as a verdict. It prints before the target runs, so it now says the target runs again, and it names the failed attempt rather than the step ref, which moved to the new run's result once that run landed. The target always re-ran; the exit status was already that run's own.
- An unrecognized boolean in a
MAGUS_*variable is an error instead of silently keeping the previous value. - An unrecognized spawn decision ranks as deny, not allow, when two rules' verdicts merge.
- A vulnerability database release no longer invalidates every Go target's cache. The database date keys only targets that run govulncheck.
- Workspace load is 10x faster. A bare library import is no longer executed as a
candidate spell, Buzz tokens are shared across sessions, and
magus lsloads once. A run skips re-evaluating a magusfile that does not export the target, and an exact source no longer walks the tree.magus lshere: 1.45s to 0.11s.
Security
- Breaking: the remote cache refuses pre-domain
ed25519signature envelopes. They signed the manifest alone, so a store could replay a genuine entry with its log and descriptor swapped or stripped. Every magus from 0.4.0 signs the domain-separated form; an entry signed by 0.3.x is now a miss and rebuilds. - Breaking: queue hooks run in the base's sandbox. The gate, the regeneration and a
--factscommand run under the base'ssandboxpolicy, at leastbest-effort, rooted at their checkout: landlock confines their files on Linux, and everywhere their environment is the sandbox's.--sandbox=requiredrefuses a hook the kernel cannot confine;tools/gha-queue.buzzpasses it. - The daemon's unauthenticated
/console/serves only the app shell. It served every built console file, including the demo graph JSON holding the whole knowledge graph and its notes. Other files and directory listings now return 404, on loopback and on the LAN share, and an attached graph explorer never falls back to that demo data. - A leased worker can no longer slip past the harness refusal or borrow another job.
A global flag's value (
magus --root . agent harness apply) or a single-dash word with anh(-o=template=hi) hid the command from the guard. A spawn title naming a job outside the spawner's own lease tree is recorded as untrusted and attributes nothing. - The merge queue trusts less of what a change controls. A review counts only from
an account that can push, at the commit it approved;
describereportsrequired_approvals. The regeneration proof covers what merged beneath a candidate and treats no extension as data. Regeneration follows no committed symlink, and update commits carry the queue's committer. magus queue applyfollows only the base's own validation run. A pull request's run executes its own copy of the queue workflow and could upload a forged plan and verdicts that merged ungated. Apply now refuses any run but--workflowstarted by a push or dispatch on--baseof its repository (MGS3027), andqueue-apply.yamldispatches main's run instead.magus queue applychecks the plan against what it reads itself. A plan naming another base or remote, a base commit the base lacks, or a stack base that is not the reviewed head beneath stops applying (MGS3028): a forged stack base could merge a revert of the base. Apply writes the squash message itself.- The merge queue's own git no longer runs what a change's hook configures. Git in
a candidate reads the repository recorded when the checkout was made, with fsmonitor
and submodule recursion off; a rewritten
.gitrefuses the change. Hooks get the shared object store read-only, and every head is fetched before the first hook. - Queue hooks run on an allowlisted environment. A hook inherits only the sandbox's
default names and the base's
sandbox.env.passthrough, so no token or Actions file command reaches it. A unit starting with-or holding a line break is refused, and a hook's process group is reaped without a race. - A merge queue kick-back shows what validation reported as text. The claim, file
names and commits render as code spans or fences no backtick run can close, and the
facts line escapes every character that could end its HTML comment. The reproduce
lines come from
magus queue apply --reproduce-gateand--reproduce-regenerate, never from a verdict. - Files that run code after the run are write-protected. Under any mode but
off, magus's checks refuse writes to.git/hooks,.git/config,.git/info,magus.yaml, mise and asdf pins,.envrc,.claude/,.cursor/,.mcp.json,.vscode/tasks.jsonand git hook managers' config. Landlock cannot deny inside a grant, so a child can still write them. - Buzz's own
osandiogo through the sandbox, each run'senv\setstays in that run, the proc socket demands a token (MAGUS_PROC_TOKEN), and the lease marker moved out of the cache dir a confined run can write. - The sandbox confines each child, not magus. A launcher applies the landlock
ruleset to every process a run starts, so a server serves each workspace under its
own policy.
requiredneeds landlock ABI 3 (Linux 6.2), else MGS2012. MGS2010 now means a nested or forwarded run asked for a weaker mode. - Sandbox path checks follow symlinks the way the kernel does. A
..after a symlink, a write through a dangling link, andfs\symlinkto a path outside the policy no longer pass the binding-level check. - Breaking: no job holding a secret or a write token restores an Actions cache. A
merge queue hook can read the runner's runtime token and plant cache entries in the
default branch's scope, so trusted jobs now install cold.
setup-magusrestores run history only withrestore-history: 'true'. A conventions test enforces both.
[v0.4.3] - 2026-09-06
See the full changelog at https://github.com/egladman/magus/compare/v0.4.2...v0.4.3
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 set of recorded 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 suite 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.