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

ADR 0005: deterministic answers

  • Status: Accepted, partly implemented. Each item below carries its own state.
  • Date: 2026-09-29
  • States: done is in the tree; in progress has a worker on it; planned is decided and queued; proposed is an idea not yet decided; not built was weighed and declined.

Context

A change of about 340 files was left uncommitted when a Codex session hit its usage limit halfway through reviewing it. That session had been finishing a Cursor session that ended the same way. Picking the work up in Claude Code, reviewing it with five parallel Go reviewers plus earlier TypeScript and Buzz passes (about 140 findings, nits included), and fixing it with isolated workers showed a pattern rather than a list of bugs: wherever magus had no deterministic answer, agents improvised, and wherever a guard rule had no evidence to judge with, it went quiet.

Measured during the pass:

  • Discovery went through the knowledge graph for 6% of reads and searches in recent Claude sessions (11% in this one, 20% for Codex, 2% for Cursor). Ranged reads took their line numbers from grep about 25 times as often as from the graph.
  • 657 whole reads of Go or Markdown files over 120 lines ran; 3 were denied.
  • 908 magus invocations across Claude transcripts were wrapped in coreutils timeout.
  • --concurrency 1, recommended to keep a busy machine calm, deadlocked every target whose body calls ctx.needs.
  • Two jobs whose holders died on a usage limit kept their leases for 38 hours and blocked new forks. No Stop or SubagentStop hook fired for the limit.
  • A raw git diff <snapshot> reported 43 untracked files as deleted; no magus command could say what had changed since a snapshot, untracked files included, or who owned each change.
  • Of 2,007 interpreter and heredoc calls, the largest groups were mining transcripts (260), rewriting one block (245), counting command output (216), building job records (125) and renaming by regex (46, 34 of them denied).

Decision

Principles

  1. The person drives. magus prints what to run and the person runs it. magus does not write host configuration or own a checkout.
  2. Determinism where magus holds the facts. The graph, the job store, the declared outputs and the VCS state answer questions an agent would otherwise guess at.
  3. Typed all the way. MCP results, Buzz members and JSON records are typed. An agent reads a type faster and more reliably than prose.
  4. The idiomatic middle. Keep a part only when a person sees a better outcome from it, reuse an existing mechanism before adding one, and push complexity into magus only when it buys the user a better experience.
  5. Do not fight the model. Agents keep their tools (Python included). magus offers a better path and denies only the shapes that cause harm, each deny naming the command it would accept.

1. Attention queue (the original Cursor brief)

Use case State
A repeat of a block the queue already holds does not toast again; waiting and permission toast only when the request is newly opened; a failure still toasts; a block with no queue row (no source id, no repository) still toasts done
A request row leads with the files the event carried (Where.Files), stored beside the lease and kept out of the request id, so a richer where-line never re-keys a row done
magus session attention and the console tile list those files; the agent's message stays the caption done
A permission request closes from the expanded row only (Dispose appears after the row is open); waiting keeps the composer; the reason stays optional; magus session dispose stays a typed id; no dispose-all done
The doctrine page gains the judgment entry and the "manual on purpose" row: disposing stays manual because a person has to have seen the subject done
Group rows that share a subject (same outcome, lease and files) once hooks send files; dispose stays per id proposed
A guard that failed open becomes a standing absence on status: the fail-open arm records a session notify --outcome diagnostic and status shows it proposed
Leave out: review-time scores, a prompt asking the person to explain an approval, a model watching for fatigue, planted canaries, an ask on every command. Test for anything added later: if it would teach someone to clear the queue unread, it does not go in not built

2. MCP

Use case State
One client tool runs a Buzz main(args) against the typed magus module; the per-verb tools it covers are removed done
Tool names drop the magus_ prefix: client, buzz, status, config, console, diff (hosts already namespace by server) done
Results travel as MCP structured content, byte-identical to the text copy; a skew warning drops the structured copy so a host cannot miss it done
client is bounded at 10 minutes when called directly and declares MCP task support, so a task-capable host runs it in the background and can cancel it done
Nested magus runs from a worker no longer inherit the worker flag, and their output no longer corrupts the worker's JSON reply done
The workers live in internal/interp/mcpclient and internal/interp/transform, sharing one stdlib allowlist done
magus status --probe=mcp checks only the loopback HTTP listener, so skills no longer gate the CLI fallback on it done
magus affected ci --plan -o json is typed; the Markdown summary exists only for -o template done
The guard grades only calls addressed to the magus server (mcp__magus__<tool>), and parses a client script to see its gate runs and job writes done
One merged package for both workers (research favored it) not built

3. Guard

Use case State
Deny magus wrapped in timeout/gtimeout; serve --timeout for run and affected, the bare command otherwise, advise for magus buzz; exempt goroutine dumps and long-running watchers done
gtimeout is peeled like timeout, so it no longer hides a command from every rule done
read-navigation keeps judging on a stale index by parsing the file itself and serving sed -n A,Bp per declaration done
A multi-file read is judged per file done
Claude's and Codex's own Read tool is judged like Cursor's (restated as cat or a ranged sed); a limit over 300 lines counts as a whole read done
Buzz files get a declaration map and ranged reads (83% measured precision, shipped as a deny) done
grep used as a reader ('func X' -A N) is denied and served magus refs X --definition --source (85% measured precision) done
Rules below 80% measured precision ship as advice, with the measurement in the catalog done
Every recursive literal grep sent to refs --text; wiring Claude's Grep and Glob tools not built
Precision: the interpreter-rewrite rule judges real write targets, not every tracked path a script mentions done
Precision: a heredoc that merely mentions .magus/logs is not a write to the cache directory proposed
A magus buzz script writing a tracked file is judged like the same write from Python done
The scratch-path deny text matches behavior (scratch edits outside the workspace are not tracked-file rewrites) done
Host policy: a recursive search of the run-log directory is denied; Windows paths match; rg -t values are not paths done
Advise on git diff <rev> while untracked files exist (they read as deleted) and on hand-rolled commit-tree snapshots planned
A Cursor hook that receives malformed JSON says so instead of allowing silently done
Precision: a read-only script that merely opens files under .magus/activity is not a cache-directory write planned
Precision: the check on worker briefs judges each command as if run in the orchestrator's checkout, so it refuses the one-command bootstrap (allowed only where no binary exists) and treats a backticked word such as cat in prose as a command being taught planned
Agent shell commands run with stdin at end-of-file: the guard's hook prefixes exec </dev/null; through the host's input rewrite, announced once per session and recorded in the trail. A subagent's grep ... $(cat <missing file>) | wc -l read the host's open stdin for three and a half hours, and the host backgrounds a timed-out command instead of killing it (reported upstream). It is the only rewrite the guard makes planned
Every verdict records its catalog rule name (about 21,000 recorded verdicts carry none, so they cannot be tallied) planned
A write to a tracked hack/ script by a session with no lease naming it gets an ask, not a silent pass: the host's approval prompt is the person's consent to a self-improvement change. Needs a magus\guard.ask member planned

4. Jobs, leases and change tracking

Use case State
magus vcs checkpoint --preserve is how a dirty tree reaches isolated workers (git switch --detach <handle>), and jobs are graded against the preserved commit rather than its parent planned
magus diff --rev <rev> compares the working tree with a revision, untracked content included planned
The checkpoint digest covers every untracked file and binary edits planned
magus diff and describe file name the lease that wrote each path and the job that owns it planned
A live job whose holder provably stopped (its transcript ends in a host error such as a usage limit) shows that evidence, with its checkout's dirty and unpushed state, in ls jobs, describe job and the fork refusal planned
"Stale" counts the lease's last guarded call, not only the row's last update planned
Ending leases automatically, heartbeat expiry, new session-end hooks not built
Widening a live job's write paths without resetting it to declared (today a re-fork resets it, found three times) proposed
Replacing a declared job whose dependencies changed is judged against its new dependencies (today it is judged against the stored row, so the fix is remove and fork again) done
A typed magus\activity member returning resolved guard verdicts, so scripts stop reading the activity store directly proposed

5. Engine

Use case State
A target that calls ctx.needs runs at --concurrency 1: the Buzz pool's slot is host-owned through a generic gopherbuzz hook, and every holder (pool, spell fan-out, proc server) is visible to the deadlock check done
The guard never executes bytecode an agent could plant in the working tree; the bytecode store lives in the user cache, keyed by a compiler stamp done
The bytecode cache invalidates on changes to already-bound and shadowing imports, keeps import types, private names and source lines done
git cat-file batch reads refuse a path containing a newline (it desynchronized every later read) done
! means one thing everywhere: within one declaration call, every exclusion removes matches of every glob in that call, whatever the order; a call made only of exclusions is refused, for file globs and target patterns alike. It is parsed once where it is declared into a typed glob that carries its exclusions, and every consumer (cache, clean, describe, doctor, watch, diff, merge driver, graph) matches through that one type. A review found the first version, a flat list of strings with ! entries, re-derived by hand in about ten places with three different meanings done
Every VCS backend honors exclusions. git: .gitattributes keeps positive output patterns and adds one !merge !linguist-generated line per tracked file an exclusion carves out, computed from the real files, because gitattributes has no exclusion and last match wins. Mercurial and Sapling: a carved file is routed to :merge ahead of the output patterns. jj, which routes no paths: vcs resolve picks the files through the same typed glob done
The hand-written runtime files move out of internal/interp/bindings/gen/ into their own package, internal/interp/bindings/ffi; the exclusion feature stays for tests and shared directories done
Mercurial's and Sapling's fallback no longer picks the magus merge tool for every conflicted file; the Mercurial-family backend is renamed to vcs/mercurial.go done
A fresh checkout bootstraps with one command, GOEXPERIMENT=jsonv2 go run -trimpath ./cmd/magus run go-build --no-cache .: the real target, magus cache bypassed, Go cache kept done
The root project declares its dependency on proto (it uses the generated Go code), which the graph lacked; split-change found it done
magus --root <dir> buzz reads the VCS of <dir>, not of the process's working directory done
vcs\ref() on a detached checkout returns null (str?), so every caller must handle "no name"; it throws only on a real VCS error done
The merge queue passes a change's projects ahead of the gate line's --, not after it as forwarded args (which selected every project) done
Cached Buzz bytecode is keyed by the build's content and authenticated with a per-user key, so a planted chunk is refused done

6. Harness and host configuration

Use case State
magus agent harness apply and remove are removed; magus describe harness prints the plan and the one merge command done
Codex reaches magus MCP through the per-checkout stdio ./magus mcp done
Shell hook glue is retired: every docs/guides/integrations/agents/*.sh is deleted, the three session-load scripts without a Buzz twin are ported, and every reference follows planned
Load the Buzz authoring skill when an agent writes a .buzz file. The rule (buzz-unbriefed) already exists but never fires on Claude Code (0 verdicts over 1,950 Buzz writes): the path and Bash hook entries must declare that they observe skill loads, and a skill load must count per agent, not per session planned

7. Scripts in hack/

tools/ moves to hack/ (planned). Reference scripts sit flat beside it with a hack/README.md index; each carries a read-only test block that buzz-test runs, so a reference cannot rot. Agents copy and adapt them; editing a shared script is a self-improvement change the person asks for.

Script Replaces State
Naming a name says what a script does and what it costs before it runs: the first word is a verb from a closed list (read-only ls, show, count, diff, summarize, example; writing rename, split, which dry-run until --apply), on-<where> prefixes run a command elsewhere, and libraries the magusfile imports are nouns. CI-only steps live in hack/ci/. The hack-scripts-start-with-a-verb lint rule enforces it and hack/README.md lists every script with its effect and cost. Five personas shaped it (plans/hack-script-naming-2026-09-29/) done
rename-symbol regex renames: resolves through the graph, refuses a stale index naming magus graph build, dry-runs refs --rename, applies on --apply done
summarize-transcript ad hoc transcript mining: calls per tool, refusals by rule, retries after a refusal, uptake of the suggested command, tokens; --match counts a pattern done
count-refusals refusals by rule across recorded sessions (today only the recurring ones magus session --brief exposes; a typed trail reader would see all) done
ls-uncommitted git status | awk | sort | uniq -c: groups changed files by owning project and role done
example-typed-results -o json | python: a worked example of typed field access on magus results, to copy done
diff-dirs ad hoc tree comparisons, skipping declared outputs done
count-symbols awk declaration counts: aggregates symbols from the graph done
ls-test-failures reading 400 lines of test log: one file:line: message per failing test done
ls-worktrees git worktree list rounds: every worktree with dirty, unpushed and job columns done
show-interrupted-session picking up a session that ended abruptly (Codex, Claude Code, Cursor): prints what was asked, how it ended, the checkout and its state, and the preserve-and-switch commands, never writing into the other checkout done
split-into-branches splitting a large change by hand: groups by owning project, attaches outputs to their generator, orders by project dependencies, prints a stack of branches, creates local stacked branches on --apply, never pushes. Grouping is by project, so the root project stays one large branch; a finer split needs package or symbol coupling done
Widening a job's write paths jq edits of job records: moves into the CLI (magus job edit --add-write-path/--remove-write-path, dry run until --apply), since the job store has one writer, the binary; no refusal ever names it done
on-actions (exists) proving something on Linux without a pull request: a prefix, magus buzz hack/on-actions.buzz -- magus <argv>, pushes a scratch branch, runs the command on a hosted runner, prints its output and exits with its code, then deletes the run and the branch; --detach, --result, --delete and --ls manage runs done
on-linux the same prefix run in a local Podman container (ADR 0003's option B', revived while --platform stays on hold): magus buzz hack/remote/on-linux.buzz -- magus <argv>, with --env, --image, --keep, --ls, --delete in progress
An explicit ephemeral copy. magus buzz --copy hack/x.buzz copies the script outside the tree and outside .magus/, stamps its first line with the source path, digest and revision, and prints the path. Running the copy prints one line saying it is a one-off (modified or not) and records its provenance in the activity trail. The tracked script is never edited in place unless the person asked (see the guard's ask above) editing a reference script in place planned
narrow-tests hand-built magus run ... -- -run lines (1,516): changed symbols to the tests that reference them directly, one line per project. Could instead be a tests column on affected --impact proposed
fan-out hand-written job records (391 forks): expands a typed plan's globs, drops declared outputs, proves write sets disjoint and clear of live leases, prints the fork records; --apply writes them through magus\job proposed
worker-changes shuttling patches between checkouts (724 git -C calls): reads a worker's checkout, grades its changes against its job, and on --apply copies source changes here, refusing a path that also changed here proposed
retro self-improvement: tallies recent guard verdicts per rule (denies that keep being retried, advice that rarely converts, the same edit to an ephemeral copy across sessions) and prints candidate rule, skill or memory changes with the command the person runs; never applies anything proposed
provenance git log -S and gh pr archaeology (324): the commits, pull requests, pinning tests and memory entries behind a symbol or text; --fetch names the host and time proposed
Removing checkouts beyond ls-worktrees: a removal command printed only for clean, landed, unleased checkouts proposed
Conflict context for magus vcs resolve (each remaining hunk's enclosing declaration and the commits touching it), rather than a script awk passes over conflict markers (211) proposed
Running some scripts on a schedule proposed
Long term: an agent reaching for an interpreter reaches for Buzz proposed

Fixes found while planning the scripts: the magus-buzz-write skill's worked example imports json instead of encoding/json and calls a missing fs\list; flags\parse needs names declared with their dashes and says nothing of it (planned).

8. Review fixes folded into the change

Beyond the items above, the review's findings were fixed or refuted with evidence, nits included (done): the desktop-toast rule for rows without a queue id, typed-nil profile observers, doubled trace phases, the magus clean report shape, the Buzz source rewrite that changed string literals, unknown MCP parameters refused, JSON integers decoded as integers in transforms, int-keyed Buzz maps surviving conversion to Go, the removed magus\guard.bash alias, the removed MCP descriptor Member field, typed graph results renamed QueryResult, ExplainResult and RefsResult with their keys listed, pending-cache races and framing in the guard, auth Class to Kind, and stale skill text about paging, memory refs, impact and insight.

Practices for agent-run work

  • Hand a dirty tree to workers with magus vcs checkpoint --preserve; never hand-roll a snapshot; never ask raw git diff <rev> whether a tree changed.
  • Bound runs with --target-timeout or --timeout, never a wrapper.
  • A worker brief never says "wait and retry": a worker runs its check once and reports a refusal; the orchestrator validates centrally. A worker is never parked waiting for a message: if its assignment may change it is stopped and a fresh one started, and later work is sequenced with job dependencies.
  • A live job's write paths are widened with magus\job\put, which keeps its state; a re-fork resets it.
  • Stale leases held by dead sessions are ended one at a time by a person, after their checkouts are rescued.

Alternatives

The ideas weighed and declined are the not built rows in the tables above.

Consequences

  • Breaking changes before 1.0, each with a changelog entry: tool names, the removed harness verbs, the removed magus\guard.bash, renamed typed results, and the tools/ path.
  • The guard denies more; each new deny names the command it would accept and records its measured precision.
  • Workers build on a preserved commit in their own worktrees; the orchestrator integrates their source changes and regenerates outputs once.
adrdecisionagentsguardmcpjobsvcsbuzzhackscope
Last updated (8b20a199)
Earlier changes on this page (2)

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.

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.

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.

Trace

OpenTelemetry's name for one whole magus invocation; every target it runs is a span beneath it. 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 server.

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 server.

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 server.

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 server.

One-off

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

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 server.

Knowledge graph

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

Insight

The reports magus derives over the graph and history (hotspots, affinity, ownership, trend, volatility, unreferenced). 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.

Invocation

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

Window

The terminal a command runs in. It keys fire-once notices for a caller no host gave a session, and is never recorded as a session.

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.

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.

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.