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

ADR 0010: jobs share a file by declaration claims

Decision accepted (in progress) on .

Each item below carries its own state: done is in the tree, planned is decided and queued, proposed is an idea not yet decided, and not built was weighed and declined.

Context

A job claims a declaration with <file>#<declaration> in its write paths. The guard denies an edit inside a declaration another live lease claims, and magus job wait fails a job whose footprint leaves its claims. A fork is refused with MGS3032 when two live jobs in different checkouts hold the same driver-covered file and nothing orders them.

Claims were barely used. Measured over the job store and 2,320 transcripts since 2026-09-15:

  • 25 of 664 writing jobs, 4 percent, carried a claim.
  • 59 forks were refused with MGS3032 on 46 files. The root claimed in 1. It added --depends-on in 10, dropped the file from the row in 28, and forked the whole file later in 18.
  • No real claimed-declaration denial and no claim violation at wait were recorded, so the checks never got in the way. Placement was right for 110 of 111 held-out hunks.
  • 88 percent of the shared-file pairs landed on Go, Buzz, Markdown or TypeScript, and all four have a diff driver. A missing driver was not the reason.

Two things held claims back. The refusal printed <declaration> where the root needed the name, so serializing was cheaper than working it out. And the files edited most often were tables: one const ( block held 74 percent of types/diagnostic.go, and one var held 56 percent of the guard's rule catalog. A claim cannot split a table, because the whole table is one declaration.

Decision

A refused fork prints the claim pair and writes nothing

Done. MGS3032 computes the claims from what the holder has touched so far and from the symbols the new job's goals name, and says whether the two are disjoint. When they are, the message is a one-line re-fork. It never writes a claim into either row, because the rows are the declaration of whoever forks them: the person or the orchestrator re-forks with the pair.

A claim names exactly one declaration

Done. A claim word that names more than one declaration of its file, such as ctx or String, is refused with MGS3031, and the message prints words that name one each. A word that names none is accepted, because the declaration may not be written yet. The fork says so, so a typo shows. A claim on a glob stays refused.

Claims are the default move

Done. The magus-multi-agent skill recommends a claim when two jobs touch different declarations of a driver-covered file, and keeps --depends-on for two jobs on the same declaration. Jobs documents it.

The guard grades a whole-file write by declaration

Done. A whole-file Write in a claimed file used to stand down to the path verdict and fail open. The guard now diffs the old and new content and grades each changed region against the live claims. Imports and the rest of the preamble stay exempt, because a missed import fails the build and the check says so.

Tables that a claim cannot split are split

Done for the two with the most conflicts. Each diagnostic code in types/diagnostic.go is its own top-level constant, and each rule's documentation in internal/guard/catalog.go is its own ruleDoc variable filled into a registry. The Go driver then splits both by entry.

table share of its file State
diagnostic codes, types/diagnostic.go 74 percent done
rule documentation, internal/guard/catalog.go 56 percent done
members of var Magus, std/magus.go 46 percent proposed
var boundaryTypes, cmd/magus-utils 56 percent proposed

Two passed jobs with disjoint claims settle at integration

Done. magus vcs resolve --against <branch> settles a file that git reports as conflicting when every one of these holds:

  • a project's merge_low_risk covers the file, and a diff driver places its declarations;
  • each side carries the work of a job that writes the file;
  • magus job wait recorded every such job as pass;
  • each job writes the file by declaration claims only, and no claim of one side names a declaration a claim of the other side names.

Each side's edits then apply where that side made them, import lines merge as a set, and anything else that overlaps stays a conflict. The opt-in is the existing merge_low_risk key, so no project gets it unasked. hack/dev/merge-job-branches.buzz lists each settle under the layer it merged and runs every merged job's checks once a file settled this way. The settle reads the merge base from the VCS, so on Mercurial, Sapling and jj a claimed file says so and stays conflicted.

item State
the claim pair in the MGS3032 refusal done
one declaration per claim word, MGS3031 done
claims as the default in the multi-agent skill done
a whole-file write graded by declaration done
the settle in vcs resolve --against behind merge_low_risk done
the fork notes printed by job fork and its JSON output planned
the same settle in the merge queue not built
claim drivers for JSON, YAML and TOML not built

Principles checked

Each design principle below is held up against this decision.

  • Agents propose, humans dispose: relied on for the claim pair. The fork prints it and the person or orchestrator that declares the rows decides. The integration settle bends it, since a file lands merged without a person reading the hunk. The bend is bounded: a project opts in with merge_low_risk, every job passed, the claims are disjoint, and the merged jobs' checks run once afterward.
  • Automation you can interrogate: relied on. The vcs resolve line names each claim and job behind a settle, and the reason a file did not settle.
  • No silent defaults: relied on. A claim word that names several declarations is refused, not guessed at.
  • Friction that carries meaning: relied on. MGS3032 used to say <declaration>; it now carries the one move that clears it.

Alternatives

  • Serialize with --depends-on. It is right for two jobs on one declaration and was the root's answer to 38 of 59 refusals, which cost wall-clock time. Reopen if the claim pair turns out to be wrong often enough that roots stop trusting it.
  • Have the fork write the claims. The rows are the root's declaration, and a claim a tool wrote reads as a boundary the root chose. Reopen if rows gain a field that marks a claim as computed.
  • Settle every both-added insert at integration. It is a general fallback for any adjacent edits, with no check that the work was claim-disjoint. It stays opt-in and limited to jobs that passed with disjoint claims, so a settle always has evidence. Reopen with a measurement of settles the claim check would have refused that a check later caught.
  • Claim drivers for JSON, YAML and TOML. These make up 1.7 percent of shared pairs, a nested key cannot be named by a line pattern, and placing one would need a parser where git has a regular expression. go.mod, lockfiles and magus.yaml keep one owner, and MGS3031 already refuses a claim on them. Reopen if their share of shared pairs passes several percent.
  • Settle in the merge queue as well. The queue already settles low-risk classes. A claim settle needs the jobs' records, which the queue does not read. Reopen if the integration step moves into the queue.

Consequences

  • A fork that used to end in a wait or a dropped file can end in a re-fork with a printed pair, and the pair is only as good as the holder's touched declarations so far.
  • A table split is churn once, and a new entry is its own declaration afterward. Generated outputs that read the table, such as the rule pages, regenerate in the same commit.
  • Two same-declaration edits still need one owner. The guard denies them and this decision does not loosen it.
  • A rename that moves a declaration changes the line a claim matches, so the renaming job claims both names.
  • A project that sets merge_low_risk accepts that a claim-disjoint settle lands without a person reading the merged hunk. The merged jobs' checks run once after it.

Open questions

  • Whether std/magus.go members and the boundary types are worth splitting, once the two done splits show their effect on conflict lines.
  • This page took 0010 on the branch that wrote it. Take the number again at merge time if another decision landed first, and renumber the file, its title, its order and the links to it.
adrdecisionjobsclaimsleasesmergevcsguard
Last updated (2df714e8)
Earlier changes on this page (1)

Full history ↗ · Blame source ↗

Glossary

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. Project in full.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). Op in full.

Ward

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

Buzz

The language magusfiles are written in (the .buzz engine). Buzz in full.

CI

An ordinary target you compose yourself with ctx.needs; magus does not hardcode its stages. It is the anchor magus affected ci keys off, and ci strips the rw charm. CI in full.

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. Queued in full.

Diagnostic code

A stable MGSxxxx identifier attached to a magus warning or error, so it can be referenced and looked up; some are guardrails (see wards), others hard errors. Diagnostic code in full.

Agent

A model working through an agent host such as Claude Code, Codex, Cursor or OpenCode. magus treats an agent as one more caller of its CLI, MCP tools and console, held to the same checks as a person. Agent in full.

Tool

An action a model can call through its host: a name, a description, a typed input and a result. magus serves its tools over MCP, each a read of the workspace model or a run of the CLI it already has. Tool in full.

Skill

A packaged set of instructions an agent host loads when a task matches its description: a directory holding a SKILL.md. magus ships its magus-* skills in the binary and writes them with magus agent install. Skill in full.

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. Dispose in full.

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 daemon, for its own maintenance. The store records; the agent guard is what reads those facts back when grading a write. Job in full.

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. Run in full.

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. Lease in full.

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.