Gate every check a repo has set up
A repository that uses several manni tools usually gates them one workflow step
at a time. There is a step for meta validate, another for cite check, and
one for each domain after that. Set up a new domain and the workflow does not
learn about it. Nothing fails. The check is missing.
manni check runs the checks the repository has set up and nothing else. It
reads the same manni.config.yaml every tool reads, so setting up a domain is
what puts it in the gate. manni status says which domains that is.
Read what is in play
Section titled “Read what is in play”Run manni status from anywhere in the repository. It reads the config and the
pages, runs no check, and always exits 0.
$ manni statusmanni 4.3.0 config manni.config.yaml collection site (2 files)
meta in play meta: sectioncite not set up no page carries citations Pin a sentence to its source with manni cite add.lint not set up no lint: section Add a lint: section. manni lint templates lists the templates it can apply.docevals not set up no page resolves any evals Add a docevals: section, or declare evals in a page's frontmatter.term not set up no terms in any collection Add a page with type: term to a collection.strategy not set up no strategy records in any collection Add a page with type: journey to a collection.graph not set up no graph: section Run manni graph init to add a starter graph: section.a11y not checked needs a running site and a browser Run manni site preview, then manni a11y check.test not checked runs the pages' shell steps and synthetic users Run manni test procedures or manni test tasks.tracevals not checked judges recorded sessions with a model Run manni tracevals run over sessions, or add tracevals.runtime to judge each turn.# exit 0Each row has one of three states.
- In play. The domain is set up, and
manni checkruns it. Fortracevals, a hook runs it. - Not set up. Nothing in the repository asks for the domain. The reason says why, and the line under it says what would set it up.
- Not checked.
manni checknever runs this domain from the command line, whatever the repository holds. The line under it names the command that does.
What puts a domain in play
Section titled “What puts a domain in play”There are no config keys for this. A domain is in play when its own section of
manni.config.yaml exists, or when the pages carry its declarations. Nothing
manni would apply by default counts as set up.
| Domain | In play when | Runs |
|---|---|---|
meta | meta: exists, or a page names its own $schema. A file covered only by the built-in default schemas is skipped. | meta validate |
cite | A page carries citations, as markers or in a citations manifest. | cite check |
lint | lint: exists. | lint check |
docevals | docevals: exists, or a page declares evals. A run that resolves no evals counts as not in play. | docevals run, deterministic evals only |
term | The collections hold at least one term. | term check |
strategy | The collections hold at least one record, a page of type audience, persona or journey. | strategy check |
graph | graph: exists. | graph build in memory, then graph check |
tracevals | tracevals.runtime exists, and the user turned tracevals on, so MANNI_TRACEVALS_HOOKS is 1. | Nothing on the command line. Under a Claude Code hook, a judgement of the turn that just ended. |
The built-in default schemas are the reason meta has a rule of its own. A
repository that only uses docevals would otherwise fail every page that has no
type. The gate would block on a check nobody chose.
manni check also reads only the files in a declared collection. A README.md
outside every collection passes without a word.
Run the gate
Section titled “Run the gate”$ manni checkmeta validate✗ docs/limits.md (root) must have required property 'description' (line 1) [./schemas/page.schema.json]
2 files checked, 1 passed, 1 failed, 1 error
skipped cite check no page carries citationsskipped lint check no lint: sectionskipped docevals run no page resolves any evalsskipped term check no terms in any collectionskipped strategy check no strategy records in any collectionskipped graph check no graph: section
7 checks over 2 files: 1 failed, 6 skipped# exit 1Each block is the domain’s own report, under the name of the check that made it. Only error-level findings fail the run. A warning or a notice never does, which is the rule each domain applies to its own exit code.
Read the skipped lines
Section titled “Read the skipped lines”The skipped lines answer “what did you not check?”. Each names a check and the
reason it did not run. Here meta ran, and five checks were skipped because the
repository has not set those domains up.
A skipped line is not a failure. If a skipped line surprises you, manni status
shows the same reason and what would put the domain in play.
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
0 | Every check that ran passed. Skipped checks do not change this. |
1 | At least one check that ran found an error-level problem. |
2 | manni could not run, or a check could not run. This is not a bad page. |
The split lets a pipeline tell a bad page from a broken setup. When a check
cannot run, the others still do. Its line reads error, the cause goes to
stderr, and the exit code is 2 even if nothing else failed.
$ manni check docs/index.mdmanni lint check could not run: Unknown key "bogus" under lint: in manni.config.yaml. Supported keys: allowEmpty, overrides, structure, template, templates, types.meta validate1 file checked, 1 passed, 0 failed, 0 errors
skipped cite check no page carries citationserror lint check Unknown key "bogus" under lint: in manni.config.yaml. Supported keys: allowEmpty, overrides, structure, template, templates, types.skipped docevals run no page resolves any evals
4 checks over 1 file: 0 failed, 2 skipped, 1 could not run# exit 2A repository with no manni.config.yaml also exits 2, and the message says
where to create one. So does a config where nothing is set up, and that message
says to run manni status.
Run it in CI
Section titled “Run it in CI”Replace the per-domain steps with one. The
manni GitHub Action runs
manni check --format github when given no inputs. Each domain prints its own
github output, so findings appear as annotations on the line that caused them.
jobs: manni: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: fetch-depth: 0 # cite check reads the commit each pin was minted at
- uses: hawkeyexl/manni@v4The same gate without the Action is a run: step. Redirect stdin from
/dev/null. Otherwise check waits up to 1.5 seconds on the runner’s silent
pipe, reading it for a Claude Code hook.
- uses: actions/setup-node@v7 with: node-version: 24
- name: Run every manni check run: npx -y @hawkeyexl/manni check -f github < /dev/nullThe checkout needs the full history. manni cite check compares each pin with
the file as it was at the commit that minted it, and a shallow clone cannot
supply that. The cite CI page has the details.
For a script, -f json prints one object. Each report is that domain’s own
-f json output, unchanged.
$ manni check docs/limits.md -f json{ "status": "fail", "files": ["docs/limits.md"], "checks": [ { "command": "meta validate", "status": "fail", "report": { … } }, { "command": "cite check", "status": "skipped", "message": "no page carries citations" }, … ]}A check’s status is pass, fail, error or skipped. The last two carry a
message in place of a report. The top-level status is pass or fail.
Send every finding to code scanning
Section titled “Send every finding to code scanning”-f sarif prints one SARIF 2.1.0 log for every check that ran. Each domain’s
run carries its own category, such as manni/meta/, so upload the log once and
leave category unset. A domain you set up later reaches code scanning with no
change to the workflow. The Action’s output-file input writes the log where
the upload can read it:
jobs: manni: runs-on: ubuntu-latest permissions: contents: read security-events: write # upload-sarif needs it; without it the upload is a 403 steps: - uses: actions/checkout@v7 with: fetch-depth: 0 - id: manni uses: hawkeyexl/manni@v4 with: format: sarif output-file: manni.sarif - if: ${{ !cancelled() && hashFiles('manni.sarif') != '' }} uses: github/codeql-action/upload-sarif@v4 with: sarif_file: manni.sarifThe manni step still fails the job on exit 1 or 2, and the upload runs anyway.
When manni fails before printing, the Action removes the empty file, so the
upload is skipped. A run: step gets the same log from
npx -y @hawkeyexl/manni check -f sarif > manni.sarif < /dev/null.
The upload needs permissions: security-events: write. A last run in the log,
manni/check/, names each check that was skipped or could not run. A check that
could not run gets no run of its own, so its last analysis stays in place.
Fix first what your readers hit
Section titled “Fix first what your readers hit”A refactor can turn many pages red at once, across several checks. Each check’s report lists its own findings by file. The file order says nothing about which readers the change hurts.
When your collections hold persona and journey records, manni check ranks the
red pages by the readers they serve. One block follows the skipped lines, before
the summary. It counts every check’s findings under each persona and journey the
pages name, worst first.
$ manni check…skipped lint check no lint: section
The pages with findings serve 4 personas and 48 journeys. maya Maya, Documentation Engineer 28 errors, 151 warnings and 58 notices on 101 pages sara Sara, Schema Author / Information Architect 22 errors, 124 warnings and 62 notices on 100 pages devin Devin, Platform / CI Engineer 20 errors, 172 warnings and 63 notices on 87 pages theo Theo, Doc Contributor 3 errors, 95 warnings and 31 notices on 61 pages m1 Stand up metadata validation for my repo 15 errors, 9 warnings and 3 notices on 6 pages s2 Wire schemas to the right documents 14 errors, 2 warnings and 4 notices on 3 pages d2 Govern one schema across many repos 12 errors, 3 warnings and 3 notices on 2 pages …
7 checks over 345 files: 3 failed, 1 skipped# exit 1Errors sort before warnings, so a journey with one error outranks one with
twenty warnings. Take the journey at the top. The json output carries the
same roll-up with each entry’s pages, so one jq names the pages to fix first:
$ manni check -f json | jq -r '.impact[] | select(.id == "m1") | .pages[]'docs/src/content/docs/meta/get-started/index.mdxdocs/src/content/docs/meta/reference/configuration.mdxdocs/src/content/docs/meta/reference/schemas/index.mdxdocs/src/content/docs/meta/set-up/config.mdxdocs/src/content/docs/meta/set-up/index.mdxdocs/src/content/docs/meta/set-up/retrofit.mdxFix those pages and run the check again. The journey leaves the list, and so does every journey that shared only those pages. A draft or archived journey sorts after the live ones, whatever its count.
On a pull request, -f github opens with the same ranking as one notice. It
prints before every check’s own annotations, so GitHub’s cap on annotations
never hides it. The CLI reference
has the counting rules and each format’s shape.
Narrow the run to some files
Section titled “Narrow the run to some files”Name files, directories or globs and manni check runs the per-file checks on
the collection members among them. A file outside every collection is skipped
with one notice on stderr.
$ manni check docs/limits.md README.mdmanni: Skipped 1 file outside every collection: README.mdmeta validate✗ docs/limits.md (root) must have required property 'description' (line 1) [./schemas/page.schema.json]
1 file checked, 0 passed, 1 failed, 1 error
skipped cite check no page carries citationsskipped lint check no lint: sectionskipped docevals run no page resolves any evals
4 checks over 1 file: 1 failed, 3 skipped# exit 1Three checks cover the whole set rather than single files. term check,
strategy check and graph check run on a bare manni check and not when you
name files, because one page can move the glossary, the strategy or the graph.
cite check follows the same split.
It checks the named pages, or every page with citations on a bare run.
What stays its own step
Section titled “What stays its own step”manni check never runs a11y from the command line. It needs a built site and a browser, so
manni status lists it as not checked. Keep it as its own workflow step.
manni check never runs test either. Doc Detective runs the pages’ shell steps and synthetic users, and
check also runs from Claude Code hooks. Run manni test procedures as its own step.
tracevals needs recorded sessions and a model, so a bare manni check does not run it either.
Run manni tracevals run over recorded sessions as its own step. Inside a Claude Code session the
manni plugin does more, for a user who turned on its tracevals option. When tracevals.runtime
exists in manni.config.yaml, manni check then also judges each turn against the rules the
repository declares for agents, at Stop and at SubagentStop.
Catch a broken rule before the agent hands back walks through it.
Without that section, the row stays not checked. With the section and the option off, the row
reads runtime evals are set up, and turned off here.