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

Contributing to magus

magus is a one-person project. Issues and PRs are welcome; responses may be slow. Open an issue before a large change so neither of us wastes the effort.

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 live in a separate go.tool.mod, so the linter's large dependency tree stays out of magus's library module graph:

go tool -modfile=go.tool.mod golangci-lint run
go tool -modfile=go.tool.mod govulncheck ./...

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/ is generated into the committed docs/gen/ tree; regenerate and commit it after any doc change:

magus run generate:rw docs   # re-render, keep the output
# review `git status docs/gen`, then commit gen/ alongside your source edit

A plain magus run generate docs gates on drift and fails if gen/ was not re-rendered, so CI catches a forgotten regen.

Pages use extensionless URLs (/magus/documentation/, served from 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

The CI never uses HEAD to build the workspace. The binary that compiles this repo is the latest released version; any feature the magusfile.buzz uses must ship in a release first. This is the only stance that keeps the dogfooding contract deterministic - a CI run that compiled magus with a newer magus would be testing its own next release, not the one users have.

In practice the workflow uses the setup-magus action with installation-strategy: source and git-ref: ${{ github.sha }} for the rare case where no prebuilt artifact exists for the SHA (right after a push to main before a release). For the common case, the prebuilt artifact from the previous release is the binary every step uses. The source-build fallback carries a supply-chain gate - the commit must be reachable from main - because the build is unverified otherwise.

If your change adds a flag, a target, or a host-binding shape the magusfile uses, that change ships in a release before the magusfile change merges. The other way around is a breaking-change signal, not something to paper over.

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 clihint

A command path printed inside output goes through internal/interactive/clihint, 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>.

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_STEP_SUMMARY, the gha charm, the MAGUS_INSIGHT_OUTPUT_PATH env var) lives at one boundary, and every other piece of the pipeline stays portable.

Concretely:

  • The workflow YAML names jobs and steps, sets MAGUS_* 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 reads env vars whose names are generic (MAGUS_INSIGHT_OUTPUT_PATH, not GITHUB_STEP_SUMMARY). 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, charms, and env-var naming are portable.

Workspace references: workspace://path and friends

Project paths accept four equivalent forms; only one canonical form survives on the wire:

.                     # bare path, shell convention
pkg/foo               # bare nested path
workspace://.         # explicit root, scheme form
workspace://pkg/foo   # explicit nested path, scheme form

The CLI accepts all four at every project-arg surface. The canonical form that appears in error messages (where a user can copy-paste it back as a command) is workspace://pkg/foo - rendered via types.WorkspaceRef. The form that appears in logs, Mermaid labels, and table cells is the bare path (pkg/foo) - rendered via types.ProjectLabel. The browser shows example.com until you hover; magus shows pkg/foo until you pipe it.

Both forms route through types.ProjectRef{Path, Dir} so the two helpers share one definition. New rendering rules go on the struct's methods, and both helpers pick them up automatically.

Release-signing key rotation

The release public key has a few intentional, reviewable copies: the binary's internal/selfupdate/release.pub, docs/gen/install, the setup action, and the download guide. TestReleaseTrustAnchorMatchesInstallerAndCI makes a mismatch between those copies fail CI. Never put the private Ed25519 seed in the repository or a release artifact.

For a planned rotation, create a compatibility release signed with the current key but built with the replacement public key. Users who install that release can verify later releases signed by the replacement key. Then move the MAGUS_SIGNING_KEY secret to the replacement private key and publish future releases with it. Update every public-key copy above in the same change, run the trust-anchor test, and dry-run the installer before publishing.

A compromised key cannot be revoked from binaries that already trust it: those binaries cannot distinguish a legitimate replacement from an attacker-signed one. Treat that as an incident: stop using the compromised 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.

Operation

A single tool-native command a target composes; the middle of the work hierarchy (Spell to Operation 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 (ref1a2b3c, "ref" for short) for one target execution's captured output; it appears on each target's line, and magus query output ref1a2b3c 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-cache.

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). See insight.

Hotspot

An insight lens: edit frequency times complexity, the prime refactoring targets. The project view heat-colours 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.

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.

Surface

One of the console's apps (Log Viewer, Graph Explorer, Dashboard, Activity Trail, Settings). "Surface" 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. 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 a surface at a running daemon. The daemon serves the console from its own loopback origin, so the link is that origin plus the surface 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, a surface reads only what rides in the link itself. See reference/console.

See also

Conventions

Documentation conventions

A few conventions run through every page on this site. This page is the key.

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.

Command synopsis notation

Every synopsis on this site and in magus <verb> -h and the manpages uses the same five marks. This is the whole vocabulary:

notation means example
<value> required; replace it magus run <target>
[thing] optional; omit the brackets if you use it magus ls [flags]
<a|b|c> required, and one of these exact words magus completion <bash|zsh|fish|powershell>
<value>... repeatable; one or more, space separated magus describe file <path> [<path>...]
word[s] the s is optional - both spellings work magus describe spell[s]

The last one is the only place square brackets do NOT mean "optional argument": spell[s] means magus describe spell and magus describe spells are the same command, not that s is a separate thing you can pass.

Combining them reads left to right, so [<path>...] is "optional, and if you give it, one or more paths":

magus run <target> [flags] [project...]
magus describe file <path> [<path>...] [flags]

[flags] and [args] are categories rather than placeholders - there is nothing called "flags" to substitute. Run the command with -h to see which it accepts.

A bare -- ends magus's own arguments; everything after it is passed through untouched to whatever the target runs:

magus run test libs/foo -- -run TestX

Values are written --flag <value> in synopses, but every magus flag also accepts --flag=<value>, -flag <value> and -flag=<value>. Pick whichever reads better; they parse identically.

Some flags take a comma-separated list, which is written as one value. Spaces around the commas are trimmed and empty entries are ignored:

magus status --probe=mcp,liveness

A few take a structured value spelled key=<value> pairs, comma separated. Where a pattern is accepted it is always the same three types:

magus watch --ignore type=glob,pattern='**/node_modules/**'
magus where --filter type=regex,pattern='^libs/'

Shell commands

Command blocks omit the shell prompt - copy the whole block as-is, no leading $ or > to strip. A # comment on or after a line shows expected output or an aside:

magus version
# magus 0.4.2

Windows examples are shown in PowerShell and labelled as such.

Runnable examples

Some Buzz code blocks are live: a Run button appears in the corner and executes the snippet in the in-browser playground via WebAssembly - no install needed. Blocks without the button are illustrative only. (With JavaScript off, every block is plain, copyable text.)

Admonitions

Call-outs are rendered from GitHub-style alert blockquotes and carry a colored accent per type:

Note

Context worth knowing, but not a warning.

Warning

Something that can bite you if ignored.

The types are NOTE, TIP, IMPORTANT, WARNING, and CAUTION.

Footnotes

An aside that would break the flow inline is written as a footnote: a bracketed superscript like this1 links to a short note at the foot of the page, which links back. The generated module reference uses them to flag methods that also exist in Buzz's own standard library without cluttering each signature.

Reach for a footnote when a sentence needs a source, a caveat, or a pointer that would derail it inline: a citation or external reference, an edge case that qualifies the claim, or a "see also" that is worth keeping but not worth interrupting the thought. Prefer a footnote over a parenthetical that runs long, and over dropping the detail entirely.

Code-block titles

A fenced block can carry a filename or label in a small caption bar above it, so you know which file a snippet belongs in (for example a magusfile.buzz).

Diffs

A ```diff block shows a change: added lines (leading +) render as a green band, removed lines (leading -) as a red one.

 export fun ci(ctx: magus\Context, args: [str]) > void {
-    ctx.needs(lint);
+    ctx.needs(lint, test);
 }

Auto-generated pages

Pages built from source - the module reference, the spell reference, the man pages, and the configuration reference - carry an auto-generated chip. Edit the generator, not the page; a hand edit is overwritten on the next build.

Reading time

Longer pages show an estimated reading time near the top. It is a word count of the source, not a tracker - nothing is measured about you.


  1. Authored as text[^label] in the prose, with a matching [^label]: note line anywhere in the file. ↩︎