magus v0.4.2 is out. See what's new

Contributing to magus

Thanks for being here. magus is a one-person project, and anyone who takes the time to try it, file an issue, or send a patch is doing me a favor.

Issues and PRs are both welcome. Replies may be slow, which is about how much time I have rather than how interested I am. If you're planning something big, open an issue first so we can talk it through before you spend an evening on it.

Using AI tools

I use AI assistance to build magus, so it would be strange to tell you not to. Use whatever you like.

If you're new to the codebase, one suggestion rather than a rule: make your first couple of PRs without it, and keep them small. A docs fix, a test, a one-line change. Nothing controversial.

That's less about the tools than about the size of the change. What I'd rather not get is a big multi-file PR rewriting how something works, from someone who hasn't had a chance to poke around yet. These tools are about as good as the context of the person steering them, and when they're wrong they're usually wrong in a way that still reads fine, which is hard to spot if you're new to a codebase. Doing a couple of small things by hand is the quickest way past that.

After that, go for it. I'm looking at the change, not at how you made it: can you explain what it does and why, does it match the conventions below, does magus affected ci pass. You don't need to tell me either way.

And if any of this is unclear, or the setup fights you, please open an issue. I'd much rather answer a question than have someone give up on it quietly. Friction is the thing this project exists to remove, so hitting some on the way in is worth telling me about.

Build and test

git clone https://github.com/egladman/magus
cd magus
go build ./cmd/magus
go test -race ./...

Integration tests sit behind //go:build integration and are named TestIntegration*. go test ./... runs the fast unit tests; go test -tags=integration ./... runs everything.

Lint and the vuln check run as pinned binaries from mise.toml, so the linter's large dependency tree stays out of magus's module graph entirely:

magus run lint

They deliberately do not go through go tool. That compiles the tool from source with whichever go leads PATH, which is how CI once built golangci-lint with a different toolchain than the one it analyzed with, and panicked. mise install gets you both binaries at the pinned versions.

Adding to a public surface

magus promises that a magusfile which works today keeps working, and there is no plan for a 2.0. docs/concepts/compatibility is the promise itself; this is how to work inside it.

Add, never substitute. A new magus\project key goes beside the existing ones, a new protobuf field takes a new number, a new flag joins the current set. If you find yourself changing what an existing name means, that is the thing the promise forbids, and the answer is a second name rather than a redefinition.

Three gates catch a break before review does, one per surface:

Surface Gate
CLI internal/cli/testdata/api.lock, a drift-gated snapshot of every subcommand, flag, config key, and target
protobuf buf breaking, composed into lint
magusfile keys the required_version floor, plus doctor's check that the floor is accurate

A diff to api.lock is the signal to read carefully: a line removed there is a removed public surface. Regenerate it with go generate ./internal/cli/... after an intentional addition, and treat a deletion as a question rather than a regeneration.

The fourth surface has no gate: std/ host-module method names. A whole-tree review in August 2026 established this, and it is worth knowing before you touch a descriptor. Method.Name in a std/ module is run through std.CamelCase and becomes the identifier every magusfile calls, so renaming it breaks them - and nothing catches it:

  • MGS1025's removed-API table covers only the magus.* namespace, so it cannot soften a fs.* or os.* rename, and it is a friendlier error rather than a working alias either way.
  • internal/interp/bindings/testdata/magus-api.lock pins only magus.* member names. No lock file lists std method names at all.

So a std rename is a silent break that review has to catch by eye. If you make one deliberately, it belongs under Breaking in the changelog with the old and new spellings, and every in-repo caller must move with it - magusfile.buzz, spells/, docs/**/*.buzz, tools/, testdata scripts, and the .txtar fixtures under cmd/magus/testdata/.

The same descriptor's Doc: string is not documentation either: it is codegen input, reaching the generated .d.ts, docs/reference/buzz/*.md, and editor hover text. A wrong Doc teaches every Buzz author the opposite of the contract, which is how three fs and vcs methods came to promise a false/zero/empty return while their bodies raised.

A new key means the workspace needs a newer magus. Additive changes are safe for existing users and unsafe for the repo's own magusfile, which CI runs against a pinned release. The rule is release-first: if magusfile.buzz starts needing a feature no release contains, cut the release before the magusfile depends on it. When you hit this locally the symptom is a load error from the pinned binary, which is the mechanism working rather than a problem to route around.

Raise required_version in magus.yaml in the same commit that adds the key, naming the release that will carry it. Doctor's floor check asserts the two agree, so a floor left behind is caught rather than remembered.

This used to say the opposite: raise it in the release commit, because a floor naming an unreleased version would reject a source build of the very commit that raised it. Between releases a build describes itself from the last tag, so a tree 42 commits past v0.3.0 reports v0.3.0-42-gabc and compares as 0.3.0. That hazard is gone. ward.CheckRequiredVersion exempts dev builds outright, and types.IsDevMagusVersion counts anything carrying a -g<sha> suffix, which is every build from source. The floor is enforced only against a clean release tag, which is exactly who it is for.

Two things follow from raising it early, and both are the mechanism working: audit.yaml's compat job pins the newest release and will fail until the one you named ships, and a released binary meeting the workspace gets MGS1021 naming the version it needs. The alternative is worse: a floor that still admits the old release lets it load far enough to fail on whichever key it happens to hit first, which reads as a typo rather than a version problem.

Before 1.0, renaming is on the table; after, it is not. magus is pre-1.0, so a badly chosen name can still be fixed, and the changelog records it under Breaking. That window closes at 1.0. If you are adding a name you are not sure about, say so in the pull request - it is much cheaper to argue about it now than to keep it forever.

Deprecating means "there is a better way now", not "this stops working". Keep the old surface working, point at the replacement where someone meets it rather than only in the changelog, and list it under Deprecated.

Performance changes need evidence

This is the rule I care about most. Any change that claims to be faster ships with a checked-in Benchmark* and the benchstat numbers behind it. No speculative micro-opts.

Capture a baseline, make the change, then compare:

go test -run=^$ -bench=. -benchmem -benchtime=2s -count=10 ./PKG > before.txt
# ... your change ...
benchstat before.txt after.txt

Put the benchstat rows in the commit message, not the tree. Leave an inline optimization: comment at the hot path, in the form used in internal/cache/mtime.go:

// optimization: <what changed in one line>.
//   measured: <BenchmarkName> <delta> (benchstat, n=N).
//   trade-off: <legibility/portability cost>.
//   assumes: <platform/kernel/build constraint>.

so the trade-off is reviewable without re-running the bench. Per-OS fast paths (see internal/cache/reflink/) always keep a portable fallback; never gate behavior on a fast path.

Docs site

The docs site under docs/ renders into docs/gen/, which is not committed. .github/workflows/cd.yaml renders it on every push to main and publishes that. To look at your change, use the dev server rather than opening a file:

magus run build docs
magus run build console
magus run serve .

serve is the workspace-root loop for both deployables at once. It hosts one server with the GitHub Pages layout - / is docs/gen, /console/ is console/gen - and re-renders the affected tree when you edit a source, so a reload shows the change with no restart. It refuses to start until both gen/ trees exist, which is what the two builds above are for; a docs-only change still needs the console one, once.

Reach for magus run generate docs when you want the drift gate and the integrity, orphan, and abandoned-URL checks rather than a preview. It is the slower path, and it is what CI runs.

It was committed for years. The reason it no longer is: every page embeds the commit that last touched its source, so committing a source change immediately staled the rendered output committed beside it. That bought a second "refresh generated metadata" commit after every real one, and amending could not escape it, because the new hash restales the footer it just recorded. Rendering at deploy time knows the final commit already.

What still gates on drift is the generated Markdown that is tracked: MAGUS.md, the root CHANGELOG.md, docs/src/gen, and the derived pages under docs/reference/. A plain magus run generate docs fails if any of those was left un-regenerated, so CI still catches a forgotten regen for everything a reader can find in the repository.

One thing under docs/ stays tracked on purpose:

  • docs/active.urls.lock is the ledger proving a previously published URL never starts 404-ing. That gate only works if the ledger outlives a single build.

The playground wasm used to be a third, vendored because CI had no TinyGo to rebuild it. CI's stock Go builds it now, so it is ordinary render output under gen/ (magus run build-playground docs).

Pages use extensionless URLs (/magus/documentation/, served from docs/gen/documentation/index.html). If you rename or move a page, keep the old URL alive by listing it under aliases: in the page's frontmatter, so external links do not die:

---
title: Download
aliases: [install] # clean, gen-root-relative old paths
---

The build emits a redirect stub at each alias and fails if an alias collides with a real page or is claimed twice.

Dogfooding: how magus builds magus

CI runs the pipeline twice, in series, because there are two questions.

From source, and it gates. preflight and the ci shards build magus from the commit under test and run the workspace with it. Only this pass can answer "does this change work" - a magusfile needing a new host binding has no released binary able to load it. A failure blocks the PR.

From the previous release, and it informs. The compat job runs the newest published magus against the same workspace: can the binary users already have still drive it? continue-on-error, deliberately. A failure means the magusfile now depends on something unreleased, so the remedy is to cut a release, not patch the PR. Gating on it would redden every such change until an unrelated release happened.

Expect compat to fail when you add a flag, target, or host-binding shape the magusfile uses. That is the release-first signal working. Do not move a job off the released binary to clear it - that deletes the check instead of answering it.

A source build resolving a git-ref must be reachable from main; an unverified build would otherwise run with the job's permissions. source-path skips that gate by construction, since the caller checked the tree out itself.

Your worktree's merge driver resolves with your own build

The rule above is about CI. Locally there is one place where "the released binary" is the wrong answer, and it used to cost an afternoon.

magus init registers the git merge driver as whichever magus leads PATH. That is correct for someone using magus, because the registration survives an upgrade-in-place. In a magus worktree it is backwards: PATH holds a release, and the merge driver is part of what you are changing. A rebase then resolves every generated conflict with the release, not with your tree.

That is not a theoretical gap. A released driver regenerated the whole docs site once per conflicted file, so a rebase over a handful of generated files looked exactly like a hang, while the version in the tree resolved the same file in under a second. Worse, it was silent: git reads a driver that exits non-zero as "could not merge", so a broken driver and no driver look identical.

Nothing to run. A workspace that has built its own ./magus registers that as its driver, and magus doctor fails if the registered one cannot read this workspace. To pin a specific build instead - so a long rebase cannot have the driver rebuilt under it - register one by hand and it will stick:

git config --worktree merge.magus.driver "$PWD/magus vcs merge-driver %O %A %B %L %P"

git config --worktree keeps the override local to that checkout. .git/config is shared by every linked worktree, so an absolute path there would aim all of them at one checkout's binary.

Naming

Names are the API most people meet first, so they get decided deliberately rather than by whatever the file was called. From the outside the results can look arbitrary - go lives in spells/golang/, ls and describe both list things - so here is the reasoning, which is not arbitrary.

Source layout and registered identity are independent

A spell's directory and the name it registers answer different questions, and they are allowed to differ.

The directory is where a contributor finds the code. The registered name (mgs_getName) is the identity users type and every listing prints.

spells/golang/          <- idiomatic directory name for Go source
  registers "go"        <- the language's actual name

Both are right. golang is the conventional directory spelling; go is what the language is called. Forcing one to match the other would make one of them wrong. Do not "fix" a mismatch on sight - check which question each is answering first.

A registered name has to stand alone

The registered name appears in magus describe spells, in diagnostics, and in error text, always without the directory around it to supply context. So it must be unambiguous on its own:

  • Name the thing, not the job it does here. The S3 backend registers aws-s3, not s3-cache. Its siblings are named for products, and a capability name reads as a different kind of entity in the same list.
  • Qualify when the bare word names nothing. spells/github/actions/ registers github-actions, not actions - "GitHub Actions" is the product's real name, and actions alone identifies nothing.
  • Never take a word the core model already owns. spells/gitlab/ci/ used to register ci, which collides with the ci target that magus affected ci anchors on. A listing then showed a ci spell beside a ci target meaning entirely different things. It registers gitlab-ci.

One verb, one job

Subcommands are split by the question they answer, not by the noun they touch, so two verbs never differ only in verbosity:

  • ls enumerates. Breadth. What exists here, what can I run. Everyday.
  • describe explains one thing fully. Depth. A definition plus the complete record. Occasional.

That is why magus ls shows targets but not source globs: the globs are the full record, which is magus describe project's job, and printing them in both would make the boundary mush. When adding output, ask which question it answers and put it in exactly one place.

Prefer a noun on an existing verb over a new subcommand. magus ls targets rather than magus targets, because the latter invites magus spells, then magus charms, and the surface becomes a pile of noun-commands with no rule to learn. One verb, a noun that says what.

Enumeration is spelled ls everywhere - magus ls, magus run ls, magus memory ls - never list.

Package names mirror the contract they serve

Where a package exists to serve one wire contract, it takes that contract's name, so the two are correlated by reading rather than by grep. A wire-mapping subpackage internal/handler/<name> owns the over-the-wire concerns of the protobuf package magus.<name>.v1:

proto/magus/graph/v1     <->  internal/handler/graph
proto/magus/status/v1    <->  internal/handler/status
proto/magus/viewer/v1    <->  internal/handler/viewer

Adding proto/magus/foo/v1 means adding internal/handler/foo - same name, no exceptions for the wire packages. Two subpackages there are deliberately not proto-backed and so are not part of the mapping: mcp is a protocol adapter and trailrpc is a transport concern.

internal/handler/README.md is the authority and carries the full table plus what does and does not belong in the layer; keep the rule there rather than restating it per package.

Hints belong in hint

A command path printed inside output goes through internal/hint, never a string literal. A drift test walks every registered command and asserts it still resolves, so a rename cannot leave a hint pointing at a command that no longer exists. That has already happened once: a failing target printed magus query <ref> long after the command became magus query output <ref>.

Generated files in this repository

The general rules are in Generated files and your toolchain: linters never gate on generated output, formatters cover it as long as the generator emits formatter-normal output, and the scope for both derives from ctx.writesFiles rather than a hand-maintained ignore list. What follows is how that lands here.

Language Formatter Generated output Why
Go gofmt, via go-fmt covered internal/generate/emit gofmts before writing, so the check is a no-op
TypeScript Biome, via biome-format excluded (!src/gen/**) bundler and protobuf output is not Biome-normal and never will be
Markdown dprint excluded (dprint.json) generators do not emit dprint-normal output yet; see below

Two things worth knowing before you change any of it:

  • Go formatting is gated by golangci-lint, not by the format target. gofmt -l lists unformatted files on stdout and exits 0, and magus reads an op's verdict from the exit code alone, so unformatted Go used to pass green. .golangci.yml enables gofmt under formatters:, which reports the same finding and exits 1. The format target keeps gofmt -l as the local reporter, and format:rw flips it to -w.

  • Markdown formatting is dprint, and generated Markdown is excluded from it. That exclusion is the documented fallback, not the goal: it stays until each Markdown generator emits dprint-normal output, at which point its entry comes out of dprint.json. dprint over prettier because it needs no Node in the format path, and over Biome because Biome does not read Markdown at all. .prettierignore was deleted when prettier stopped being installed; it had outlived the tool by long enough that six of its seven paths no longer existed.

  • The console lint target conflates formatting with linting, and should not. The typescript spell exposes biome-check and biome-format as separate ops precisely so a target can compose them independently, but console/magusfile.buzz calls proc\exec("pnpm", ["exec", "biome", "check", "src"]) directly, and biome check reports formatting as lint findings. That is the exact blur the concepts page argues against, in our own tree. Route it through the spell ops when you next touch it.

Byte-stability is the one rule with no exceptions, and this repository has paid for it more than once. Every instance was a real bug in a generator, never a reason to relax the rule:

  • The knowledge store merged shards in Go's randomized map order, so which shard supplied a node's source changed run to run. Node and edge counts never moved, so it surfaced only as magus run generate failing its drift gate on provenance lines. Fixed by sorting shard names before the merge (internal/graph/knowledge/store.go).
  • catalog_fingerprint identifies the binary, not the graph, so two builds of one source produced different values and a regeneration that changed no node and no edge still rewrote the file and failed CI with the fingerprint as the entire diff. It is excluded from graph export --reproducible for that reason.
  • The @runtime shard put locally observed diagnostics into MAGUS.md's anchor ranking, so the committed index depended on which codes that machine had tripped. Routing() now excludes runtime edges.

Graph.Output and writeShard both sort before emitting, and both say why in a comment. Do the same in any new generator.

When you add a generated artifact here, follow the five-step checklist in the concepts page. The magus-specific part is that the root generate target already drift-gates every generated file in the workspace, so a per-artifact drift test in Go is redundant and will drift from the real //go:generate directive.

Workflow targets, not inline shell

The GitHub Actions workflow files are intentionally thin. Every meaningful CI operation lives in a target in magusfile.buzz, not in a workflow YAML step. The workflow just orchestrates: it sets up the toolchain, calls the target, and uploads artifacts. Host-specific knowledge (GitHub Actions concepts like $GITHUB_OUTPUT, the gha charm) lives at one boundary, and every other piece of the pipeline stays portable.

Concretely:

  • The workflow YAML names jobs and steps, sets env vars that host the host-specific plumbing, and uploads artifacts. It never contains logic that could live in a target.
  • The magusfile targets hold the actual work: which projects to fan out over, what to render, how to interpret drift, what to write to a sink the workflow supplies via env.
  • A target that wants host-specific behavior declares it via a charm (gha, cd, rw) and only reaches for a host-specific env var (ci_shard reads $GITHUB_OUTPUT) once that charm is set - without the charm it prints a preview instead. The workflow sets the env to whatever the host provides.

This way a future port to a different CI system only needs a thin workflow file plus the same magusfile. The targets and charms are portable; a target that must read a host-native env var confines that knowledge behind its charm check.

Workspace references: the bare path

A project reference is a workspace-relative path written bare. That is the only canonical spelling, in and out:

pkg/foo               # a nested project, measured from the workspace root
.                     # the project the cwd is in
./pkg/foo  ../foo     # measured from the cwd

Every project-arg surface takes those, and every surface that prints a project prints the bare path - rendered via types.ProjectLabel, which reads the root as the workspace directory's name rather than a bare .. What magus prints is what magus takes, so nothing has to be rewritten between the two.

project:pkg/foo is a different grammar, not a second path syntax: it is the kind-prefixed node reference the graph commands (explain, query, path) use where kinds mix, alongside target: and spell:.

The workspace://pkg/foo scheme is DEPRECATED. It still parses, with a warning (internal/file/resolve.go), so a ref written against it keeps resolving; it is not taught or printed any more, and it never bought a reading the bare path did not already have. types.WorkspaceRef still renders it and is down to two error wraps in magus.go; both it and ProjectLabel route through types.ProjectRef{Path, Dir}, so new rendering rules go on the struct's methods and both pick them up.

Release-signing key rotation

magus trusts a KEYRING, not a key: internal/selfupdate/release-keys.json lists every Ed25519 public key a signature may come from, each with a state. Signing uses exactly one; verification accepts any that is not revoked. The ring has reviewable copies in docs/gen/install, the setup action, and the download guide, and TestReleaseTrustAnchorMatchesInstallerAndCI fails CI when they disagree. Never put a private Ed25519 seed in the repository or a release artifact.

state signs verifies
active yes yes
standby no yes
retired no yes
revoked no no

The fingerprint is derived from the key (KeyID, the first 16 hex digits of its SHA-256), never written down, so an entry cannot name a key it does not hold.

A planned rotation, which is why the ring exists. The old procedure was a chain: ship a compatibility release signed by the current key that embeds the replacement. It only worked for someone who passed THROUGH that release, and SelectRelease takes the newest by default - so anyone a version or two behind jumped straight to a signature their binary had never been told to trust and was stranded with no in-band way back. The rotation was correct on paper and skipped exactly the people who most needed it.

Instead, ship the replacement as standby long before it signs anything:

  1. Generate the keypair. Add the public half to release-keys.json as standby, and to the installer and setup action. Run the trust-anchor test.

    Rebuild with --no-cache. release-keys.json is embedded but matches none of the go spell's source globs, so an ordinary magus run build reports cached and leaves a binary carrying the old ring. Shell completions have the same gap.

  2. Release normally, more than once. Every binary in the field now trusts a key that has signed nothing.

  3. When you are ready, flip standby to active and the old entry to retired, and move the MAGUS_SIGNING_KEY secret to the replacement private key.

  4. Run the Release index workflow. The served index is signed with the same key, so until it is re-signed docs/gen/public/release/index.json.sig still carries the old signature and magus self update refuses it. That workflow is manual precisely for this: a rotation, a corrected manifest, and a yank are the moments the index must change without a version being cut.

  5. Drop a retired entry entirely once nothing you still need to verify was signed by it.

Revoking a compromised key. Set its state to revoked and delete it from the installer and the setup action, then run the Release index workflow. Its fingerprint is published in the signed index's revoked[], and a client refuses any signature from a listed key.

This is worth stating precisely, because it is easy to overclaim. Revocation only works because the revoking index is signed by a DIFFERENT key - the standby one - which an attacker holding the compromised key does not have. Signing a revocation with the revoked key would prove nothing, so the client refuses an index that revokes its own signer.

What it does NOT fix: an attacker holding the active key signs whatever they like, and binaries trusting that key accept it until they see a revocation. The residual hole is that the attacker can keep serving an OLD index carrying no revocation. expires_at is what bounds that - past it the client refuses the file rather than trusting a stale one, converting an indefinite compromise into a denial of service, which is the right trade. It is 180 days, and nothing republishes on a timer: magus doctor warns as the deadline approaches and the Release index workflow is the remedy.

If the compromise is real, treat it as an incident rather than a procedure: stop using the signing secret, publish the replacement binary and public key through an independent trusted channel, and ask affected users to reinstall manually. Do not describe that path as an automatic update.

Glossary

Glossary

The vocabulary that runs through the rest of the docs. Each entry is a short definition; follow the link for the page that covers the term in depth. Every term has its own anchor, so you can deep-link a single definition (for example glossary/#output-reference).

Core model

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Charm

An execution modifier attached with : (lint:rw) that changes how a target runs, not which one; the built-in rw flips a check-only target to mutate in place, and ci always strips it. See charms.

Ward

A coded diagnostic that inspects a resolved op and nudges or blocks an anti-pattern before it runs. See wards.

Module

A magus stdlib namespace a magusfile imports for host capabilities: filesystem, exec, vcs, and more. See the module reference.

Buzz

The language magusfiles are written in (the .buzz engine). See engines.

Engine

The interpreter a magusfile runs on; magus embeds the Buzz engine. See engines.

Execution and caching

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

Sandbox

The restricted filesystem and environment a target runs in, so builds stay reproducible and side-effect-free. See sandbox.

Service

A long-running or shared process magus manages across runs, distinct from a one-shot target. See services.

Daemon

The background magus host that owns shared state such as services and the warm knowledge graph. See daemon.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs - magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Output reference

A short, shareable id (out1a2b3c, "ref" for short) for one target execution's captured output; it appears on each target's line, and magus query output out1a2b3c prints those exact bytes. In OpenTelemetry terms it corresponds to a span (one target execution) within its trace (the whole magus invocation). See output-refs.

Trace

OpenTelemetry's name for one whole magus invocation; every target it runs is a span beneath it. See telemetry.

Span

OpenTelemetry's name for one unit of work under a trace - a target execution, whose sub-operations are child spans. An output reference points at a span's captured output. See telemetry.

Pool

The concurrency pool: the shared set of slots that caps how many targets run in parallel on one machine. Its capacity defaults to MAGUS_CONCURRENCY, then 4 on GitHub-hosted runners, then min(NumCPU, 8); magus status and the dashboard report it live. See daemon.

Slot

One unit of the pool's capacity. A target acquires the slots it needs to run (most take one) and releases them when it finishes; the pool tracks capacity (total slots), running (acquired), and queued (blocked). See daemon.

Concurrency

How many targets run at once. It is bounded by the pool's capacity and set with --concurrency, MAGUS_CONCURRENCY, or the concurrency config key. See daemon.

Queued

A target that wants a slot while the pool is full; it blocks first-in-first-out until a slot frees. The dashboard colors a sample with queued > 0 accordingly. See daemon.

Pool mode

Which pool a run uses: daemon (one shared pool the background daemon owns across every workspace and client) or proc (a per-process pool for a single one-off invocation). See daemon.

One-off

A single magus invocation that runs a target and exits, using a per-process pool; the opposite of the long-lived daemon or a service. See daemon.

Remote cache

A CI-only backend that shares content-addressed artifacts across runners: a cold machine replays a build another runner already did instead of rebuilding. Every remote artifact must be signed by a trusted key. See remote.

Snapshot

A point-in-time view of live state - the pool's occupancy or a tick of exported metrics - as opposed to accumulated history. See daemon.

Backfill

The recent history the daemon replays to a dashboard on connect, so its charts start populated instead of empty. It is served from a bounded ring buffer of the last few hundred samples. See daemon.

Telemetry and health

Latency

How long an operation takes. magus records latency as OpenTelemetry histograms per family - target execution, cache op, pool wait, and graph query - and reports each as a count, sum, and percentiles. See telemetry.

Percentile

A latency value at a given rank, interpolated from a histogram's buckets: p50 is the median, p95 and p99 are the tail that most latency budgets care about. See telemetry.

Health

The at-a-glance daemon state derived from the pool: healthy when the pool is reporting, degraded when it reports an error, down when there is no pool. The dashboard color-codes each state. See daemon.

Volatility

A target that fails once and passes on rerun is volatile, as opposed to a regression that started failing and stays failing. magus keeps per-target pass/fail history and a Wilson-score volatility rate to tell them apart and auto-retry the noise. See volatility.

Insight and knowledge

Knowledge graph

The queryable graph of a workspace's spells, targets, docs, and code relationships; query it with magus query/explain/path. See knowledge.

MAGUS.md

The committed routing index at a workspace root, regenerated from the knowledge graph: it lists every node and points at the exact query for a given question, so it is the entry point an agent reads first. See knowledge.

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). See insight.

Hotspot

An insight lens: edit frequency times complexity, the prime refactoring targets. The project view heat-colors the dependency graph by churn; --files ranks individual files. See insight.

Affinity

An insight lens: projects that change together (temporal coupling). A pair that co-changes without either declaring a dependency on the other is a candidate architectural smell. See insight.

Ownership

An insight lens: author concentration - the primary author and their share, the distinct-author count (the bus factor), and abandonment. See insight.

Trend

An insight lens: the recent half of the window against the earlier half. A positive delta is a rising hotspot; a negative one is cooling. See insight.

Diagnostic code

A stable MGSxxxx identifier attached to a magus warning or error, so it can be referenced and looked up; some are guardrails (see wards), others hard errors.

Design and scope

Two rules named often enough elsewhere to need a definition of their own.

Scope test

The question every proposed capability has to answer: does it read the model magus already had to build, or does it make magus learn something new about the world? Reads stay small; acquisitions are where a tool loses its shape. See scope.

One-vocabulary rule

Each concept gets one name, used everywhere: target, spell, charm, op. A second word for the same thing is a house dialect, and it costs every reader (and every agent) a lookup that never ends. See doctrine.

Sessions and leases

The vocabulary of magus watching work happen: who ran what, what an agent is blocked on, and which agent owns which paths. The policy behind these terms lives in doctrine.

Session

One magus process's recorded facts - the targets it finished, their outcomes, and the lease it acted as - kept in a repo-scoped store every worktree shares. magus session lists them; the store prunes itself by last-fact age.

Attention request

A durable "an agent is blocked" record, opened when a magus session notify event carries the waiting or permission outcome and held until a person disposes it. magus session attention lists what is open. Nothing closes one on its own - see doctrine.

Dispose

The human act of closing an attention request: a judgment rendered, recorded with who and why. Distinct from resolving a review thread or a merge conflict - a disposition answers a request; it does not merge anything.

Lease

One row of the lease ledger: a piece of work an orchestrating agent handed out, with its goal, the checkpoint it was cut against, and the paths it owns or must not touch. The ledger records; the agent guard is what reads those facts back when grading a write. See doctrine.

Lease id

The short identifier a worker carries (the --lease flag, or the magus.lease member of the W3C BAGGAGE environment channel) so its runs, journal facts, and guard verdicts attribute to its lease. Letters, digits and -_./: only.

Spawn claim

What a spawning tool said about itself in the environment: TRACEPARENT (the W3C trace and the parent span this process runs under) and the magus.spawner baggage member (a label for whoever spawned it). magus records each verbatim beside the session's own minted span id, and no verdict reads any of them - the ancestry is a relation between recorded sessions, the way a process tree is a relation between pids.

Advisor

One read-only check from the advice suite: it reads the changeset through magus and writes one titled section of findings. The same advisors run as a pull request comment in CI and inside magus diff --impact locally.

Console

The vocabulary of the browser app. These terms name things you only meet in the console's UI, so they are defined here rather than left to be inferred from it.

Console

The browser app that reads a magus workspace: a tabbed, tiling page hosting the log viewer, graph explorer, dashboard, and activity trail. It is a separate static app, not something the daemon serves - the daemon exposes a loopback API it calls: read-only views plus one bearer-gated job-control service for maintenance jobs. See reference/console.

Console app

One of the console's applications (Runs, Log Viewer, Graph Explorer, Dashboard, Activity Trail, Settings). "App" rather than "page" because one is never a document you navigate to: it is mounted into a tab, or into a pane beside another one. Each is single-instance - opening one you already have focuses it instead of duplicating it.

The glossary term is two words on purpose. As a bare "App" the auto-linker matched every unrelated "app" in the corpus - a ChatGPT desktop app, a Postgres app - and pointed each at this definition. See reference/console.

Pane

A split within a tab. Splitting divides the focused pane along its longer side, so the same action tiles side-by-side on a desktop and stacks on a phone; a tab with no split is a single pane. Drag the divider to re-weight the split. See reference/console.

Chord

A key combination bound to a console command, written mod+k - where mod is Cmd on macOS and Ctrl elsewhere, so one binding fits both. Every chord is rebindable (Settings > Keybindings), and a command remains reachable from the command bar whether or not it has one. See reference/console.

Command bar

The console's runner: one searchable list of every command and its chord, opened with mod+k. It is the discoverable route to any action - the menus and chords dispatch the same commands it does. See reference/console.

The URL that points an app at a running daemon. The daemon serves the console from its own loopback origin, so the link is that origin plus the app path and a bearer token in the fragment (http://127.0.0.1:7391/console/graph/#token=...). The daemon prints it; the console consumes the token, stores it, and strips it from the URL, so the secret never lingers in history or a copied link. The origin must be literal loopback - localhost and hostnames are rejected before any request. Without one, an app reads only what rides in the link itself. See reference/console.

See also

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.