muharmutation harness
Pages

CLI reference

muhar -spec <file> [-j <n>] [-shard <i>/<n>]

muhar uses Go's standard flag syntax: -j 2, -j=2 and --j 2 are equivalent. Flags must come before any positional argument; muhar takes none.

Flags

FlagDefaultMeaning
-spec <file>none, requiredPath to the spec JSON, relative to the current directory. Missing: muhar: -spec is required, exit 1.
-j <n>1Mutations in flight at once. 1 runs sequentially, in place. With at least two effective workers, each gets its own copy of the tree. 0 sizes to the machine, capped at 2. Negative: muhar: -j must be >= 0, exit 1.
-shard <i>/<n>none: run every mutationRun only this invocation's share: the mutations at spec index ≡ i (mod n), 0-based. Baseline and control still run in full.
-h, -helpPrint usage to stderr and exit 0.

-j: parallel workers

A sweep is one suite run per mutation, so the suite is almost the whole cost. With -j above 1 and at least two mutations, the mutations are dealt to a pool of workers. Each worker owns an isolated copy of the tree, so no two workers ever edit the same file; sharing one would make every verdict depend on timing.

ValueWorkers
1 (default)One, in place, in root. No copies.
n above 1At most n, capped at the number of mutations. Two or more get their own copies of root in the system temp directory; one runs in place.
0One per CPU, capped at 2. On a one-CPU machine, one.

When the spec (or shard) has fewer mutations than -j, muhar uses that many workers; with one, it runs in place like -j 1. This also means an interrupted run may leave the original file mutated (signals and cleanup), even when the requested -j is above 1.

Nothing about the verdicts changes with -j. The baseline and control still run first, in the first worker's copy when at least two workers are used, or in place otherwise, so the copy itself is proven sound before any verdict rests on it. Results are reported in spec order, whichever worker finished first. The report on stdout is byte-identical to a sequential run's.

With -j above 1 muhar prints one line to stderr naming the tree it copies:

muhar: 2 mutations in flight, one isolated copy of /home/dev/limits per worker

The number is the requested pool size; with fewer mutations than that, fewer workers start. The notice is still printed when the effective count is one and execution falls back to running in place.

Why -j 0 is capped at 2

Every worker holds a full copy of the tree and runs its own suite process, so peak memory grows with the number of mutations in flight, not with the size of the spec. Sizing the pool to the CPU count on a many-core CI runner put enough copies and builds in memory at once to exceed the runner's memory limit and kill the sweep. Capping -j 0 at 2 bounds peak memory by construction. An explicit -j <n> is used as given: a caller who asks for 8 is making their own memory trade.

Copies and Go modules

A copied go.mod with a relative replace directive pointing outside the tree would resolve to nothing in the temp directory, and every suite run in the copy would fail. muhar rewrites those directives to absolute paths in each copy and re-reads the file to prove the rewrite landed. This needs go on PATH. Details: How a run works.

Use relative file paths with -j above 1. An absolute file points at the original tree, not the worker's copy (spec reference).

-shard: splitting a spec across machines

-shard i/n runs the mutations whose 0-based index in the spec satisfies index mod n = i. For a fixed n, the n shards are pairwise disjoint and their union is the whole spec: every mutation runs exactly once across the set. Round-robin by index, rather than contiguous blocks, spreads neighbouring mutations — which tend to hit the same file and cost about the same — evenly across shards. muhar's test TestShardSelectPartitions holds the partition property.

Spec
012345678
Shard 0/3
basectrl036
Shard 1/3
basectrl147
Shard 2/3
basectrl258
Nine mutations split with -shard i/3. Each shard runs the baseline and the control in full, then only its own mutations.

Each shard is independently trustworthy: it proves the baseline and the control before reporting anything, so a broken harness fails every shard, not one of them. Its Totals: line covers its own mutations only. A shard's report is a partial sweep by design, and muhar says so on stderr:

muhar: shard 0/2 — running 2 of 3 mutations (indices ≡ 0 mod 2); the other shards own the rest

Running all n shards is the caller's job. CI integration shows a matrix that does it.

-shard and -j combine: the shard is selected first, then its mutations are dealt to -j workers.

Shard errors

All exit 1. A malformed shard is refused; it never degrades to running everything or nothing.

InputMessage
-shard 1muhar: -shard must be "i/n" (0-based shard i of n), got "1"
-shard x/3muhar: -shard "x/3": shard index "x" is not an integer
-shard 0/xmuhar: -shard "0/x": shard count "x" is not an integer
-shard 0/0muhar: -shard "0/0": shard count must be >= 1
-shard 2/2muhar: -shard "2/2": shard index must satisfy 0 <= i < n

Streams

StreamCarries
stdoutThe report only: the health line, one line per mutation, Totals:, and the NOTE: line when any mutation could not be planted. Or, for a broken run, HARNESS BROKEN — run discarded. and the reason.
stderrmuhar's own notices (-j and -shard lines, errors) and everything the suite prints, on both its stdout and stderr, in run order.

Capture stdout to gate on; send stderr to the log. report=$(muhar -spec mutations.json) does exactly that in a shell.

Environment

The suite inherits muhar's environment with one change: GOCACHE is set to a fresh directory created for this run and removed when it ends. Any GOCACHE you set is overridden for the suite. GOMODCACHE is not touched, so module downloads are shared and not repeated.

The temp directory for worker copies and the build cache is the system default, TMPDIR on Unix. Point TMPDIR at a disk with room for -j copies of the tree.

Why the per-run cache exists is explained in How a run works.

Exit status

StatusMeaning
0Harness healthy. Read the report for verdicts.
1Usage, spec or I/O error. No suite ran, or setup failed.
2HARNESS BROKEN: baseline red, control not caught, or a worker copy could not be made. Also: an unknown flag or malformed flag value, from Go's flag parser.
130Interrupted by SIGINT or SIGTERM.

The full table, and how to gate on it, is in Output and exit codes.