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

Jobs

A job is a written agreement about a piece of work: which files it changes, which revision it starts from, and what has to be true before anyone calls it done. You declare one with magus job fork, take it in the checkout you will edit with magus job exec, hand back what happened with magus job exit, and have magus job wait check the result against the agreement. magus ls jobs is the board. Every step is a command you type; magus records and answers, and moves nothing on its own.

This page follows three people at a terminal: one developer splitting a refactor, two teammates who both need the same file, and a CI job checking merged work. The second page splits work that is not a refactor: a dependency upgrade, a translation, a release checklist.

What the store gives you

  • Who holds what. Each job names the files it may write. magus describe job lists the files other live jobs hold as off limits, and magus ls jobs -o json carries the checkout each job was taken in (checkout_root) and the login that declared it (registered_by). A job one checkout holds is not taken from another: job exec there is refused until the holder gives it up.
  • Since when. The same record carries when the job was declared (created), taken (registered) and last moved (updated), in Unix seconds, and every file a job gave up, with the time and the sha256 the file had at that moment.
  • What done means. A job names one check, a target to run, and may declare goals. magus job wait grades evidence rather than anybody's word: a passing run recorded no earlier than the second the job was declared, and the diff since the revision the job started from. When it says no, it exits 1 and names every rule that failed.

The store is kept per repository in your state directory ($XDG_STATE_HOME/magus/jobs/), so every worktree and clone on one machine reads the same set. It does not travel between machines, which is why the CI example declares its own.

Every transcript below is what the command printed, compared byte for byte by the scripts in cmd/magus/testdata/script/job_people_*.txtar. Paths read ~/src and output refs read out<hex>, the two parts that change from run to run.

One person, two worktrees

Ana's shop prices things in whole units, and she wants cents. Two halves can move independently: total.sh in the pricing directory and tax.sh in checkout. Both are checked by one target:

import "magus";
import "proc";

// test runs the shell checks in test/run.sh.
export fun test(ctx: magus\Context, args: [str]) > void !> any {
    ctx.readsFiles("pricing/*.sh", "checkout/*.sh", "test/*.sh");
    proc\exec("sh", ["test/run.sh"]);
}

She declares a job per half, from her main checkout:

$ magus job fork refactor/pricing --criteria 'totals are computed in cents' --write-paths pricing/total.sh --check 'test .'
forked refactor/pricing, declared, with 1 write path(s). Its holder reads the terms with `magus describe job refactor/pricing` and takes it with `magus job exec refactor/pricing`
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder
magus job fork refactor/tax --criteria 'tax rounds half up' --write-paths checkout/tax.sh --check 'test .'
git worktree add ../shop-pricing
git worktree add ../shop-tax

The fork recorded the revision her checkout was on as each job's checkpoint. The board:

$ magus ls jobs
JOB               HOLDER  STATE     MODEL  PATHS  PROOF  CHECK
refactor/pricing  -       declared  -      1      alone  magus run test .
refactor/tax      -       declared  -      1      alone  magus run test .

in flight: never fetched; `magus queue ls --provider <provider> --base <branch>` reads the open changes
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

HOLDER names the worktree a job was taken in, - until somebody takes it; server marks magus's own housekeeping. declared means nobody has taken it yet, and job exec moves it to running. PROOF is what the fork could prove about the write paths in that checkout: alone, disjoint or overlapping. The in flight line is about open pull requests, which this repository never fetched, and the console line says where to watch a job in a browser once magus server start is running.

In ../shop-pricing she takes the first job:

$ magus job exec refactor/pricing
recorded job refactor/pricing's base as a1d513296908525d76f16e0f80c672c5ec69acbb, which is the checkpoint it was handed. Nothing to reconcile; carry on.
took refactor/pricing in ~/src/shop-pricing; read the terms with `magus describe job refactor/pricing`

Had the worktree been on a different revision, exec would have said so and recorded it anyway: a divergence is a fact for the record, not a refusal. The terms:

$ magus describe job refactor/pricing
job: refactor/pricing

criteria
totals are computed in cents

write paths
  pricing/total.sh

projects
  .

deny paths the workspace declares
  checkout/tax.sh: owned by live lease refactor/tax: coordinate, never work around
  magus.yaml: workspace configuration: changing it changes the plan every lease is running

the only check you run
magus run test .
refactor/pricing: 0 of 1 goal(s) met
  [unmet] check
      carries no output_ref, so there is no run to reopen
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

The other half's file is listed as somebody else's, and the check is unmet because no run has been filed. She edits total.sh and runs the check:

magus run test .
# [pass] shop-pricing (ran, <duration>)
# magus run test .
# ref  out<hex>

The ref line names that run's captured output (see output references). Her result says what she changed and which run proves it:

{"schema_version": 2,
 "changed_paths": ["pricing/total.sh"],
 "validation": {"command": "magus run test .", "output_ref": "out<hex>"},
 "unresolved_risks": []}
$ magus job exit refactor/pricing --stdin < result.json
exited refactor/pricing, recorded exited, with its result filed. Whoever forked it verifies with `magus job wait refactor/pricing`

The tax half turns out not to be needed yet. She takes it in ../shop-tax and gives it back with no result:

$ magus job exit refactor/tax
abandoned refactor/tax, recorded no_return

no_return is not a failure. A failed job came back and said so; an abandoned one said nothing, and the board keeps the two apart.

Back in the main checkout, she checks the result against the job:

$ magus job wait refactor/pricing
verified refactor/pricing, recorded pass
its holder reports it ran magus run test .
goal check: verified (out<hex>)
footprint: where its diff since the checkpoint landed
  pricing/total.sh:1-1
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

wait reopened the run the ref names in the worktree that took the job, confirmed it passed and was recorded after the job was declared, and read the diff since the checkpoint: every changed line is inside the job's write paths. A run older than the job, a failing run, or a change outside total.sh would have been named here, and wait would have exited 1. Whether the change is good code is still Ana's call; wait checks what is mechanical.

$ magus ls jobs
JOB               HOLDER        STATE      MODEL  PATHS  PROOF  CHECK
refactor/pricing  shop-pricing  pass       -      1      alone  magus run test .
refactor/tax      shop-tax      no_return  -      1      alone  magus run test .

in flight: never fetched; `magus queue ls --provider <provider> --base <branch>` reads the open changes
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

Two people, one file

Ana and Ben share a workstation, each in a worktree of the shop: ../ana and ../ben. Ana takes a job on total.sh and format.sh, both in the pricing directory:

magus job fork cents --criteria 'prices are integers of cents' --write-paths pricing/total.sh,pricing/format.sh --check 'test .'
magus job exec cents

Ben's change needs format.sh too, and he declares it anyway:

$ magus job fork currency --criteria 'prices carry a currency code' --write-paths pricing/format.sh,checkout/tax.sh --check 'test .'
forked currency, declared, with 2 write path(s). Its holder reads the terms with `magus describe job currency` and takes it with `magus job exec currency`
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

The store accepted the claim and recorded the overlap. For a file with a diff driver, such as Go source or Markdown with diff=markdown set, the same fork is refused (MGS3032); the translation example shows that refusal. A shell script has none, so the overlap is recorded rather than refused.

Before he edits, Ben asks whether the file is free:

$ magus shell --path pricing/format.sh -o name
advise

An advisory, exit status 0. Without -o name the text says the path is inside cents, quotes its criteria, and says this is not a block: a person who names no job is never stopped from writing their own repository (the leased-path rule). The question is also noted on Ana's job, as an unattributed entry in magus ls jobs -o json, so she can see somebody outside her job was about to write there.

Ben takes his job, and the board shows the shared ground:

$ magus ls jobs
JOB       HOLDER  STATE    MODEL  PATHS  PROOF  CHECK
cents     ana     running  -      2      alone  magus run test .
currency  ben     running  -      2      alone  magus run test .

overlaps
  cents and currency claim common ground
    cents: pricing/format.sh
    currency: pricing/format.sh
    footprints: disjoint

in flight: never fetched; `magus queue ls --provider <provider> --base <branch>` reads the open changes
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

footprints: disjoint means neither of them has changed a line the other has. They talk, and Ana hands the file over by applying her job's record without it. The record is the whole spec, so it repeats what stays:

$ magus job apply -f - <<'EOF'
{"schema_version": 11, "id": "cents",
 "criteria": "prices are integers of cents",
 "write_paths": ["pricing/total.sh"],
 "check": {"target": "test", "project": "."}}
EOF
updated cents, still running:
  write_paths -pricing/format.sh
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

magus job fork --schema prints every field a record takes, and --dry-run prints the change without writing it. The dropped path is recorded on cents as a release carrying the file's sha256 at that moment, so Ben can tell later whether he built on the file Ana left. The overlap is gone:

$ magus ls jobs
JOB       HOLDER  STATE    MODEL  PATHS  PROOF  CHECK
cents     ana     running  -      1      alone  magus run test .
currency  ben     running  -      2      alone  magus run test .

in flight: never fetched; `magus queue ls --provider <provider> --base <branch>` reads the open changes
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

and Ben's terms now list only total.sh as Ana's:

$ magus describe job currency
job: currency

criteria
prices carry a currency code

write paths
  pricing/format.sh
  checkout/tax.sh

projects
  .

deny paths the workspace declares
  pricing/total.sh: owned by live lease cents: coordinate, never work around
  magus.yaml: workspace configuration: changing it changes the plan every lease is running

the only check you run
magus run test .
currency: 0 of 1 goal(s) met
  [unmet] check
      carries no output_ref, so there is no run to reopen
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

CI checks the merged work

Each job passed in the worktree it was written in. Whether the work still passes once the branches are merged is a different question, and magus job wait --integration answers it in the tree it runs in. A CI runner starts with an empty store, so the plan is committed beside the code:

{"schema_version": 11, "id": "release/pricing", "criteria": "totals are computed in cents", "write_paths": ["pricing/total.sh"], "check": {"target": "test", "project": "."}}
{"schema_version": 11, "id": "release/tax", "criteria": "tax rounds half up", "write_paths": ["checkout/tax.sh"], "check": {"target": "test", "project": "."}}

with the evidence the CI step fills in once it knows the run's ref:

{"schema_version": 2,
 "changed_paths": [],
 "validation": {"command": "magus run test .", "output_ref": "out<hex>"},
 "unresolved_risks": []}

The step, on the merge commit:

magus job apply -f ci/jobs.jsonl
ref=$(magus run test . -o jsonl | sed -n 's/.*"ref":"\(out[0-9a-f]*\)".*/\1/p')
sed "s/out<hex>/$ref/" ci/evidence.tmpl.json > evidence.json
for job in release/pricing release/tax; do
  magus job wait "$job" --integration --stdin < evidence.json || exit 1
done
$ magus job apply -f ci/jobs.jsonl
created release/pricing, declared, with 1 write path(s)
created release/tax, declared, with 1 write path(s)
$ magus job wait release/pricing --integration --stdin < evidence.json
integration: release/pricing passes in ~/src/shop; its state is unchanged
release/pricing: 1 of 1 goal(s) met
  [met] check

The grade is recorded on the job as its integration, beside its state, which does not move:

$ magus ls jobs
JOB              HOLDER  STATE     MODEL  PATHS  PROOF  CHECK
release/pricing  -       declared  -      1      alone  magus run test .
release/tax      -       declared  -      1      alone  magus run test .

in flight: never fetched; `magus queue ls --provider <provider> --base <branch>` reads the open changes
console: nothing is serving it; `magus server start` to watch this job without interrupting its holder

When the merge breaks the totals, the same step fails the build:

$ magus job wait release/pricing --integration --stdin < evidence.json
integration: release/pricing fails in ~/src/shop; its state is unchanged
release/pricing: 0 of 1 goal(s) met
  [unmet] check
      the run behind output ref "out<hex>" failed, so its check did not pass

Exit 1 is a verdict: the evidence was read and the check did not pass. Exit 2 means magus could not answer at all, such as evidence that would not decode. The runner's store goes away with the runner, so keep the record with -o json, which prints the grade as JSON, if you want it after the build. See CI for running magus in a pipeline.

Where to go next

  • Splitting other work: a dependency upgrade, a translation and a release checklist, each split into jobs.
  • magus job <verb> -h ends with an example for that verb, and magus job has every flag.
jobsmagus jobworktreesteamsciwrite pathsmagus job forkmagus job execmagus job exitmagus job waitmagus job apply
Last updated (895d94e4)
Earlier changes on this page (1)

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.

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.

Sandbox

The restricted filesystem and environment a target runs in, so builds stay reproducible and side-effect-free. See sandbox.

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.

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.