Skip to content

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.

Run manni status from anywhere in the repository. It reads the config and the pages, runs no check, and always exits 0.

Terminal window
$ manni status
manni 4.3.0 config manni.config.yaml collection site (2 files)
meta in play meta: section
cite 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 0

Each row has one of three states.

  • In play. The domain is set up, and manni check runs it. For tracevals, 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 check never runs this domain from the command line, whatever the repository holds. The line under it names the command that does.

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.

DomainIn play whenRuns
metameta: exists, or a page names its own $schema. A file covered only by the built-in default schemas is skipped.meta validate
citeA page carries citations, as markers or in a citations manifest.cite check
lintlint: exists.lint check
docevalsdocevals: exists, or a page declares evals. A run that resolves no evals counts as not in play.docevals run, deterministic evals only
termThe collections hold at least one term.term check
strategyThe collections hold at least one record, a page of type audience, persona or journey.strategy check
graphgraph: exists.graph build in memory, then graph check
tracevalstracevals.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.

Terminal window
$ manni check
meta 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 citations
skipped lint check no lint: section
skipped docevals run no page resolves any evals
skipped term check no terms in any collection
skipped strategy check no strategy records in any collection
skipped graph check no graph: section
7 checks over 2 files: 1 failed, 6 skipped
# exit 1

Each 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.

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.

CodeMeaning
0Every check that ran passed. Skipped checks do not change this.
1At least one check that ran found an error-level problem.
2manni 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.

Terminal window
$ manni check docs/index.md
manni lint check could not run: Unknown key "bogus" under lint: in manni.config.yaml. Supported keys: allowEmpty, overrides, structure, template, templates, types.
meta validate
1 file checked, 1 passed, 0 failed, 0 errors
skipped cite check no page carries citations
error 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 2

A 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.

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.

.github/workflows/docs.yml
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@v4

The 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.

.github/workflows/docs.yml
- uses: actions/setup-node@v7
with:
node-version: 24
- name: Run every manni check
run: npx -y @hawkeyexl/manni check -f github < /dev/null

The 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.

Terminal window
$ 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.

-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:

.github/workflows/docs.yml
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.sarif

The 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.

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.

Terminal window
$ 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 1

Errors 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:

Terminal window
$ manni check -f json | jq -r '.impact[] | select(.id == "m1") | .pages[]'
docs/src/content/docs/meta/get-started/index.mdx
docs/src/content/docs/meta/reference/configuration.mdx
docs/src/content/docs/meta/reference/schemas/index.mdx
docs/src/content/docs/meta/set-up/config.mdx
docs/src/content/docs/meta/set-up/index.mdx
docs/src/content/docs/meta/set-up/retrofit.mdx

Fix 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.

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.

Terminal window
$ manni check docs/limits.md README.md
manni: Skipped 1 file outside every collection: README.md
meta 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 citations
skipped lint check no lint: section
skipped docevals run no page resolves any evals
4 checks over 1 file: 1 failed, 3 skipped
# exit 1

Three 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.

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.