Musts Validation Skill
Drop this file into your agent's skill folder. Claude Code reads it from .claude/skills/; other agent runners have their own paths. Source: docs/musts-design.md §14 + docs/PLAN.md Phase 7.
Purpose
Ensure repository-defined musts validation is clean before declaring a task done.
The single hard rule of musts:
The task is not done until musts validate is empty.
Protocol
Run musts validate at the start of every task that touches code and any time you are about to declare work complete.
Treat the returned task list as the validation todo list. validate emits every dirty task (no batching) and is idempotent — re-running it never invalidates the ids it just issued.
Close each task by kind:
- Deterministic (
do: is a plain command — cargo/*, bazel/{build,test}): run musts run <task-id>. musts executes the command, checks the real exit code, and records evidence for you — no re-running to satisfy the loop. A non-zero exit prints the output and records nothing; fix and re-run.
- Judgment (
agent, mav): perform the validation yourself and record evidence (step 7).
If multiple judgment tasks are independent, use subagents in parallel — but not when the underlying tool is single-resource (simulators, local servers, build locks, shared databases). When in doubt, run sequentially.
If one task satisfies multiple checks, execute it once. Do not split.
Do not invent evidence requirements. Use the evidence: and submit: lines in the task report. Asset kinds and the text requirement are extension-defined.
Record evidence (judgment checks) with:
musts evidence <task-id> --text "<one-line summary>" --asset <path>...
The <task-id> comes from the report. --asset may repeat. Assets are validated in place — musts no longer archives them; the committed .musts/ledger.lock.yaml is the record. Keep logs outside the workspace so edits don't perturb the scope hash.
If musts run/musts evidence exits non-zero, read the error:
- Exit 1 = the command failed (
musts run) or the extension rejected the evidence (missing kind, zero-byte file, failure markers in the log). Fix and re-run.
- Exit 2 with "stale" = files inside this task's scopes changed after the task was issued. Re-run
musts validate and follow the fresh task list.
- Exit 2 with "no longer applies" = that task id isn't in the current report (its check is already green, or a fresh
validate changed the set). Re-run validate and use the current ids.
Re-run musts validate. If new tasks appear, repeat the loop.
If musts validate reports clean, the work can be reported as complete.
Hard rules
- Do not silence the loop. If a task feels redundant or already-satisfied, that's the extension's call, not yours: every task in the report is dirty per the ledger. Run it, submit evidence, or fix the underlying issue.
- Snapshot assets outside the workspace when you can, especially logs you produce while running the task. Writing them inside the workspace mutates the scope hash and can stale the task you're about to submit evidence for.
- Run the validation loop after your last edit, before you commit. Any change to any file in a scope — including a comment, whitespace, or a
.gitignore rule that doesn't actually move files in or out — re-hashes that scope and invalidates the matching entries in .musts/ledger.lock.yaml. Order: edit → validate → submit → commit. "Submit → edit → commit" looks fine locally (the SQLite ledger still has the old scope_hash) but ships a stale lock to every clone.
The committed ledger lock
.musts/ledger.lock.yaml is the portable record of what's been validated. Every accepted musts evidence appends a (check_id, scope_hash) entry to it; scope_hash is a content-hash fingerprint of the files in the check's effective scope. The file lives next to the rest of the workspace (it is committed, unlike state.sqlite which is per-machine) and musts validate consults it alongside the local SQLite ledger when answering "is this check green?". A clone that pulls the lock inherits the team's validated state immediately — the agent only sees tasks for scopes its own changes invalidated.
What this means for your workflow:
- A scope's hash changes any time any file inside it changes. Comments, doc tweaks,
.gitignore edits, reordering imports — none of them change the underlying tool's behaviour, but all of them re-hash the scope and detach it from the prior (check_id, scope_hash) entries. musts chooses conservative invalidation over guessing what's "semantic" vs "cosmetic"; it has no way to tell the difference. If you must edit late in the cycle, run the loop again before you commit.
- A check is only invalidated by what it depends on. Its own declaration (
uses, with, paths, exclude_paths), the files in its effective scope, and the extension implementing its capability. Editing a sibling check in the same MUSTS.yml, adding a comment to the file, or registering an unrelated extension does not reopen it. Narrow paths: is therefore the main lever you control over how often a check comes back.
- The lock is a union, not a snapshot. Multiple
(check, scope_hash) entries can accumulate per check as the codebase evolves. That's by design — a clone is green if its current scope hash matches any of them. Don't hand-prune the file; musts writes it monotonically and a future cleanup pass will retire dead entries.
- Sub-workspaces (fixtures, demos, examples) often gitignore their own lock so the canonical walkthrough starts with nothing validated. If you're working on one of those and
validate keeps reporting pending tasks despite a clean run, check the project's .gitignore before assuming musts is broken.
- Commit
.musts/.gitattributes. musts writes it next to the lock with ledger.lock.yaml merge=union, which is what stops two branches that both recorded evidence from conflicting on the lock. It only works once it is committed. If you ever do see conflict markers in the lock, keep every entry from both sides — the ledger is append-only, so the union is always the right resolution.
- A merge whose result nobody validated reopens the checks covering it, and that is correct. If
main moved while your branch was open, the tree that lands carries both sets of edits and neither side ever checked that combination. Nothing was lost from the ledger — the tree is genuinely new. To avoid paying for an expensive check twice, bring main into the branch and re-close the loop before merging, so the branch validates the tree that actually lands. Narrower paths: and nested manifests are the other lever: they keep unrelated churn from touching an expensive check's scope.
Authoring a MUSTS.yml
Run musts lint after writing one. Everything below is a rule it enforces,
and every rule exists because a real manifest got it wrong and quietly cost
an agent's reasoning on every change for months.
When a finding is deliberate, silence that single rule for the file rather
than deleting the check or widening a glob to make lint quiet:
# These two globs are built to be disjoint knowing `*` crosses `/`.
# musts-lint: allow glob-crosses-directories
The suppression covers the whole manifest and only the named rule; take
several with allow rule-a, rule-b. It also clears the exit code for an
error-level rule, so an opted-out finding does not gate CI.
The single question to ask of each check: does satisfying this need
judgment, or does it need a command run? Only judgment belongs under
uses: agent.
- "The command exited 0" is not judgment. A fact like
Run `bazelisk test //T:T` and confirm all tests pass (exit 0) forces an agent to run
the command, read the output, and write prose about it — every time. Use a
runnable capability (bazel/test, cargo/*) instead and musts run <task-id> executes it, checks the real exit code, and records the
evidence for you. The agent never reads the log.
- "If changes touch X…" is a
paths: entry, not a fact. Written as
prose, every unrelated change pays an agent to read the condition,
decide it does not apply, and submit evidence saying so. Written in
paths:, the check does not fire at all. Same for "if changes are
unrelated, this is trivially satisfied" — that sentence is the absence of
a paths: filter, spelled out.
- No
paths: means every change in the manifest's folder. For a runnable
capability that is usually right. For uses: agent it means an agent
re-reasons about the whole folder because someone fixed a typo in it.
- Do not assert negatives about files that automation edits. "
build_number
was not edited manually" is unsatisfiable in practice: release automation
edits the watched file on every beta, which changes the scope hash, which
reopens the check, which makes an agent write prose — forever. One repo's
ledger carries 70 satisfactions of exactly this check, and it has never
once gone red. If a rule is about who changed a file, enforce it in CI
against the diff, not in the validation loop.
- Prefer two disjoint sets of positive globs over exclusions. "Views run
the snapshot suite, non-views run the unit suite" is clearer, and stays
correct, as two
paths: lists that cannot both match than as one broad
list with exclude_paths: carved out of it.
Use musts stats on an existing repo to find the checks worth rewriting: a
check with many reopens and zero reds is paying for validation work that has
never objected to anything.
Glob semantics
paths: and exclude_paths: are matched against the path relative to
the manifest's own folder. A MUSTS.yml in App/macOSUI/MainWindow/
writes MacOSMainView.swift for the file beside it. Repeating the folder
(App/macOSUI/MainWindow/MacOSMainView.swift) matches nothing.
Beyond that they do not behave like .gitignore. Three surprises, all
of which have bitten real manifests:
|
Behaviour |
| Case |
Insensitive. *View.swift also matches RequestReview.swift and MeetingPreview.swift. |
* and ** |
Both cross /. UI/*View.swift also matches UI/Deep/Nested/FooView.swift. There is no "one directory level" wildcard. |
Leading ! |
Rejected at parse time. globset treats ! as a literal, so !foo would match nothing at all. Use exclude_paths: instead. |
A check whose paths: match nothing cannot fire, and validate lists it
under Ignored checks with the reason. It is never dropped in silence —
that is how one repo's check went 89 days without running once.
musts lint reports each of these against the files actually in your tree,
naming the ones that match only because of the surprise — so you never have
to reason about it from the pattern alone.
Capabilities at a glance
agent is built into the musts binary. Manifests using uses: agent need no installed extension; the task tells you which facts to verify and asks for a text summary plus whatever assets you captured.
- Any third-party
uses: ... is installed as an extension under <workspace>/.musts/extensions/<name>/. They can be Rust binaries, bash scripts, Python — anything that speaks the JSON protocol.
Quick reference
# What does musts want from me right now?
musts validate
# Submit evidence for one task.
musts evidence bazel-build-login \
--text "bazel build //App/Login:Login succeeded" \
--asset /tmp/login-build.log
# Submit evidence with multiple assets.
musts evidence mav-login-flow \
--text "Validated valid + invalid email flows" \
--asset /tmp/login-success.png \
--asset /tmp/login-run.mp4 \
--asset /tmp/mav-report.json
# Confirm everything closed.
musts validate
What the report looks like
Musts validation pending: 2 tasks.
1. bazel-build-login
do: Run `bazel build //App/Login:Login`.
run: musts run bazel-build-login
2. mav-expect-app-login
do: Validate MAV expectations for App/Login …
evidence: screenshot + video + mav-report
submit: musts evidence mav-expect-app-login --text "..." --asset <screenshot> …
Run runnable checks with `musts run <task-id>`; record judgment checks with `musts evidence`. Then rerun `musts validate` until clean.
Deterministic checks (cargo/*, bazel/{build,test}) show a run: line — musts run executes them and records evidence for you. Judgment checks (agent, mav) show evidence: + submit:.
When clean:
Musts validation clean.
When you should NOT use this skill
- Tasks that don't touch a musts-validated repo (
.musts/ absent, no MUSTS.yml).
- Refactors that have already shipped — musts ratchets evidence forward, it doesn't re-validate the past.
- One-off scripts that bypass the agent loop entirely.
If musts validate exits 0 with "No MUSTS.yml files found.", the workspace isn't validated by musts and the loop doesn't apply.
1---2name: docs3description: Musts Validation Skill4---5# Musts Validation Skill67Drop this file into your agent's skill folder. Claude Code reads it from `.claude/skills/`; other agent runners have their own paths. Source: `docs/musts-design.md` §14 + `docs/PLAN.md` Phase 7.89---1011## Purpose1213Ensure repository-defined musts validation is **clean** before declaring a task done.1415The single hard rule of musts:1617> The task is not done until `musts validate` is empty.1819## Protocol20211. **Run `musts validate`** at the start of every task that touches code and any time you are about to declare work complete.222. Treat the returned task list as the validation todo list. `validate` emits **every** dirty task (no batching) and is idempotent — re-running it never invalidates the ids it just issued.233. Close each task by kind:24 - **Deterministic** (`do:` is a plain command — `cargo/*`, `bazel/{build,test}`): run `musts run <task-id>`. musts executes the command, checks the real exit code, and records evidence for you — no re-running to satisfy the loop. A non-zero exit prints the output and records nothing; fix and re-run.25 - **Judgment** (`agent`, `mav`): perform the validation yourself and record evidence (step 7).264. If multiple judgment tasks are independent, use subagents in parallel — but **not** when the underlying tool is single-resource (simulators, local servers, build locks, shared databases). When in doubt, run sequentially.275. If one task `satisfies` multiple checks, execute it **once**. Do not split.286. **Do not invent evidence requirements.** Use the `evidence:` and `submit:` lines in the task report. Asset kinds and the `text` requirement are extension-defined.297. **Record evidence** (judgment checks) with:3031 ```bash32 musts evidence <task-id> --text "<one-line summary>" --asset <path>...33 ```3435 The `<task-id>` comes from the report. `--asset` may repeat. Assets are validated **in place** — musts no longer archives them; the committed `.musts/ledger.lock.yaml` is the record. Keep logs outside the workspace so edits don't perturb the scope hash.368. If `musts run`/`musts evidence` exits non-zero, **read the error**:37 - Exit **1** = the command failed (`musts run`) or the extension rejected the evidence (missing kind, zero-byte file, failure markers in the log). Fix and re-run.38 - Exit **2 with "stale"** = files inside this task's scopes changed after the task was issued. Re-run `musts validate` and follow the fresh task list.39 - Exit **2 with "no longer applies"** = that task id isn't in the current report (its check is already green, or a fresh `validate` changed the set). Re-run `validate` and use the current ids.409. **Re-run `musts validate`.** If new tasks appear, repeat the loop.4110. If `musts validate` reports clean, the work can be reported as complete.4243## Hard rules4445- **Do not silence the loop.** If a task feels redundant or already-satisfied, that's the extension's call, not yours: every task in the report is dirty per the ledger. Run it, submit evidence, or fix the underlying issue.46- **Snapshot assets outside the workspace** when you can, especially logs you produce while running the task. Writing them inside the workspace mutates the scope hash and can stale the task you're about to submit evidence for.47- **Run the validation loop after your last edit, before you commit.** Any change to any file in a scope — including a comment, whitespace, or a `.gitignore` rule that doesn't actually move files in or out — re-hashes that scope and invalidates the matching entries in `.musts/ledger.lock.yaml`. Order: **edit → validate → submit → commit**. "Submit → edit → commit" looks fine locally (the SQLite ledger still has the old `scope_hash`) but ships a stale lock to every clone.4849## The committed ledger lock5051`.musts/ledger.lock.yaml` is the portable record of what's been validated. Every accepted `musts evidence` appends a `(check_id, scope_hash)` entry to it; `scope_hash` is a content-hash fingerprint of the files in the check's effective scope. The file lives next to the rest of the workspace (it is **committed**, unlike `state.sqlite` which is per-machine) and `musts validate` consults it alongside the local SQLite ledger when answering "is this check green?". A clone that pulls the lock inherits the team's validated state immediately — the agent only sees tasks for scopes its own changes invalidated.5253What this means for your workflow:5455- **A scope's hash changes any time any file inside it changes.** Comments, doc tweaks, `.gitignore` edits, reordering imports — none of them change the underlying tool's behaviour, but all of them re-hash the scope and detach it from the prior `(check_id, scope_hash)` entries. musts chooses conservative invalidation over guessing what's "semantic" vs "cosmetic"; it has no way to tell the difference. If you must edit late in the cycle, run the loop again before you commit.56- **A check is only invalidated by what it depends on.** Its own declaration (`uses`, `with`, `paths`, `exclude_paths`), the files in its effective scope, and the extension implementing its capability. Editing a *sibling* check in the same `MUSTS.yml`, adding a comment to the file, or registering an unrelated extension does **not** reopen it. Narrow `paths:` is therefore the main lever you control over how often a check comes back.57- **The lock is a union, not a snapshot.** Multiple `(check, scope_hash)` entries can accumulate per check as the codebase evolves. That's by design — a clone is green if its current scope hash matches *any* of them. Don't hand-prune the file; musts writes it monotonically and a future cleanup pass will retire dead entries.58- **Sub-workspaces (fixtures, demos, examples) often gitignore their own lock** so the canonical walkthrough starts with nothing validated. If you're working on one of those and `validate` keeps reporting pending tasks despite a clean run, check the project's `.gitignore` before assuming musts is broken.59- **Commit `.musts/.gitattributes`.** musts writes it next to the lock with `ledger.lock.yaml merge=union`, which is what stops two branches that both recorded evidence from conflicting on the lock. It only works once it is committed. If you ever *do* see conflict markers in the lock, keep every entry from both sides — the ledger is append-only, so the union is always the right resolution.60- **A merge whose result nobody validated reopens the checks covering it, and that is correct.** If `main` moved while your branch was open, the tree that lands carries both sets of edits and neither side ever checked that combination. Nothing was lost from the ledger — the tree is genuinely new. To avoid paying for an expensive check twice, bring `main` into the branch and re-close the loop *before* merging, so the branch validates the tree that actually lands. Narrower `paths:` and nested manifests are the other lever: they keep unrelated churn from touching an expensive check's scope.6162## Authoring a `MUSTS.yml`6364Run `musts lint` after writing one. Everything below is a rule it enforces,65and every rule exists because a real manifest got it wrong and quietly cost66an agent's reasoning on every change for months.6768When a finding is deliberate, silence that single rule for the file rather69than deleting the check or widening a glob to make lint quiet:7071```yaml72# These two globs are built to be disjoint knowing `*` crosses `/`.73# musts-lint: allow glob-crosses-directories74```7576The suppression covers the whole manifest and only the named rule; take77several with `allow rule-a, rule-b`. It also clears the exit code for an78error-level rule, so an opted-out finding does not gate CI.7980The single question to ask of each check: **does satisfying this need81judgment, or does it need a command run?** Only judgment belongs under82`uses: agent`.8384- **"The command exited 0" is not judgment.** A fact like ``Run `bazelisk85 test //T:T` and confirm all tests pass (exit 0)`` forces an agent to run86 the command, read the output, and write prose about it — every time. Use a87 runnable capability (`bazel/test`, `cargo/*`) instead and `musts run88 <task-id>` executes it, checks the real exit code, and records the89 evidence for you. The agent never reads the log.90- **"If changes touch X…" is a `paths:` entry, not a fact.** Written as91 prose, every *unrelated* change pays an agent to read the condition,92 decide it does not apply, and submit evidence saying so. Written in93 `paths:`, the check does not fire at all. Same for "if changes are94 unrelated, this is trivially satisfied" — that sentence is the absence of95 a `paths:` filter, spelled out.96- **No `paths:` means every change in the manifest's folder.** For a runnable97 capability that is usually right. For `uses: agent` it means an agent98 re-reasons about the whole folder because someone fixed a typo in it.99- **Do not assert negatives about files that automation edits.** "`build_number`100 was not edited manually" is unsatisfiable in practice: release automation101 edits the watched file on every beta, which changes the scope hash, which102 reopens the check, which makes an agent write prose — forever. One repo's103 ledger carries 70 satisfactions of exactly this check, and it has never104 once gone red. If a rule is about *who* changed a file, enforce it in CI105 against the diff, not in the validation loop.106- **Prefer two disjoint sets of positive globs over exclusions.** "Views run107 the snapshot suite, non-views run the unit suite" is clearer, and stays108 correct, as two `paths:` lists that cannot both match than as one broad109 list with `exclude_paths:` carved out of it.110111Use `musts stats` on an existing repo to find the checks worth rewriting: a112check with many reopens and zero reds is paying for validation work that has113never objected to anything.114115### Glob semantics116117`paths:` and `exclude_paths:` are matched against the path **relative to118the manifest's own folder**. A `MUSTS.yml` in `App/macOSUI/MainWindow/`119writes `MacOSMainView.swift` for the file beside it. Repeating the folder120(`App/macOSUI/MainWindow/MacOSMainView.swift`) matches nothing.121122Beyond that they do **not** behave like `.gitignore`. Three surprises, all123of which have bitten real manifests:124125| | Behaviour |126|---|---|127| Case | **Insensitive.** `*View.swift` also matches `RequestReview.swift` and `MeetingPreview.swift`. |128| `*` and `**` | **Both cross `/`.** `UI/*View.swift` also matches `UI/Deep/Nested/FooView.swift`. There is no "one directory level" wildcard. |129| Leading `!` | **Rejected at parse time.** `globset` treats `!` as a literal, so `!foo` would match nothing at all. Use `exclude_paths:` instead. |130131A check whose `paths:` match nothing cannot fire, and `validate` lists it132under **Ignored checks** with the reason. It is never dropped in silence —133that is how one repo's check went 89 days without running once.134135`musts lint` reports each of these against the files actually in your tree,136naming the ones that match only because of the surprise — so you never have137to reason about it from the pattern alone.138139## Capabilities at a glance140141- **`agent`** is built into the musts binary. Manifests using `uses: agent` need no installed extension; the task tells you which facts to verify and asks for a text summary plus whatever assets you captured.142- **Any third-party `uses: ...`** is installed as an extension under `<workspace>/.musts/extensions/<name>/`. They can be Rust binaries, bash scripts, Python — anything that speaks the JSON protocol.143144## Quick reference145146```bash147# What does musts want from me right now?148musts validate149150# Submit evidence for one task.151musts evidence bazel-build-login \152 --text "bazel build //App/Login:Login succeeded" \153 --asset /tmp/login-build.log154155# Submit evidence with multiple assets.156musts evidence mav-login-flow \157 --text "Validated valid + invalid email flows" \158 --asset /tmp/login-success.png \159 --asset /tmp/login-run.mp4 \160 --asset /tmp/mav-report.json161162# Confirm everything closed.163musts validate164```165166## What the report looks like167168```text169Musts validation pending: 2 tasks.1701711. bazel-build-login172 do: Run `bazel build //App/Login:Login`.173 run: musts run bazel-build-login1741752. mav-expect-app-login176 do: Validate MAV expectations for App/Login …177 evidence: screenshot + video + mav-report178 submit: musts evidence mav-expect-app-login --text "..." --asset <screenshot> …179180Run runnable checks with `musts run <task-id>`; record judgment checks with `musts evidence`. Then rerun `musts validate` until clean.181```182183Deterministic checks (`cargo/*`, `bazel/{build,test}`) show a `run:` line — `musts run` executes them and records evidence for you. Judgment checks (`agent`, `mav`) show `evidence:` + `submit:`.184185When clean:186187```text188Musts validation clean.189```190191## When you should NOT use this skill192193- Tasks that don't touch a musts-validated repo (`.musts/` absent, no `MUSTS.yml`).194- Refactors that have already shipped — musts ratchets evidence forward, it doesn't re-validate the past.195- One-off scripts that bypass the agent loop entirely.196197If `musts validate` exits 0 with "No MUSTS.yml files found.", the workspace isn't validated by musts and the loop doesn't apply.