Musts
musts is the validation loop. The task is not done until musts validate
exits clean.
When to use this skill
- After making code changes in a repo with a
MUSTS.ymlat the root. - Before declaring work complete, opening a PR, or asking for review.
- Whenever the user asks "is it ready?" / "is this validated?".
If musts validate prints "No MUSTS.yml files found.", this skill does not
apply.
The loop
musts validate
# → prints every pending task, exit code 1 if pending, 0 if clean
- Run
musts validate. It emits every dirty task (no batching), so it is idempotent — re-running it never invalidates the ids it just issued. - Close each task:
- Runnable checks (
do:is a plain command —cargo/*,bazel/build,bazel/test): justmusts run <task-id>. musts executes the command itself, checks the real exit code, and records the evidence for you. You never re-run the build to satisfy the loop. - Judgment checks (
agent,mav— verify facts, drive a UI): do the work yourself, thenmusts evidence <task-id>(see below).
- Runnable checks (
- Re-run
musts validate. - Repeat until exit code
0.
A task shown as (unchanged since last validate …) is one you already saw —
its full body isn't reprinted; act on it the same way.
Reading a task
Each task in the report tells you:
- Task id — passed to
musts runormusts evidence. - do — exactly what to run.
- evidence — what to attach when recording (judgment checks only).
- submit — the
musts evidencecommand shape for that task.
Do not invent extra steps. Do not skip required evidence.
musts run — the fast path for deterministic checks
musts run cargo-test-root
- Executes the task's command from the workspace root, captures the log outside the workspace, and — on exit 0 — records evidence automatically.
- On a non-zero exit it prints the output and records nothing: fix the failure and re-run.
- Refuses judgment tasks (
agent,mav) and any check provided by an installed extension — usemusts evidencefor those.
Dispatching to subagents
When the report has multiple independent judgment tasks:
- Dispatch each to its own subagent in a single batch.
- Each subagent runs one task's work, captures any asset, and returns the asset path back to you.
- After the batch finishes, you record evidence for every task with
musts evidence.
If two tasks share a single resource the report cannot know about (a
simulator, a database, a port), run them sequentially. Deterministic checks
are simpler: just musts run them (in parallel is fine unless they contend
on a build lock).
Recording evidence (judgment checks)
musts evidence <task-id> \
--text "<one-line summary of the result>" \
--asset <path-to-log-or-screenshot>
--assetrepeats. Attach every file the report'sevidence:line asks for.- The asset is validated in place and not copied anywhere — musts no
longer archives evidence under
.musts/evidence/. The ledger (.musts/ledger.lock.yaml) is the record. - Prefer asset files outside the workspace (e.g.
$TMPDIR): a log written inside the workspace mutates the scope hash and can invalidate the task you're closing. - Submit one
musts evidencecall per task. If it fails, fix the missing evidence and call it again.
After recording
- Run
musts validateagain. - If the report is empty, you are done.
- If a task comes back "stale", files it covers changed after it was
issued — re-run
musts validateand act on the fresh id.
Don'ts
- Don't skip the loop because "the change is trivial". Every task in the report is dirty per the ledger; run it, submit evidence, or fix the underlying issue.
- Don't hand-edit
.musts/ledger.lock.yaml.musts run/musts evidenceare the only writers.
Writing or editing a MUSTS.yml
Run musts lint afterwards — it enforces everything below and reports each
glob against the files actually in the tree.
If a finding is deliberate, silence that one rule for the file with a comment; do not delete the check or widen the glob to make lint quiet:
# These globs are built to be disjoint knowing `*` crosses `/`.
# musts-lint: allow glob-crosses-directories
Scope is the whole manifest, and it is per-rule — other rules keep
reporting. Several rules: # musts-lint: allow rule-a, rule-b.
One question decides where a check goes: does satisfying it need judgment, or does it need a command run?
| Don't write this | Write this instead |
|---|---|
uses: agent with Run `bazelisk test //T:T` and confirm it exited 0 |
uses: bazel/test with targets: ["//T:T"] — then musts run executes it and records the real exit code, and no agent reads the log |
A fact saying "If changes touch src/Foo/** …" |
paths: ["src/Foo/**"] — the check then does not fire at all on unrelated changes |
| A fact saying "if unrelated, this is trivially satisfied" | Delete it; that sentence is a missing paths: filter |
"build_number was not edited manually" |
Nothing. Automation edits that file on every release, so the check reopens forever and can never fail. Enforce it in CI against the diff |
One broad paths: with exclude_paths: carved out |
Two disjoint sets of positive globs |
No paths: means the check fires on every change under the manifest's
folder. Fine for a runnable capability; expensive under uses: agent.
Glob semantics — these are not .gitignore rules
- Relative to the manifest's own folder, not the workspace root. A
MUSTS.ymlinApp/macOSUI/MainWindow/writesMacOSMainView.swiftfor the file beside it — neverApp/macOSUI/MainWindow/MacOSMainView.swift, which repeats the folder and matches nothing. - Case-insensitive.
*View.swiftalso matchesRequestReview.swift. *and**both cross/.UI/*View.swiftalso matchesUI/Deep/Nested/FooView.swift. There is no single-level wildcard.- Leading
!is rejected at parse time. Useexclude_paths:.
If paths: match nothing, validate says so under Ignored checks —
a check that cannot fire is never hidden. Treat that line as a bug in the
manifest, not as "nothing to do".
Auditing an existing manifest
musts stats lists each check's reopens and how often it was ever red. A
check with many reopens and zero reds costs validation work and has never
objected to anything — rewrite it or delete it.
Adding a .mustsignore
.mustsignore is .gitignore for musts: files it matches are excluded
from the walker that builds each check's scope hash, so editing them
never re-invalidates the ledger.
Reach for it when a file is making the loop noisier than it should:
- canonical fixtures the user wants committed but doesn't want gating validation;
- local artefacts that aren't gitignored (logs in a vendored sub-repo, IDE state outside the standard ignore list);
- generated files that change far more often than the underlying source.
Don't reach for it to silence a failing check — that's the extension's call, not yours.
# at the workspace root (or any subdirectory — applies to that subtree,
# same as nested .gitignore):
cat > .mustsignore <<'EOF'
*.log
scratch/
!scratch/keep-this.log # negation works on file patterns
EOF
git add .mustsignore
Commit .mustsignore. If it isn't committed, two clones produce
different scope hashes for the same code and the lock stops
reproducing. After adding patterns, run musts validate once: any
check whose scope contents change re-opens, so submit evidence for
those tasks, then commit the refreshed .musts/ledger.lock.yaml.
Gotcha: standard gitignore rule. If you ignore a directory (fixtures/),
you cannot re-include children with !fixtures/keep. Use a file-pattern
(*.junk / !keep.junk) or ignore the directory's contents
(fixtures/* / !fixtures/keep).
Installing the skill
musts skill install
Installs this skill globally for every detected agent runner. Requires
npx (Node.js) on PATH. Pass --agent claude-code (or cursor,
windsurf, …) to restrict the install to one runner.