Documentation
¶
Overview ¶
Package agent owns the two provider-neutral halves of Magus's agent surface: the agent-skill artifact (this file - command packages supply embedded source files, and this package renders, installs and verifies the generated surface without knowing about a particular CLI host), and the guard verdict wire contract (guard.go, which lives here because `package main` cannot be imported, so a parity check outside cmd/magus would otherwise have to restate it).
Index ¶
- Constants
- func FormerNames(name string) []string
- func FullTwinName(base string) string
- func GuardDecisions() []string
- func GuardSurfaces() []string
- func IsFullTwinName(name string) bool
- func WellKnownSkillDirs() []string
- type AgentSkill
- type Catalog
- func (c *Catalog) AgentsBlock() string
- func (c *Catalog) CheckStatuses(dir string) []Status
- func (c *Catalog) EmbeddedSkills() ([]AgentSkill, error)
- func (c *Catalog) PlanSkillTree(dir, dest string, v Variant) ([]string, error)
- func (c *Catalog) PruneSkillTree(dir, dest string) ([]string, error)
- func (c *Catalog) Render(def AgentSkill, v Variant) (AgentSkill, error)
- func (c *Catalog) RenderSkill(skill AgentSkill) []byte
- func (c *Catalog) RenderedSkills(v Variant) ([]AgentSkill, error)
- func (c *Catalog) Section() string
- func (c *Catalog) SkillBytes(name string, v Variant) ([]byte, error)
- func (c *Catalog) SkillDigest(name string) string
- func (c *Catalog) SkillTar(dest string, v Variant) ([]byte, error)
- func (c *Catalog) StaleSkillDirs(dir, dest string) ([]string, error)
- func (c *Catalog) StampSkill(name string, body []byte, v Variant) []byte
- func (c *Catalog) VariantSize(v Variant) (int64, error)
- func (c *Catalog) WriteSkillTree(dir, dest string, force bool, v Variant) ([]string, error)
- type SkillRef
- type Status
- type Variant
Constants ¶
const AgentsFile = "AGENTS.md"
AgentsFile is the repo-root instruction file magus prints a managed block for but never writes, and the Status.Location CheckStatuses reports that block under - so a caller can tell the one location whose remedy magus cannot run.
const GuardSchemaVersion = 1
GuardSchemaVersion is the version of the verdict envelope every host glue parses, carried on the wire as schema_version. Bump it only when an existing field changes MEANING: adding an optional field that existing glues ignore is not a bump, and neither is adding a rule. A bump is expensive: a glue that meets a schema it does not recognize fails open, so every one of them must be re-downloaded before it guards again.
const GuardTemplateMarker = "magus-guard-template:"
GuardTemplateMarker introduces the version line each template carries, and is what a reader greps for in their own copy.
const GuardTemplateVersion = 10
GuardTemplateVersion is the revision of the hook templates a reader installs into their agent host.
The templates are the one shipped artifact with no self-correcting path. An installed skill is generated, stamped and regraded on every `magus doctor`; a hook template is COPIED into a host's config and then owned by its reader, so a fix magus makes to the source never reaches the copy, and nothing about the copy says how old it is. That is not hypothetical: a change to the guard's exit code turned every unfixed copy into one that judges a denied command twice, and on one host into one that fails open on every block. The docs were correct within the hour; every installed copy stayed wrong indefinitely.
A version rather than a content digest, because these files are explicitly the reader's to edit ("adjust to taste"). A digest would flag every legitimate customization and be switched off within a week; a marker survives editing and still answers the only question worth asking: is this copy older than the fix?
Bump it whenever a template's BEHAVIOR changes - not for a comment or a rewording. TestShippedTemplatesCarryTheCurrentVersion makes the bump total: every template must be re-stamped or the build fails.
2: docs/guides/integrations/agents/opencode-plugin.ts unconditionally passed the attribution flag, which no released binary accepts (v0.3.0 predates it) - an older binary rejected it, the plugin's judge() got unparsable stdout, and every verdict silently allowed. The sh templates already retried without attribution on exactly this failure (magus-guard-command.sh's guard()); the plugin now does the same.
3: that flag is now --agent-name (was --host, which read as a network host) and the templates' variable is GUARD_AGENT_NAME (was GUARD_HOST). A copy still passing the old spelling degrades rather than breaks - the retry that version 2 added drops attribution and keeps the verdict - so an unbumped copy loses the activity trail's host label, not its guard.
4: two changes, neither released before this. The path surface learned to render a deny arm - it handled only advise, so a deny rendered EMPTY while magus exited 2, and both scripts read empty-output-plus-nonzero as a broken guard and exit 0, which every host takes as allow. And the templates now resolve ./magus before PATH: an older PATH binary does not fail when it lacks a rule, it reads the config key that ARMS the rule as unknown and answers pass, so the guard enforces nothing at exit 0.
7: the advise arm is now suppressible. A host whose pre-tool-use hook REJECTS the context key - treating it as an error and then failing OPEN - was not merely ignoring an advisory, it was disarmed by one for that call. A copy that predates this keeps sending it and keeps failing open, which no verdict anywhere reveals. Suppression is opt-in per host (GUARD_NO_ADVISE), so the rendered response for a host that keeps the arm is byte-identical to version 6. Which hosts need it is recorded in their own guide pages, not here.
8: the templates find the binary by walking UP to the magusfile instead of testing `./magus` in the process's own directory. A hook runs in the host's SESSION directory, which is not always the workspace root, and every copy that predates this silently judges with PATH's binary there - or, where PATH's copy cannot load the workspace, does not judge at all. Version 4 established preferring the workspace's binary; this is the half of it that was only true from the root.
9: the notice a template prints when the binary is found but cannot judge now names the evidence - which binary path it resolved, that binary's version, and the error it actually printed - instead of guessing. The wording it replaces blamed "too old for session hook, or cannot load this workspace", and the second half is not a cause: the deny rules need no workspace, so a reader who took the sentence at its word went looking for a workspace problem that was never there. A copy that predates this keeps sending them, which is why this bumps even though enforcement is unchanged. 10: the two notices a template prints when the guard is not enforcing are held to one firing per session, keyed on a TMPDIR marker rather than on magus, which is the thing that is missing when they fire. Measured over recent sessions: 2,741 unavailable and 653 could-not-judge firings, 99% of them same-session repeats, and one session took 913. A copy that predates this keeps sending all of them, and a reader who has learned to skip the notice skips the one that mattered too.
const LocalSkillName = "magus-local-development"
LocalSkillName is reserved for a workspace's OWN rules, and magus must never ship a skill by that name.
Nothing in the installer or the verifier knows this constant, and that is the design rather than an omission: install writes only the names in skillSources, and grading skips any file magus did not write, so a name magus does not ship is untouchable by structure rather than by exception. The reservation exists so it stays that way - a future shipped skill called magus-local-development would, on the first --force after the upgrade, silently overwrite every early adopter's file. TestLocalSkillNameIsReserved is what makes the promise enforceable.
const SkillVersion = 51
SkillVersion changes when the installed skill contract changes. It is part of the generated provenance and lets verification explain stale installs.
37: a --simple install also writes each skill's always-full <name>-full twin (see fullTwinSuffix), and both entries carry a cross-reference in their description. The content digest cannot catch this on its own - it hashes the SOURCE bodies, which did not change, while what an install writes did. 38: magus-commit-composition - restructuring an unpushed branch into reviewable commits from project ownership, declared outputs and blast radius. 39: `magus graph verify` is gone; the installed copies are graded by `magus doctor`'s agent skills check, which every skill that named the old verb now points at. 40: magus-multi-agent learns two lease failure modes observed in the field: a worker's actual base can differ from the handed checkpoint (verify it, or materialize and re-record), and a lease whose environment cannot execute magus gets ROOT-DEFERRED validation up front. 41: the vocabulary drops "unit" for a row of the ledger, in the skill, the ledger table, and the tools it names. 42: the lease runtime reaches the skills - magus-multi-agent teaches ledger register, environment enrollment and the guard's deny/advise split, attention events for a blocked worker, and the session audit of what a lease ran; both it and magus-vcs-hygiene read `magus diff --impact` before landing. 43: the session CLI family (`magus session`, `session attention`, `session dispose`, `session hook`, `session notify`) replaces the sessions/attention/ notify/hook top-level verbs in the skill text; there are no compat aliases. 44: enrollment moves to the W3C channels - a worker exports BAGGAGE=magus.lease=<id>, plus TRACEPARENT and magus.spawner when its host has them. The magus-specific environment variable it replaces is gone. 45: the concept is a LEASE - the ledger row, the `--lease` flag, the BAGGAGE member magus.lease - and magus-delegate-multi-agent is renamed magus-multi-agent. Nothing answers to the old names. 46: verification instructions become proof obligations wherever the evidence is cheap - a gate you added is shown FAILING before its green counts (magus-run), drift is proven by a second regenerate rather than by reading the diff (magus-vcs-hygiene), fan-out waits on the collision check REPORTING the write sets disjoint and the root reopens a lease's evidence itself (magus-multi-agent), and every audit finding carries the command that reproduces it (magus-context-audit). 47: magus-multi-agent names `magus graph build` as the prerequisite for its central evidence command - in a fresh worktree `magus refs --occurrences` answers "unknown, not absent" rather than reporting the edit sites, and the partition is then built on a silence. 48: magus-query teaches the doc-section layer - every markdown heading is a `docsection` node, so prose is retrieved with `magus query kind=docsection` (a `path#anchor` pointer to one passage) instead of reading the whole file. Pairs with the guard's doc-search advisory on a cat/grep of a `.md`. 49: the query grammar teaches operators - kind=spell (match), kind!=op (exclude), id=~regex (regex) - with the `:`/`-kind:op` spelling kept as a compat alias. `=` reads as a match over a structured graph, and `!=` removes the flag collision the dash negation carried. 50: magus-handoff-journal gains the `elimination` record: a hypothesis an investigation killed, carrying the why plus an `excerpt` of the evidence that killed it. The excerpt is required because an output ref resolves only from the checkout that minted it, which leaves the ref beside it a best-effort handle. 51: magus-handoff-journal teaches what `put` writes: the fields the caller sends, keeping the rest, so refreshing a status does not drop the body. Clearing a field or changing a type is a delete and a create, and `allow_missing: false` (CLI `--amend`) says the entry is meant to exist.
Variables ¶
This section is empty.
Functions ¶
func FormerNames ¶
FormerNames returns the names a skill previously shipped under, oldest first, or nil for one that has never been renamed.
func FullTwinName ¶
FullTwinName returns the always-full twin's name for a base skill name.
func GuardDecisions ¶
func GuardDecisions() []string
GuardDecisions returns every decision a verdict can carry.
func GuardSurfaces ¶
func GuardSurfaces() []string
GuardSurfaces returns every input the guard judges.
func IsFullTwinName ¶
IsFullTwinName reports whether name is a full twin rather than a primary skill entry. Callers that enumerate an INSTALLED tree need this: a simple install writes both, so a name-by-name comparison against the canonical skill list sees twins it would otherwise call unrecognized.
func WellKnownSkillDirs ¶
func WellKnownSkillDirs() []string
WellKnownSkillDirs returns the conventional locations verification probes. Installation remains explicit: callers must name every destination to write.
Types ¶
type AgentSkill ¶
type AgentSkill struct {
Name string
Description string
Body string
// Variant is what THIS entry was actually rendered as, independent of the
// Variant requested from RenderedSkills. A simple request also returns
// each skill's always-full twin (see fullTwinSuffix), and the twin's own
// stamp must say "full", never "simple" - StampSkill and friends key off
// this field, not the request. Meaningless on an unrendered definition
// from EmbeddedSkills.
Variant Variant
}
AgentSkill is Magus's provider-neutral skill contract. Renderers own provider metadata and file shape; the Markdown body only explains the workflow.
type Catalog ¶
type Catalog struct {
// contains filtered or unexported fields
}
Catalog binds the skill source assets embedded by the application to the schema version of that application. It contains no CLI parsing or rendering.
func Default ¶
Default returns the catalog over magus's own embedded skill sources at the current knowledge schema version.
It is the ONE construction every shipping caller wants - the CLI's `agent install`, the docs generator, and anything that needs to know which skills magus ships. NewCatalog stays exported for a caller supplying different sources, which in practice means tests.
func (*Catalog) AgentsBlock ¶
AgentsBlock returns the managed magus guidance wrapped in its begin/end markers, ready to paste into a repo's AGENTS.md.
Magus deliberately has no counterpart that WRITES this into AGENTS.md, and the reason is the same one that makes an installer appending to your .bashrc a bad neighbor: the file belongs to the developer, the merge logic is never as careful as it looks, and re-runs leave cruft nobody wrote and nobody can audit. Instruct, do not mutate. Reading AGENTS.md back to grade the block's stamp (CheckStatuses) is a different thing and stays.
func (*Catalog) CheckStatuses ¶
CheckStatuses inspects known skill locations plus AGENTS.md, returning only locations with a Magus install. The result order is deterministic.
func (*Catalog) EmbeddedSkills ¶
func (c *Catalog) EmbeddedSkills() ([]AgentSkill, error)
EmbeddedSkills returns every embedded skill's canonical, unrendered definition, in name order: Body carries the raw template source, exactly as checked in. Variant is meaningless on these entries - render one for a specific permutation with Render, or get the full install-ready list (twins included) with RenderedSkills.
func (*Catalog) PlanSkillTree ¶
PlanSkillTree returns the paths WriteSkillTree would write, writing nothing.
Shares checkDestination with the writer, so a plan cannot name paths the run would not. The --force conflict check is not repeated: a plan reports what a successful run produces.
func (*Catalog) PruneSkillTree ¶
PruneSkillTree removes the stale skill directories under <dir>/<dest> and returns what it removed.
Never a side effect of installing. Install writes files it can name in advance; this deletes files the caller has not seen, chosen by a rule that lives in a binary they may have just upgraded. Those are different enough acts that the second one asks - so install reports what is stale and names this, and a person decides. The stamp makes the deletion safe; it does not make it expected.
func (*Catalog) Render ¶
func (c *Catalog) Render(def AgentSkill, v Variant) (AgentSkill, error)
Render renders def's raw template Body for v, returning a new AgentSkill whose Body is the final Markdown and whose Variant records which permutation produced it - RenderSkill and StampSkill key off that field on the RESULT, never off an ambient caller-supplied variant, so a mixed batch (see RenderedSkills) stamps every entry correctly regardless of what was requested. def is not mutated.
func (*Catalog) RenderSkill ¶
func (c *Catalog) RenderSkill(skill AgentSkill) []byte
RenderSkill renders the open Agent Skills format. Other provider renderers can consume AgentSkill without duplicating the source definitions.
func (*Catalog) RenderedSkills ¶
func (c *Catalog) RenderedSkills(v Variant) ([]AgentSkill, error)
RenderedSkills returns every embedded skill rendered for v, in name order - the install-ready list SkillBytes, SkillTar, and WriteSkillTree all write. When v is VariantSimple, each skill is followed immediately by its always-full <name>-full twin (see fullTwinSuffix), so every one of those callers gets the dual install for free. VariantFull adds no twins: the primary entry already IS the full form, so a twin would only duplicate it under a second name.
func (*Catalog) SkillBytes ¶
SkillBytes returns the rendered+stamped bytes for one named skill. Pure rendering: callers decide what to do with the bytes (write to a file, embed in a tar, hash, log).
func (*Catalog) SkillDigest ¶
SkillDigest fingerprints ONE skill: its body, name, and description.
A catalog-wide digest restamped all 26 installed files and all 16 reference pages whenever any skill changed, so a diff could not show which one moved. Both permutations of a skill still share this value - see StampSkill. The catalog-wide contentDigest survives for the AGENTS.md block, which routes to every skill by name and so does depend on the whole set.
func (*Catalog) SkillTar ¶
SkillTar returns a tar archive of every embedded skill at the path `<dest>/<skill-name>/SKILL.md`. The archive is reproducible: tar headers carry a fixed mtime and deterministic mode bits so byte-equal output is possible when the binary and skill content are unchanged. Piping the result to `tar -xf - -C <dir>` is the supported way to install skills outside the workspace root - the shell sees the command, the sandbox sees it, and the user gets to choose the destination.
func (*Catalog) StaleSkillDirs ¶
StaleSkillDirs returns the installed skill directories under <dir>/<dest> that magus wrote and this binary no longer ships, as <dest>-relative paths.
Detection is separate from removal, and both halves matter. A generator that owns its writes but not its deletions leaves a mess that outlives every reason for it: renaming a skill leaves the old directory installed, still stamped, still loaded by the host, still teaching whatever it said the day it was orphaned. Nothing reported it either - a drift gate compares the files a generator DECLARES against what it wrote, and an extra file is in neither set. This is what makes it reportable; PruneSkillTree is what acts on it, and only when asked.
The stamp is the authority on what is a candidate at all, and that is the whole safety story: magus considers only files it can prove it wrote. A directory with no SKILL.md, or a SKILL.md without the generated footer, is someone's hand-authored skill sitting in the same folder (magus-skill-authoring and a workspace's own magus-local-development both live there) and is never a candidate even though the catalog does not name it.
func (*Catalog) StampSkill ¶
StampSkill injects provenance frontmatter and appends a generated-by footer.
The stamp names the variant but keeps the SOURCE content digest, deliberately: both permutations come from one body, so they must report the same digest and go stale together. A per-variant digest would let a simple install look current against a source its full sibling had already outgrown.
func (*Catalog) VariantSize ¶
VariantSize returns the total rendered size of every skill's PRIMARY entry in v, stamp included, so a caller can state the context cost of an install without performing one. Deliberately excludes RenderedSkills' full twins - reportContextCost uses this to compare "what you have" against "what the other variant would be", and a twin-inclusive total would make VariantSize (VariantSimple) larger than VariantSize(VariantFull) precisely because simple installs more files, silently inverting the comparison it exists to answer ("would --simple cost less").
func (*Catalog) WriteSkillTree ¶
WriteSkillTree renders the standard Agent Skills format into <dir>/<dest>. The destination must be a path relative to <dir>; absolute paths are refused so magus never silently writes outside the working tree. The caller is responsible for that guard at the CLI surface; this method enforces it for safety.
type SkillRef ¶
type SkillRef string
SkillRef is a checked reference to a skill magus ships: the NAME, and nothing else.
It exists so a prompt or a message can point a reader at a skill without carrying a copy of it. A copy would be a second definition free to drift from the installed one, and it would spend the reader's context on what their tools already loaded - so what travels is the name, and this type is the guarantee that the name resolves.
func MustSkill ¶
MustSkill returns a reference to the named shipped skill, and PANICS when magus ships no such skill.
Panicking is right here for the same reason regexp.MustCompile does it: every caller passes a literal, the catalog is embedded at build time, so a bad name is a fact about the binary rather than about anything that happened at runtime. Failing at init means a renamed skill breaks the build's own tests instead of silently leaving a reader pointed at a skill they cannot load - which is a failure that renders perfectly and helps nobody.
The name must be the CANONICAL one. A former name still resolves for an already-installed copy but reads as a typo to anyone looking it up, so it is refused here.
type Variant ¶
type Variant int
Variant selects which permutation of a skill body to render.
BOTH PERMUTATIONS ARE CURATED, and that is the whole design. The simple one is not a summary, a truncation, or a model-generated paraphrase: there is exactly one human-written body per skill, and its author brackets the spans that only the full permutation keeps. So the two can never come to describe different behavior, they share one content digest, and they version together - which is the property a second hand-maintained file could not give.
The reason to offer a shorter one at all: a skill is a bet about what the reader cannot infer, and that bet ages. Models keep getting better at inferring the why, so the rationale that earns its context today is the same text that is dead weight in a year. Rather than let the skills quietly become bricks, the choice is a flag - and re-asking "does this still earn its context?" is the audit, not a rewrite.
A {{if .Full}} branch alone caps how short the simple permutation can get, because it can only SUBTRACT. Simple is "everything minus the full-only branches", so a passage BOTH permutations must express sits in the shared text at whatever length the full form needs, and the only way to shorten it further is to drop it entirely and lose the step. The {{else}} arm is the one construct that reaches it: full keeps the long wording, simple gets the short one.
Measured 2026-07-31 across the ten shipped skills: simple came out 20.3% smaller than full, on 137 full-only branches against only 28 {{else}} arms. The most prose-heavy simple forms are magus-run (82.6% prose) and magus-vcs-hygiene (91.0% prose). The headroom is in the wording of the shared text, not in the tables. An earlier version of this comment blamed the tables; that was wrong, and it pointed authors at the one part of the page they should not touch.
const ( // VariantFull is the default: every mechanical step spelled out, plus the // rationale that says why each one is the right move and what goes wrong // otherwise. VariantFull Variant = iota // VariantSimple sheds ENUMERATION and keeps JUDGMENT, for the most capable // readers - not the least. A capable reader can re-derive the mechanical // steps from the tool surface on its own; what it cannot re-derive is // which failures are silent, what is load-bearing, and where a judgment // call is being asked of it. So simple is a bet ON the reader, not a // lossy compression - which is why the split is a judgment an author // records, and why anything a step cannot survive losing belongs in the // unmarked core instead. VariantSimple )
func (Variant) Full ¶
Full and Simple let a skill body branch on the permutation with {{if .Full}}. They exist because text/template cannot reference a package constant, so the predicate has to hang off the value being rendered.