magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
16 min read

Agents

magus knows more about a workspace than any agent can rediscover by reading files: the project DAG, every target's inputs and declared outputs, which files are generated, what a diff affects, where churn and coupling concentrate.

The agent surface exposes that through three artifacts, and one rule governs all of them. Every tool answers a question from declared sources; none of them decides, plans, or injects itself into the agent's context. Answering is the tool's job, deciding is the model's.

artifact answers freshness
MAGUS.md WHAT is in this workspace (targets, counts, anchors) regenerated with the workspace (magus describe graph -o markdown)
Skills HOW to use the magus tool surface ships with the binary; versioned, drift-checked
MCP surface live answers (query, run, explain, logs) host-managed; clients discover it at task start

The split is deliberate: skills never mention workspace specifics, so they go stale only when the tool surface changes, and that staleness is detectable.

MAGUS.md is per-project as well as per-workspace. A nested project can commit its own, generated by its generate target and scoped to that project's targets, so an agent working inside one project gets a routing index sized to it.

Hosts, not LLM providers

An LLM provider supplies a model; a host (or harness) decides which skills, MCP tools, hooks, and instruction files that model receives. Claude Code, Codex, Cursor, and OpenCode therefore differ at the host layer, even when they use models from the same provider. magus normalizes its guidance and guard verdict, not those host capabilities.

Set up your host

A full setup is two steps: install the guidance where the host reads it, then wire the guard hook. Both are on your host's page.

host skills guard
Claude Code .claude/skills/ shipped PreToolUse scripts via harness spell
Codex .agents/skills/ + AGENTS.md shipped PreToolUse scripts via harness spell
Cursor AGENTS.md only one self-contained script
OpenCode .opencode/skills/ TypeScript plugin (harness spell is skills-only)
Any other host wherever it reads them a collaborator-owned harness descriptor or small adapter

What you wire is yours. Prefer a harness spell selected from the root magusfile (import "ghcr.io/egladman/magus/spells/<id>"; then magus\harness.provider(<id>), with the spell declared in magus.yaml and pinned in magus.lock; Remote spells explains the pin). Wire several providers when you bounce between hosts; each call appends.

To adapt a Buzz harness without modifying Magus source, copy the shipped spell into the workspace and declare the copy as a path: override of the registry path in magus.yaml. The import and magus\harness.provider(...) stay as they are. Edit the workspace Buzz, then magus agent harness apply / verify. That ownership switch is documented in the workspace-rules skill under "Adapting a Buzz harness" and in Recurring guard friction. Additive deny/advise that is not host-shaped stays in magus\guard.shell({...}).

magus agent harness apply (no --id) applies every magusfile-wired provider; --id <id> targets one spell. verify reports whether the fragments are still present and that the config invokes magus. A host Magus has never heard of can ship its own harness spell without a Magus release, by the same ownership switch as adapting a shipped one; a portable adapter remains an option where a native event contract is not available. Doctrine records that trade and what it costs you.

This repository as a worked example

The magus repository wires every shipped provider on itself and commits what that writes, so the wiring can be read whole rather than assembled from the pages above. Its maintainers do not use every host; the files for the others are kept as examples. magus agent harness verify reports whether each is still what its spell writes, and the root tests grade the committed hook configs against each host's vendored schema.

file what it shows
magusfile.buzz the four magus\harness.provider(...) calls that select hosts
.claude/settings.json Claude Code guard hooks, as the spell writes them
.codex/hooks.json Codex guard hooks
.cursor/hooks.json Cursor hooks, all routed through one script
AGENTS.md the managed skills block between its magus:skills markers

Installed skills are not committed, because magus agent install regenerates them from the binary. The two under .claude/skills/ that are committed are written by hand for working on magus itself.

The shared reference pages sit behind those: Skills for the install surface, The guard for what is denied and why, Guard hook templates for the files Claude Code and Codex run, Session load adapters for reading a host's own session log back into magus, Attention hooks for magus session notify, and Jobs and leases for the surface an agent uses when it fans work out across several.

When a session loses its history

A host that compacts a long session replaces what happened with a summary of it, and the model works from that summary. The branch it is on, what it has already changed, which rules it agreed to, and the failure it was in the middle of all survive only as a retelling, and nothing in the transcript says how much each retelling lost.

magus session --brief answers that with state instead of prose: branch and revision, commits not yet on the base ref, the dirty tree split by the same classifier magus describe file uses, the live leases with the command that binds each one, the last recorded run's failures with the ref that holds their output, whether any hook config here invokes magus, and where this workspace's rules live. When recurring guard evidence needs a human decision, it adds one bounded line pointing to magus doctor (its recurring-guard-denials check). Every line is read off the disk on the call, so none of it can degrade; it restates no rule, because a rule copied into a context block is a second copy to go stale.

magus prints it and your host places it. Wire magus-rehydrate.sh to whatever event your host fires after compaction: SessionStart with matcher compact on Claude Code and on Codex, where REHYDRATE_FORMAT=json wraps the text in the reply Codex parses. OpenCode has no hook to point at a file, so its plugin pushes the same brief into the compaction prompt itself. Cursor is the one host that cannot: preCompact returns a message for the person and nothing for the model, so there run the command yourself and read the same thing. The review belongs here rather than in a Stop hook: a stop event can record evidence, but cannot reliably inject an actionable message into the model's next context.

Parity across hosts

Every host gets the same RULES - they come from one binary, and none of them is per-host. What differs is how much of a verdict a host's hook surface can carry. Claude Code is the reference contract: parity means every supported adapter exercises the same guard decision cells and preserves the verdict semantics. It does not mean vendors expose the same tools, payloads, or lifecycle events.

Manual host end-to-end harness

The ordinary checks are deterministic: vendored schemas validate configuration and reply shapes, and the fixture-driven transport cases execute every guard decision cell against the shipped adapters. That proves the guard's decision and adapter behavior for controlled events; it does not prove that a locally installed vendor client discovers configuration or dispatches a real hook.

That separate, opt-in end-to-end check currently proves one narrow boundary: host discovery and delivery of a denied command. It is not a general host certification or a replacement for the deterministic cases:

MAGUS_HOST_E2E=1 magus run host-integration docs/guides/integrations/agents -- \
  host-e2e/claude-code.json host-e2e/codex.json host-e2e/opencode.json

It is deliberately not part of ci or magus affected ci. The runner makes a temporary Magus workspace and applies each supplied descriptor's isolated host configuration, then asks the host to use Bash for the denied command. The guard must intercept that call and render a deny; a broken hook can affect only the temporary workspace. A missing selected host is reported as a structured skip, while an installed host that never dispatches its hook is a failure. A run in which every selected host skips is inconclusive and exits nonzero rather than looking like a pass. Failures retain their workspace with the trace, generated configuration, and capped host stdout/stderr logs for inspection. Successful workspaces are deleted by default; set MAGUS_HOST_E2E_KEEP=1 to retain them deliberately.

It can use local authentication and quota, so the environment variable is a second explicit confirmation. The descriptor is the extension seam: a user can add a host without changing the runner by declaring its binary, isolated setup, launch command, and evidence contract. A descriptor must represent a real host dispatch path; the runner never substitutes a fixture and calls that an end-to-end pass.

command rules declared-output rule deny advise manual E2E: command discovery and block dispatch MCP call rules
Claude Code yes yes yes yes yes wired, rule-empty; live pending
Codex yes yes yes yes yes wired, fixture-verified; live pending
Cursor yes yes yes yes unsupported not wired: event exists, payload does not
OpenCode yes yes yes yes yes not wired: sees call, tool name unconfirmed

"Fixture-verified" means the adapter executed against this binary with a controlled host event on stdin. "Manual-E2E-proven" means a locally installed host discovered an isolated configuration, dispatched the git stash command hook, and delivered its deny. Neither label proves filesystem-path delivery, MCP delivery, or lifecycle delivery unless that cell says so; those remain pending. "Wired, rule-empty" means the transport carries a verdict end to end but the guard has no MCP-specific rule yet, so every call passes - not because the channel is silent, but because nothing has been asked of it.

Both decisions now reach the model everywhere, which they did not until recently: one host was sent no advisory at all, one delivered it to the person on stderr, and one logged it. What still differs is WHEN. A host whose gating event carries a message only with a denial has to send the explanation on its post-tool event instead, which means the call has already run and that magus was asked about it twice, leaving two rows in the activity trail where the other hosts leave one.

What each host carries

One row per job magus does through a host event. A cell names the event that carries it, or the reason nothing does. This is the intended host capability mapping, not a claim that the manual command/deny E2E run has proven delivery of each row. Path, MCP, and lifecycle delivery remain pending unless the evidence matrix above explicitly marks them proven.

job Claude Code Codex Cursor OpenCode
command guard, deny PreToolUse Bash PreToolUse Bash beforeShellExecution tool.execute.before
command guard, advise additionalContext additionalContext postToolUse.additional_context tool.execute.after
write guard, deny PreToolUse on the edit tools PreToolUse on the edit tools preToolUse tool.execute.before
write guard, advise additionalContext additionalContext postToolUse.additional_context tool.execute.after
MCP call guard PreToolUse mcp__magus__.* (rule-empty) PreToolUse mcp__.* (fixture-verified; live dispatch pending) not wired: payload unconfirmed not wired: tool name unconfirmed
post-compaction rehydration SessionStart compact SessionStart compact (JSON envelope) not expressible: preCompact returns user_message only not wired: only a PRE hook exists
checkpoint Stop Stop sessionEnd the session.idle bus event
lease provenance PreToolUse on the sub-agent tool no event carries the handed prompt subagentStart (unverified live) not wired: no confirmed tool id
read observation PreToolUse Read not wired not wired not wired
tool-failure hint not wired not wired not expressible: no response fields not wired
session-load adapter ships one ships one none written ships one

Two kinds of blank belong in that table and they are not the same. Not expressible is a host contract magus cannot reach through, and it is named rather than approximated. Not wired is a choice: a hook earns its place by changing a verdict or restoring state the model cannot otherwise get, and read observation changes neither: it records a path for the activity trail, which is worth one host's wiring and not four.

Everything else is additive. A host missing a file-write hook still gets every command rule, and adding one later changes no magus code, because the rules and the verdict already exist and only the wrapper is host-shaped.

Project references

A project reference is a workspace-relative path, written bare: pkg/api. That is the one spelling to read, to write, and to quote back. What a project arg prints is what a project arg takes, so a path out of any magus command pastes straight back into the next one.

Four spellings in, one reference outmagus accepts a workspace URL, a workspace-relative path, a dot-relative path or the bare root alias, and parses them all to one project reference. Human output prints the short path; machine output prints the workspace URL.AGENTSFour spellings in, one reference outACCEPTED AS INPUT, INTERCHANGEABLEPRINTED-O JSONworkspace://pkg/apipkg/api./pkg/apiworkspace://the root aliasSTEPparseOne referencepath: pkg/apiHuman outputpkg/apiMachine outputworkspace://pkg/apiONE CANONICAL FORM
form means
pkg/api that project, measured from the workspace root, from anywhere
./pkg/api, .. measured from the current directory
. the project the current directory is in
project:pkg/api the same project as a GRAPH NODE, for explain, query, path

Three rules cover the whole surface:

  • A bare path is absolute enough. It is measured from the workspace root, so it means the same project from any directory. Only the dot forms depend on where you are standing, and when you are standing outside the workspace entirely, --root <path> measures them from the workspace it names - so a command written once keeps working when it is run from somewhere else.
  • Never rewrite a path magus printed. Every surface prints the bare workspace-relative form - -o name, -o json, logs, error messages, Mermaid node labels - so it is already in the form the next command wants. Commands that take fuzzy search tokens rather than paths, such as magus where, take the same bare text.
  • Quoting a project back to a user: prefer whatever magus printed. The workspace root is the case that bites, because it is the one project whose path is a bare .; human output renders it as the repository's directory name, so a . never leaks into a sentence where it reads as punctuation.

The project: prefix in the last row is not a second path syntax. It is the kind-prefixed node grammar the graph commands use where kinds mix, alongside target:, spell:, and doc:; magus explain project:pkg/api disambiguates a name that could be either. Commands that only ever take a project - run, affected, ls, describe project - take the bare path.

Note

A workspace://pkg/api reference still parses and resolves to the same project, with a deprecation warning. It bought no reading the bare path did not already have, and it is not what magus teaches or prints any more. Drop the scheme wherever you find one written down.

Incremental review

Answer "I reviewed earlier - what changed since, and what do I need to look at now" without re-reading the whole workspace:

  1. At review time, record where you stopped: magus vcs checkpoint -o name prints the revision, or <revision>+<digest> when the tree was dirty - the digest says which dirty tree was reviewed, since the revision alone reads the same for every dirty tree built on it.
  2. Later, pipe the delta through the annotated view instead of reading a raw diff: git diff <revision> | magus diff - reports each changed file's reach, public-surface exposure, and referents - the surrounding code worth a second look, not just the literal hunks. magus diff refuses a git ref given positionally, on purpose - a swallowed ref once printed the reader's own edits as the answer - so the pipe form above is the only sanctioned spelling, and the refusal message says so.
  3. Reviewing through a diff session carries this further: per-hunk viewed marks key off content digest, not position, so a hunk that has not changed stays marked reviewed and one that has resurfaces on its own.

Handing work to other agents

The same checkpoint identifies a piece of work handed to another agent, and it is one leg of a wider surface: a declared set of jobs, a console Jobs view that draws them, and a spawn recorded but never judged. magus records what an orchestrating agent says it intends and enforces none of it: ownership is settled by diffing against the checkpoint each job was handed. Jobs and leases covers that loop.

MCP

An MCP-connected host gets the same verbs as the CLI plus run and log tools; magus describe mcp-tools lists all of them with parameters. See MCP for transport and token setup, and Knowledge graph for the graph the query tools read.

Skills prefer the MCP tools and fall back to the CLI, so they work in both connected and disconnected sessions. MCP availability is a host/integration concern: configure it once for the host that owns it. An agent checks magus status --probe=mcp when it needs to know, then uses the CLI fallback if the surface is unavailable; it must not manually start Magus merely to obtain MCP.

One tool carries state across sessions: magus_memory, a user-owned memory of per-repository records, each pointing at something magus can reopen, kept in the user state directory outside the repo and shared across branches and worktrees. It is pull-based - nothing is injected into an agent's context - and also available through magus memory ls|get|put|delete|verify. Use verify to surface stale, malformed, or broken linked entries. Captured build output is addressed by output references.

The CLI works without it

The CLI still reads the workspace, runs targets, uses the cache, and answers graph queries with no server running. What it lacks is MCP tool discovery, the warm graph and background indexes, structured output retrieval, and MCP-only capabilities such as the memory store.

An agent must not turn that into a blocker. At task start, or after an MCP error, run magus status --probe=mcp; if it is unavailable, use the CLI fallback. Restoring or changing host MCP wiring is a user-owned integration action, not ordinary agent work.

Why a server, not a wrapper

If an agent can already run magus query in a shell, what does an MCP server add? If the server only ran the CLI and handed back its stdout, the answer would be nothing, plus a round trip. The difference is the context the agent has to work with.

An agent working through a shell falls back on the habits it learned everywhere else: grep and cat over the files. Those return text matches. They do not return the project DAG, the declared outputs, the affected set, or the blast radius of a symbol, because none of that is written in the files - it lives in the graph the server keeps warm. So the agent reasons one layer below the structure it is trying to understand, and fills the gap by guessing: this file looks generated, these two packages probably change together. Those guesses are frequently wrong, and the agent has no way to check them.

The tools answer from what the workspace declares. Ask magus_describe_file about a path and it does not read the filename and infer; it checks the project's own globs and reports role: output with the note "generated: never hand-edit, regenerate." In one case an agent spent close to an hour working out whether a committed gen/ file was safe to edit - running generate repeatedly, planting sentinel writes, diffing timestamps - when one call to that tool would have answered it in a line.

So the server adds three things a shell-out leaves on the floor. Discovery: the tools arrive in the model's context with their descriptions and parameters, so the agent knows they exist without reading --help first. Shape: results come back structured and sized for a model, rather than a human-formatted table wrapped in color codes and pagination it has to scrape and pay for by the token. Ground truth: the server reports what the workspace declares, which the agent can rely on, rather than what a text pattern happened to match, which it cannot.

None of this comes from the protocol. A server that only shelled out would be a wrapper whether or not it spoke MCP. MCP is how the graph reaches the model; the value is in the graph and the curation.

agentsskillsagent installMCPMAGUS.mdAGENTS.mdhooksguardknowledge graphmemoryclaudecodexcursoropencode
Last updated (95680f58)
Earlier changes on this page (7)

Full history ↗ · Blame source ↗

Glossary

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.

Buzz

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

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.

Server

The background process a person starts with magus server start. It serves MCP, the console, background jobs and the warm knowledge graph, and adopts nested magus calls into one pool. See server.

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.

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.

Ownership

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

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it.

Job

The unit of delegated work, and one row of the job store: what an orchestrating agent handed out, with its goal, the checkpoint it was cut against, the paths it may write or must not touch, and the one check it runs. A job's holder is either a session, for work an orchestrator handed out, or the server, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. See doctrine.

A job is not a run. magus run build web is a run, and no job exists for it. A job causes runs: its check executes as one, and a server job records the invocation of its last one. Jobs are listed with magus ls jobs and in the console's Jobs view; runs are listed in the Runs view.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Lease

The grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it with magus job exec. A job is the piece of work; a lease is permission over it.

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.

Conventions

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.

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.