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

Jobs and leases

One agent hands work to several. magus neither runs that fan-out nor polices it. It answers what the working state is, records what the orchestrating agent says it intends, and shows a person the result: the same rule the rest of the agent surface obeys, where answering is the tool's job and deciding is the model's.

Two nouns, and the difference decides how everything below reads. A job is the unit of delegated work: one row, with a goal, a checkpoint, the paths it may write, and the one check it runs. A lease is the grant a holder takes on a job: the write and read paths that job declared, enforced in the checkout that took it. A job is the thing; a lease is permission over it.

How to split the work is not on this page. That is the magus-multi-agent skill: partition by write set rather than by affected project, prove the jobs cannot collide, narrow the scope at every level, match a model to each job. This page is the surface that skill writes to and reads from.

step surface
Record the working state magus vcs checkpoint, magus_vcs_checkpoint
Declare the work magus job fork, magus_job
Hand out the jobs the host's own spawn, recorded but never judged
Give the holder its terms magus describe job <job>
Take the lease magus job exec <job>
Watch it magus ls jobs, the console Jobs view
Return and verify magus job exit, magus job wait

Only one thing in that table enforces, and it is not the store. The job row is a declaration, the checkpoint is a reading, and the console renders both. The guard is what reads the declaration back, on every file write and every command: see what the guard enforces under a lease. It grades only a holder that took its lease, so the last step below, the actual diff against the checkpoint, is still what catches a write nobody could attribute.

A declared plan, released early, verified by diffAn orchestrator checkpoints the working state, partitions the work by write set using graph evidence, and forks one job per unit: goal, checkpoint, write paths. Holders release paths as soon as they are done with them, so the plan shrinks while it runs, and the Jobs view renders the rows, their overlaps and their staleness for whoever is watching. The orchestrator verifies each job by diffing since that job's checkpoint against what it claimed, then integrates and runs magus affected ci. magus records and answers; it enforces none of it.LEASEA declared plan, released early, verified by diffREPORTS DONERELEASEJOB ROWSWATCHESmagus vcs checkpointrevision + dirty digestPartition by write setrefs, explain, affectedDeclare the plangoal, checkpoint, pathsWorkers editone write set eachVerify each leasediff since its checkpointIntegratemagus affected ciPlan surface/console/plan/, overlaps, staleThe humanwatches and decidesmagus records and answersit enforces none of itTHE WHOLE GUARANTEEAGENTS AND PEOPLE

Record the working state

A checkpoint is the identity of the tree right now: the head revision, the branch carrying it, whether the tree is dirty, and a digest of the uncommitted patch.

magus vcs checkpoint
# <revision> <branch> clean
# <revision> <branch> dirty <digest>

magus vcs checkpoint -o name
# <revision>              when the tree is clean
# <revision>+<digest>     when it is not

magus vcs checkpoint -o json
# the whole record: revision, branch, dirty, patch_digest, vcs

It resolves and records; it never mints. No tag, no stash, no ref, no file, nothing changed anywhere, so taking one per job costs the tree nothing, and one nobody keeps costs nothing either.

The digest is the half a revision cannot supply. Every worker on a branch shares the revision, so a dirty tree's revision does not say WHICH dirty tree was handed out; comparing two digests does. That is why -o name renders a dirty checkpoint as <revision>+<digest> and a clean one as the bare revision: the clean form is a token anyone can check out, and the + marks the other as a revision plus uncommitted work that nobody can.

-o name is the single citable token, sized for the one cell a job row gives it. Feed the revision half to anything that takes a revision, such as magus graph diff --rev <revision>.

The command takes no arguments: it reports the whole workspace's working state. A path argument is refused rather than ignored, because magus vcs checkpoint <path> would read as a path-scoped digest, which is a different and much narrower fact. Agents connected over MCP call magus_vcs_checkpoint, which takes no parameters and returns the same record. Full flags: magus vcs.

Declare the work

magus job fork records one job: what its holder is handed, where it may write, and the one check it runs. It replaces any job with the same id.

magus job fork <job> \
  --criteria 'move the store' \
  --write-paths internal/job/store.go,internal/job/store_test.go \
  --check 'test internal/job'

magus job fork --stdin < job.json   # the same job as a record
magus job fork --schema             # what that record must satisfy

Write paths name files. An existing directory is refused (MGS3018) unless it is the root of a project the job owns whole, or does not exist yet because the job creates it.

A write path can also claim one declaration of a file, so two jobs can start on one file: run.go#executeStages, docs/scope.md#The knobs. The part after the # names the declaration line git's diff driver matches for that file (the diff= attribute in .gitattributes): the whole line, or any run of it that starts and ends on a word boundary, so executeStages names func (m *Magus) executeStages(ctx context.Context) error { and execute does not. fork refuses a claim nothing could grade (MGS3031): an empty declaration, a glob, or a file whose extension has no diff driver. Two claims on different declarations of one file do not overlap, and the lines above a file's first declaration (its package clause and imports) belong to no claim. A doc comment directly above a declaration is that declaration's, and in Go a top-level var, const or type (a single one or a ( block) is a declaration of its own.

A path that itself contains # is spelled so the # cannot start a claim: ./notes/a#b.md (with a leading ./ the whole entry is the path) or notes/a\#b.md (escaped, which also allows notes/a\#b.md#Intro). fork refuses an unescaped entry that names an existing path, and names both spellings.

A row carries id and optionally parent (the job this one was forked from), criteria (the prose half; the machine-checkable half is completion_gates), checkpoint (as magus vcs checkpoint -o name prints it), write_paths, deny_paths, read_paths, depends_on, model, check, state, and read_only. The store adds schema_version, the actor that recorded the row, created, updated, releases, unattributed (paths this job owns that somebody outside it wrote, noticed by the guard), write_proof, checkout_root (where magus job exec took the job), and end_reason (why magus ended it, see jobs magus ends itself), all output-only: a timestamp a client sent would be a fact about that client's clock. updated moves only on the job's own writes; the guard recording an unattributed write leaves it alone.

There is no --state on fork. It declares a NEW job, and one nobody has taken is declared; a holder moves its own job with magus job exec and magus job exit. Every job ends in pass, fail, or no_return, and no_return is not a failure: a job that failed came back and said so, while one that died, stalled, or was cancelled said nothing. Silence is not a pass.

Five properties are worth stating plainly.

One author per job, enforced by the store. A session holding a lease may record the base it landed on, SHRINK its own write_paths (which is how it releases a path), end its own job, and fork a child of itself inside its own paths. Every other write is refused, by name and with the remedy: widening a boundary, changing the plan's shape, and verifying a result are the forking session's. The rule lives in the store rather than in a guard pattern because the CLI, the magus_job MCP tool and magus\job all reach the same file and only one of them is a command a pattern can read. A session holding no lease, the orchestrator or a person at a terminal, writes anything.

Taking a lease is one-way. magus job exec for a session that already holds a different job is refused. Retaking is how a holder would be graded against another job's paths, and it costs nothing to a holder that runs its bootstrap twice: taking the lease it already holds is allowed and does nothing.

magus job exec --vacate gives the binding up. One-way means one-way until something releases it, and nothing did: the marker outlives the job it names, so a checkout bound to one that finished (exited, passed, failed, or was never returned) stayed stuck until a person deleted the file by hand. --vacate clears it instead, refusing only while the job is still declared or running - walking away from those two would leave the checkout's next write ungraded. A job already exited, one the store no longer carries, or no binding at all all vacate cleanly, and a checkout with nothing bound reports that rather than erroring. It releases only the SESSION's own binding, so a sibling session working in the same checkout keeps its lease.

A workspace-load file needs a worktree of its own. fork refuses a job whose write_paths cover a file magus must READ to load the workspace - any project's magusfile.buzz or magusfiles/*.buzz, its magus.yaml, and the workspace-local spell sources those magusfiles import - while another live job with write paths is already bound to the same checkout. The refusal names the file and the job that holds the checkout, and the fix it names is a worktree rather than a narrower boundary. Half-saved, one of those files stops the workspace loading for EVERY worker in the checkout at once: they lose magus run, magus ls and their own tests, over an edit none of them made and none of them can see. The same three doors are covered, since magus job fork, magus_job and magus\job.put share one declaration path.

Nothing else about a shared checkout is refused. Two sets of write paths that merely overlap are the orchestrator's call - it may have sequenced them deliberately - so the fork RECORDS what it could prove instead, in write_proof: alone when no other live job with write paths was bound to the checkout, else disjoint or overlapping. magus ls jobs prints it per row, so a plan read later says which forks were checked and which were not.

A declared boundary is enforced elsewhere. Beyond the row ownership above, this store gates nothing: it records the text an orchestrator put in a holder's prompt, where a human can read it. The agent guard is the one reader that turns it into a verdict. Every uncertainty there fails open with at most an advisory (no store, an unreadable one, a writer that took no lease) because this is a seatbelt for a harness that opted in and not a sandbox.

One set of jobs per repository. The rows live in one JSON file in the per-REPOSITORY state directory (<XDG state>/magus/jobs/<repo>/jobs.json), keyed the way memory and session history are keyed: every worktree and every clone of one repository reads one set. That is what lets an orchestrator declare a plan in its own checkout and a holder take its lease from another. A store an older magus left behind is carried forward the first time the new one opens it.

A read-only job carries an abbreviated row: read_only set, and empty write and deny paths that then read as deliberate rather than forgotten.

A timeout is optional. magus job fork <job> --timeout 2h (or timeout in the --stdin record or the magus_job call) has the store stamp a deadline on the row at fork time; nothing accepts a deadline directly. It is unset by default, and a job with acceptance criteria needs no bound. Past the deadline the guard denies every write graded under that lease and its write paths stop blocking other jobs, while the row stays live: a deadline alone never ends it, and magus ls jobs marks it overdue.

Limits exist only when the workspace sets them, in magus.yaml:

jobs:
  max_depth: 3 # levels below the root job; 0 = unlimited
  max_live: 12 # live jobs under one root; 0 = unlimited
  default_timeout: 2h # applies when fork names no --timeout
  stale_after: 30m # end untaken jobs untouched this long; default 2h, 0 = never

Every key but stale_after is unset by default. magus job fork refuses past max_depth or max_live, naming the key that set the limit.

Two answers the store gives back

Neither is enforcement. Each is something an orchestrator would otherwise derive by hand from a table it wrote itself, and each leaves the decision where it was.

Overlaps. A list reports every pair of live jobs whose write_paths intersect, each side's own declarations kept apart, because they are rarely the same string and which job claimed which is the part a reader acts on. Derived on the read and stored nowhere, so it cannot go out of date with the rows. A path is compared by containment (a job owning internal/job overlaps one owning internal/job/store.go) and a glob is judged by the directories it names, which over-reports rather than misses a pair. A job in a terminal state is in no pair, because a finished or released job is not competing for anything. Two jobs claiming different declarations of one file are still listed, with claims: disjoint: the pair is an integration order, not a wait.

Releases. Shrinking write_paths is how a job announces it has finished editing a path, and the store records each dropped path with the digest that path carried at that moment: the file's sha256, or one of three words when it cannot be one. absent when nothing is there, dir for a directory, which has no single content hash, and unreadable for a path that is there and could not be hashed. absent and unreadable are deliberately not the same answer: "the releaser deleted it" and "something is there nobody could read" send you to different places. Hand the digest to the job taking the path over; one that no longer matches at verification time means that job built on a tree the releaser never saw. Dropping a declaration claim (run.go#executeStages) releases that declaration, digested over the lines it spans now, so another job editing the rest of the file does not change it.

Jobs a holder went quiet on. A list marks a live job stale when it was not updated within jobs.stale_after, naming magus job exit <id> for each, and magus doctor's job-tree check reports the same. These are the jobs magus could not prove dead, so ending them stays yours.

Three doors write the store and they reach one set of rules: magus job is the person's, magus_job is the agent's, and magus\job is a magusfile's.

magus ls jobs                # every job, parents above the ones they forked
magus ls jobs -o json        # the same records, overlaps included
magus describe job <job>     # one job's terms

Jobs magus ends itself

Every read of the store (magus ls jobs, magus job fork, the guard's lease lookup, magus doctor, the magus_job tool) first ends each live job magus can prove nobody holds, as no_return with an end_reason, and prints one line per job on stderr:

ended fleet/w2: taken in /src/app/.worktrees/w2, which no longer exists

A job is ended when:

  1. An ancestor ended in pass, fail, or no_return. The tree dies with its root, children and grandchildren alike.
  2. Its checkout is gone. magus job exec records the checkout it took the job in; once that directory no longer exists, nobody can be working there. An exited job is spared: its holder already returned, and removing the worktree is the normal end of that.
  3. Nobody took it. It is still declared, no magus job exec ever took it, it was not updated within jobs.stale_after, and no live job hangs under it, so a root outlives the children still working. The window is read from the workspace's own magus.yaml: 2h by default, 0 for never.

A job whose liveness cannot be decided stays live: a checkout path on a mount that cannot be read is not a path that is gone. The server's own maintenance jobs are never ended this way. Ending a row moves its updated; nothing else magus does for a job does, so a job other agents keep writing near still ages.

Two habits follow. Remove a worker's worktree only once its job is done, and advance a root row you are still forking under, since an untaken root with no live children is as dead as any other untaken job.

Give the holder its terms

The context a delegated agent receives should come from the row, not from the orchestrating model's recollection of it. magus describe job <job> renders that context and nothing else: the goal and acceptance criteria verbatim, the write, deny and read paths, the projects those paths reach, the paths a sibling job is holding and who holds them, the affinity the graph knows about, the single check that job is allowed to run, and the jobs it depends on. A section with nothing in it is dropped, which is the mechanism rather than a nicety: ci cannot leak into a holder's terms because nothing renders it. Two renders of one row are byte-identical, which hand-typed prompts are not.

It REFUSES a job whose check is the gate, or a target that chains to one, and says so rather than printing terms nobody may act on.

Take the lease

magus job exec <job>
magus job exec <job> --session <the host's session id>

That takes the lease on the job in this checkout and records the base this tree landed on beside the checkpoint the job was handed, with the divergence between them as a fact rather than a refusal. With no job named, it prints the one this checkout holds.

A lease binds per SESSION, not per checkout. --session names the session taking it, as the agent host names the conversation to its own hooks, and the binding is that session's. So several workers sharing one checkout each hold their own lease, each has its own write paths graded, and each is denied outside them. Without it the binding is the whole checkout's, which is what every binding was before: a session that reports none, and a session nobody bound, both read the checkout-wide marker, so a worktree bound by hand still grades the sessions inside it. A session that HAS its own binding never reads the checkout-wide one, which is the boundary that matters - a worker bound to one job cannot silently act under another.

Pass the same id your host reports to the guard hook, or the two halves bind and grade under different names. Where the host reports no session, the checkout-wide fallback is the honest answer: one worker per checkout, which is the arrangement the worktree rule asks for anyway.

The base is the half a revision cannot supply on its own. The checkpoint is what the orchestrator HANDED the job; the base is what the checkout actually LANDED ON, and hosts that isolate workers in per-worker trees routinely branch them from an older revision than the tree that was partitioned. Recording it is a FACT and not a gate: a divergence records, because refusing would leave the orchestrator with no record that a holder went to the wrong base, which is the one case the record exists for.

Wiring the lease into a worker

A declared boundary grades nothing until the writing process says which job it is acting on, and it says that through its ENVIRONMENT. So the orchestrator that spawns a worker exports the id into that worker's environment, and the hook process the worker's host launches inherits it from there:

export BAGGAGE=magus.lease=<the job id>

That is the W3C Baggage channel, carried in the environment under the OpenTelemetry convention, and magus.lease is the one member a verdict reads. Export TRACEPARENT too when your host has one, and add magus.spawner=<your label> to the baggage: magus records the trace, the parent span and the label as CLAIMS for magus session ls to show, and keys no verdict on them. The magus-multi-agent skill requires this of every worker prompt it writes, in the same spelling.

Exporting it is the ORCHESTRATOR's job today. The shipped guard templates pass --agent-name and --session, which are attribution, and nothing that names a job, so a worker whose orchestrator never exported the variable is graded as an editor magus cannot attribute, which is an advisory rather than a deny and leaves every rule above it inert. A wrapper that builds its own argv can pass magus shell --lease <id> instead; an explicit flag outranks every other source.

What the guard enforces under a lease

Once a worker names a live job, the guard reads that row on every file write and every command. It denies:

the guard refuses the row field that decided it
any write, before the holder takes the lease here the recorded base
any write, once the job's timeout has passed deadline
any write, by a job that gathers evidence and writes nothing read_only
a write covered by this job's own deny list deny_paths
a write covered by another live job's write list write_paths (that job's)
an edit landing in a declaration another live job claims write_paths (that job's #)
a write outside every entry in this job's own write list write_paths
a command running the ci gate check
a READ of a path outside the projects this job may read read_paths, else write_paths

It advises on one more: your own path, written from a base that diverges from the checkpoint you were handed.

Whatever the row says, it also refuses magus agent harness apply, install and remove: the hook wiring is what grades the holder, so only an unbound caller rewires it. The commands refuse themselves under the checkout's binding or a BAGGAGE claim; the guard adds the subagent and session sources a command cannot see.

A denial for another job's path also says how long ago that job was last updated and names magus job exit <id>, which releases a job nobody holds any more. A job past its deadline owns nothing against other jobs.

The declaration row (claimed-declaration) reads the edit itself. When the host's payload carries the replacement (an old_string/new_string pair, or a list of them), the guard applies it to the file in memory and places the changed lines the way the job footprint does. An edit landing only in your own claims, in a declaration nobody claims, or above a file's first declaration passes. A payload carrying no edit, such as a whole-file write, is graded by path alone, and the rule reads nothing unless another live job claims a declaration of that file.

The read row is the write paths read the other way. write_paths stands in when read_paths is empty, because a holder leased to edit a project was pointed at that project, and the boundary it opens is those projects plus what they declare depends_on (see the guard's focus rule). Set read_paths when a holder must READ something it must not WRITE: widening write_paths to open a read is how two holders end up owning one file. Without a lease taken on the checkout the same rule only advises, which is the opt-in: a hard read boundary needs somebody to have declared one.

The gate row is the one that surprises people. The gate runs ONCE per branch, in the orchestrator's tree, after every job lands; a job's check is the narrow target it was assigned, so a holder that reaches for the whole pipeline is refused and handed its own check instead. A row whose check names ci owns the gate and is not refused.

Two absences are boundaries nobody declared rather than boundaries of size zero, and both scope nothing: an empty write_paths on a row that is not read_only, and an empty check.

None of that table is visible from the verdict a worker sees, which is exactly what makes a wrong lease dangerous: a checkout holding an unknown id, a terminal row, or a live row whose base was never recorded all render as an ordinary advisory, indistinguishable from a session these rules are actually enforcing on. magus doctor's bound-lease check is the other end of that gap. It reads the same row the guard would and says, in one line, whether this checkout's lease is live and therefore actually judged, or names why it is not.

What the sandbox enforces under a lease

The guard above is a seatbelt for harnesses that opt in: it explains a boundary and, for a worker, denies the tool call that crosses it. The sandbox is the boundary itself, and it reads the same job row rather than a second declaration, because a boundary written twice is a boundary that disagrees with itself.

When sandbox.mode is not off and the acting lease resolves to a live row with a parent and non-empty write_paths, every target run and every magus buzz script in that checkout gets a filesystem WRITE grant of exactly:

  • the write_paths, resolved as globs against the workspace root (a glob that matches nothing grants nothing). A LITERAL path that does not exist yet grants the nearest directory above it that does, because a job routinely owns a file it was forked to create and the guard already admits that write; only a glob keeps the existing-files-only rule, since a typo in a glob is the case that rule protects against;
  • the workspace cache directory and the sandbox's private temp dir (every child's TMPDIR), which a target needs to produce output at all.

Write grants outside the checkout, such as /dev/null and the tool caches, are kept. Reads are untouched: the row declares a write boundary, and a holder has to read the tree it is changing. A refusal is recorded on the trail as a sandbox_denial carrying the job id, so a reader can say whose boundary was hit rather than only that something was blocked.

Nothing narrows for a ROOT job (a row with no parent is the orchestrator, and it owns the checkout), for a job id that names no live row, for a writable row with no write paths, or when the sandbox is off. A read_only row narrows the checkout to the cache directory and the private temp dir alone, which is the sandbox's reading of the guard refusing every write under such a job.

Both tiers resolve the acting lease the same way, in this order: an explicit --lease, the job magus recorded the calling subagent was spawned for, the job magus job exec took in the checkout, then the BAGGAGE a worker inherited. Every tier but the last is a record; BAGGAGE is the worker's claim about itself, so it answers only when no record does, and a checkout whose binding disagrees with it grades under the binding. The verdict names the tier that answered as lease_from: flag, agent, marker, env, or contested for a binding that overruled a different claim.

A host runs its hooks from wherever it likes, so the guard locates that checkout from the cwd its hook envelope reports and falls back to the hook process's own directory only when the envelope carries none.

A deny path inside a write path is refused, and it costs the directory holding it as well: both this policy and landlock are allowlists with no deny rule, so the enclosing grant is replaced by grants on its children, and a new file created beside the denied entry is refused with it.

Return and verify

A holder returns its job with the result of the work:

magus job exit <job> --stdin < result.json
magus job exit <job>                         # abandoned, recorded no_return
magus job exit --schema                      # what that result must satisfy

The result carries changed_paths, the validation the holder ran as {command, output_ref}, the descendants it forked, and its unresolved_risks, under a required schema_version. The run behind the output ref is resolved in the holder's OWN checkout and its record is filed alongside, because the output store belongs to that checkout and nobody else can reopen it. That is what makes the evidence portable when the parent waits from another worktree.

There is no passed field. A holder's verdict on its own run is the assertion the ref exists to replace. Do the work first, then file the result: it is a record of what happened, not a form to fill in while you are still deciding what to do.

magus job wait <job>

wait verifies the filed result against the job it was handed, and it verifies EVIDENCE rather than what the result claims: every changed path inside the write paths and outside the denied ones, a change set that is not empty, the descendants the store carries, and a run recording a PASSING execution of this job's own check. A job that verifies is recorded pass.

For a job that writes, the claimed changed_paths are held to the diff magus observes since the job's checkpoint: every claimed path must appear in it, at least one change in it must fall inside write_paths, and a diff magus cannot read fails the job rather than passing it. A read-only job has no diff to check. wait also refuses pass while any descendant (by parent chain) is still live, naming each, and grades a job against its ancestors' symbol gates as well as its own.

Two failing statuses, because they send a caller somewhere different. Exit 1 is a status: the result was read and rejected, and every rule that failed is named. Exit 2 is magus unable to answer: nothing was filed and nothing was piped in, the result would not decode, or the job would not write.

It checks what is mechanical. Whether the work is GOOD stays the reading of whoever forked it. How much of the job's acceptance criteria magus checks for you is the next section's subject.

Completion gates

A job's acceptance criteria are prose a person grades. A completion gate is the part magus grades itself, and magus job wait will not record pass until every one verifies. Declare them when the job is forked:

magus job fork api/migrate \
  --criteria "move the accounts table to the new schema" \
  --write-paths 'db/**,api/**' \
  --gate-check green='go-test api' \
  --gate-paths migration='db/migrations/**' \
  --gate-symbol-unreferenced unused='LegacyAccountStore'

A gate names a kind (what it examines) and an expect (what must be true of it). Two fields rather than a kind per pair, so absent means the same thing of a file and of a symbol:

kind expect proven by
check passed a recorded, PASSING run of that target, captured after the job was declared
paths changed, present, absent the diff since the checkpoint, or the tree as it is now
symbol changed, present, absent, unreferenced the knowledge graph, at the granularity below a file

Each kind has a natural expectation, so the common gate declares only its subject: a check is asked whether it passed, files and symbols whether this job changed them. The flags spell the others out (--gate-paths-present, --gate-symbol-absent, --gate-symbol-unreferenced), and magus job fork -h lists them.

Every kind reads something magus already holds, which is what separates a gate from an attestation. There is deliberately no escape hatch for "this command exited 0": magus did not record that run and cannot attribute it, so such a gate would be the easiest of all to satisfy falsely. Declare a target and use check.

symbol + unreferenced is the one worth knowing about: the symbol may still exist, and nothing may name it. That is the remainder a partitioned rename leaks. Split the work per project and the callers that live in no project belong to no job, so every job passes and the rename is unfinished.

The single --check is one of these gates, under the id check.

A gate is graded against what magus observes and never against the result's own changed_paths. The holder's account of its work is the thing the gate replaces, so a result claiming a file the tree does not carry is rejected, and the refusal names each subject that failed rather than only that the gate failed:

rejected api/migrate, and its state is unchanged
  completion gate "migration": nothing matching "db/migrations/**" changed since
  4a1c0cc8, so this gate is unmet

An observation magus could not make FAILS the gate: an unreadable diff, a symbol graph that will not open. That is the opposite of how the guard treats an unanswerable question, and deliberately: a guard that cannot ask must not refuse a person's own command, while a gate that cannot verify must not certify, or the cheapest way past it is to break the observation.

Asking where a job stands

magus job wait verifies and RECORDS. To ask the same question without advancing anything:

magus describe job api/migrate --gates

It grades every gate against the evidence magus holds right now, writes nothing, and exits 1 while any gate is unmet. Because it runs the same grading wait does, the two cannot disagree; because it records nothing, an orchestrator may ask while the holder is still working, and asking never blocks that holder. The gates that read the tree and the graph answer even for a job that has filed no result at all.

Sequence gates with depends_on between them when one has to be cleared before another is approached. A failed prerequisite propagates, and the declaration refuses a cycle. Gates do not nest: one wanting children is a JOB wanting splitting, which the multi-agent skill's "every level narrows" rule already covers, and keeping gates flat leaves the job tree as the only hierarchy with an owner.

A gate may not name the release gate. --gate-check ci is refused by the same rule that refuses --check ci, for the same reason: the gate runs once, in the forking session's tree, after every job lands.

Declaring gates from Buzz

A magusfile or a magus buzz script declares them through the same store, as records rather than flags:

import "magus";
import "std";

fun declare() > void {
  try {
    magus\job.put("api/migrate", {
      "criteria": "move the accounts table to the new schema",
      "write_paths": ["db", "api"],
      "completion_gates": [
        {"id": "green", "check": {"target": "go-test", "project": "api"}},
        {"id": "migration", "kind": "paths", "paths": ["db/migrations/**"]},
        {"id": "old-gone", "kind": "symbol", "expect": "unreferenced", "symbols": ["LegacyAccountStore"]},
      ],
    });
  } catch (e) {
    std\print("could not declare the job: {e}");
  }
}

kind defaults to check and expect to the kind's natural expectation, so the first gate above needs neither. The records are validated on the way in: a gate carrying both a check and paths is refused, and so is one naming paths nothing could ever match.

Watch it: the console Jobs view

The console draws one Jobs view, because a job is ONE KIND OF THING however it was created. The server holds its own maintenance jobs (graph sync, trail rotation, the review check) and a session holds the ones an orchestrator handed out; both list together, and a HOLDER column reading server or session is what separates them. magus ls jobs prints the same two sets, with the same column, as a tree with parents above the jobs they forked.

A job in a reported overlap is marked on both rows. A live row carries how long since it was last touched, and the released paths and their digests read in the detail beside the row. Beyond the jobs magus ends itself, every state was written by an agent or a person, which is why a row that has merely gone quiet is a job YOU decide is possibly dead.

The service behind it is magus.job.v1alpha1.JobService, the server's one mutating console surface, mounted behind the same loopback bind and bearer token as everything else. Start it with magus server start; see the server. magus server status prints the mcp and console URLs, and says so explicitly when the server predates the tree, because every call through an older server is answered by the older build.

The spawn is recorded, never judged

Wire your host's sub-agent tool to the same magus session hook call as the rest of the guard. A payload carrying a prompt rather than a command or a file path is a delegation: magus records it as an agent_spawn event on the local Activity Trail and returns pass without evaluating a single rule.

That exemption is the point, not an oversight. There is no command and no path to judge, only a context transfer to note, and a prompt that merely MENTIONS a denied command would otherwise block the job that describes it. The handed context is routinely kilobytes, so it lands as a content-addressed blob and only its reference rides the event.

magus does not switch on your host's tool name anywhere: a payload carrying a prompt IS a spawn, and the callee label the host supplies becomes the event's action so a page of them groups by what was spawned.

Joining an event to a job is cooperative. Nothing in a host event names a magus job and magus will not infer one from prose, so the job is stamped only when the handed context's FIRST non-blank line reads:

lease: <id>

Use the same id you forked. An orchestrator that wants the join writes the marker; one that does not gets an event with no job, which is a missing join rather than a wrong one. A marker line quoted deeper in a prompt stamps nothing, on purpose.

Verify against the diff

A holder's report of what it changed is a claim. The checkpoint is what turns it into something you can check.

  1. Diff the actual tree against the job's checkpoint, and compare THAT against the row's write and deny paths. magus graph diff --rev <revision> gives the domain-level answer; git diff <revision> | magus diff - annotates each changed file with its reach, public-surface exposure, and referents. See magus diff: it refuses a git ref given positionally, so the pipe is the sanctioned spelling.
  2. Check the dirty half of the token. A checkpoint whose digest differs from the tree you are diffing means the holder saw a different uncommitted tree, and the comparison you are about to make is not the one you think.
  3. Take acceptance evidence as an output reference the orchestrator reopens, never a holder's prose. A holder that ran a filtered subset and one that quietly restated its criteria both report success, and a transcript cannot tell you which happened.
  4. Regenerate declared outputs once, centrally, after the source work converges, then run the release gate yourself.

magus job wait prints the job's footprint: the declaration each changed line of its diff since the checkpoint lands in. For a file the job claims only by declaration, every declaration the diff touched that none of those claims names is a violation, and so is a footprint magus could not read for a job that claims declarations at all.

The same object serves review time. If you recorded a checkpoint when you stopped reading, the delta since then is the incremental-review flow on the agents hub: handing work out and picking review back up read the same identity.

What magus never does here

  • Block a writer it cannot attribute. Only a process that named a live job is graded against a declared boundary; anyone else editing this workspace is advised at most, because a human in their own checkout names no job either.
  • Transition a row on a guess. magus ends a job only on a fact it can prove (jobs magus ends itself), always as no_return, and never derives a pass or a completion; every other state was written by the agent or the person that put it there.
  • Judge a delegation prompt, or let one change a guard verdict.
  • Mint anything for a checkpoint: no tag, no stash, no ref, no file.
  • Inject any of this into an agent's context. Every surface here is pull-based, and the knowledge graph the partition is argued from is read the same way.
agentsjobsleasescheckpointmagus vcs checkpointmagus jobmagus_jobconsoleactivity
Last updated (a9ff8609)
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.

Ward

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

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.

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.

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.

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.

Knowledge graph

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

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.

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.

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.

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.