Skip to content

Keep an agent's docs green

An agent edits a page and stops. The pull request then goes red, and you paste the log into a new session and wait for a second round. The plugin closes that loop. The agent hears about a broken page at the edit that broke it, and it cannot finish while an error is outstanding.

The plugin runs manni check for the agent. It checks only what your repository has set up. A domain you never set up never blocks the agent, so a repository that only uses docevals is not failed by meta.

It also gives the agent two things to read with. A read of a docs page brings the page’s metadata, sidecar fields included. An MCP server serves your docs graph, published for the session and kept current as the agent works.

Two commands add the marketplace and install the plugin.

Terminal window
claude plugin marketplace add hawkeyexl/manni
claude plugin install manni@manni

To offer it to everyone who opens the repository in Claude Code, commit it to .claude/settings.json. Claude Code then offers the plugin to each contributor who opens the repository.

.claude/settings.json
{
"extraKnownMarketplaces": {
"manni": {
"source": { "source": "github", "repo": "hawkeyexl/manni" }
}
},
"enabledPlugins": {
"manni@manni": true
}
}

The hooks and skills run the first manni they find:

  1. In the manni repository itself, that checkout’s own build, dist/cli.js. Before npm run build, nothing runs there.
  2. The copy the project installed, found by walking up through node_modules.
  3. A global install, found under npm root -g.
  4. In a repository with a manni.config.yaml and no manni at all, the version the plugin ships at, through npx --yes. npx downloads it into its own cache on first use, so the first hook waits for that download.

A repository with no manni config and no manni gets nothing, and no step asks the registry. Pinning a version yourself means installing it in the project.

Terminal window
npm install --save-dev @hawkeyexl/manni

With no manni config and no manni, a hook exits at once and prints nothing to the agent.

Three hooks run when a session starts. The two tracevals hooks run only for a user who turned tracevals on. For everyone else they start no manni at all.

manni tracevals capture records a manifest of the session under .manni/tracevals/sessions/. It stores the hash of every instruction artifact the project holds, so tracevals can tell whether one changed after the session began. It runs at startup only.

manni tracevals prepare runs in the background and never holds the session up. When tracevals.runtime exists, it fetches a local judge model that is not on disk yet. It also extracts the rules of every file that applies to every session. The first Stop then finds both ready. With no runtime section it prints one line and does nothing else.

manni status prints what is in play, and Claude Code adds that to the agent’s context. With no manni.config.yaml it prints nothing.

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
lint not set up no lint: section
docevals not set up no page resolves any evals
term not set up no terms in any collection
graph not set up no graph: section
a11y not checked needs a running site and a browser
test not checked runs the shell steps the pages carry
tracevals not checked judges recorded sessions with a model
After you edit a file in site, manni check runs on it, and errors come back to you at once.
Before you finish, manni check runs on every file you changed. You get one repair pass.
The manni:fix skill says how to repair each finding.

Where the strategy domain is in play, the briefing gains one line before the last:

Pages name their readers and jobs in audiences:, personas: and journeys:. Write each page for the ones it names; manni strategy list prints every record.

You see the same table when you run manni status yourself, so you and the agent read one answer. Yours also has a line under each domain that is not in play, naming what would set it up. That choice is yours, so the agent never sees those lines.

The status hook also exports MANNI_GENERATED_BY for the session. It appends export MANNI_GENERATED_BY=<model> to the file Claude Code names in CLAUDE_ENV_FILE, using the model id from the session. It does nothing when the variable is already set, so a value you chose wins. Every command the agent runs afterwards records which model wrote the uncommitted lines, with no line about it in your agent instructions. See Record an agent’s edit.

The plugin registers one MCP server, graph. Claude Code starts it with every session and lists it in /mcp as plugin:manni:graph. The first session asks you to approve it, as Claude Code does for a project’s .mcp.json.

In a repository with collections: in manni.config.yaml, the server runs manni graph serve --publish. The agent gets four tools: lookup, retrieve, traverse and impact. Each passage they return carries its pins and dates, so the agent knows what is verified before it quotes it. Serve the docs to an agent covers what each tool answers.

The server publishes the graph at start when the published copy is missing or older than the checkout. It answers Claude Code first and publishes behind that, so a large site never misses the start-up deadline. A call made during the publish waits for it.

Before each later call, the server checks whether any file git status lists has changed. If one has, it publishes again and then answers. The agent’s own edits reach lookup without a restart.

manni: publishing the graph before serving it: <plugin data>/graph/3f9c0a1e7b2d4c58/chunks.json does not exist.
manni: published the graph in 12.1s.
manni: serving lookup, retrieve, traverse and impact over stdio from <plugin data>/graph/3f9c0a1e7b2d4c58/chunks.json.

Those lines go to Claude Code’s debug log, not to the agent.

The graph goes to the plugin’s data directory, one folder per project, outside your working tree. It never goes beside graph.out, so a session never writes into the folder a site deploys. The server runs manni graph serve --publish -i <plugin data>/graph/<hash>, and the build’s parse cache under .manni/graph/analyze-cache/ ignores itself. So git status shows nothing new.

The served graph carries author-facing content, as a default build does. That covers the strategy records and each passage’s pins and eval verdicts. A local manni site preview serves the graph beside graph.out, so run manni graph publish --collection site before it.

A repository with no collections: has nothing to build. The server then offers no tools and adds nothing to the agent’s context.

When the agent reads a page in one of your collections, the plugin hands it the page’s metadata beside the file. That is every field the page stores, in its frontmatter and in every sidecar file its collection declares. A field kept in {page}.meta.yaml is one the read alone never shows.

Metadata for docs/limits.md, from the page and its sidecars:
{"title":"Limits","description":"What the service accepts.","owner":["@docs-team"],"eval-suite":"reference"}

The values come from manni meta get --all-fields --no-derived, so you can ask for the same answer yourself.

A second read of the page adds nothing. When a field changes, in the page or a sidecar, the next read brings the new values and says they replace the old.

Metadata for docs/limits.md changed since it was last given here. This replaces it:
{"title":"Limits","description":"What the service accepts.","owner":["@api-team"],"eval-suite":"reference"}

Each subagent gets its own, since it has its own context. After Claude Code compacts the conversation, the next read of each page brings its metadata again. Encrypted values arrive as their stored ciphertext.

A file outside every collection gets nothing. Neither does a file manni cannot read, and reading source code starts no manni process.

A page can name strategy records in audiences:, personas: and journeys:, and a persona names its audience in audience:. When one of those values names a record that exists, the reply names the record too. Each record gets one line with its type, its id and its file. The record’s content stays in its file, one read away.

Metadata for docs/limits.md, from the page and its sidecars:
{"title":"Limits","description":"What the service accepts.","personas":["devin"],"owner":["@docs-team"],"eval-suite":"reference"}
The page names these records. Read a record's file for its details, or run manni strategy get <id>, which also lists the pages that name it:
persona devin docs/content-strategy/personas/devin.md

On such a page the key fields come first. Those are the fields of manni:core and manni:lifecycle, plus the four record fields. The reply also stays under Claude Code’s limit of 10,000 characters. When the page stores more, the reply leaves out whole fields, largest first. It never leaves out a key field or a reference, and it never cuts a value. Its last line then names each field it left out, and the command that prints them.

This leaves out citations and provenance to stay under Claude Code's limit of 10,000 characters. manni meta get citations,provenance --no-derived docs/limits.md prints them.

A page that names no record gets every field in page order, as in the first example. Past 10,000 characters, Claude Code saves that reply to a file and gives the model a preview.

When the agent uses Edit, Write, MultiEdit or NotebookEdit, the plugin runs manni check on that one file. The checks are the per-file checks of each domain in play: meta, cite, lint and docevals.

A file outside every collection passes without a word, so an edit to CLAUDE.md or to source code runs nothing.

When the check finds an error, the hook exits 2. Claude Code treats that as a block and shows the agent what was written to stderr.

manni found errors in docs/limits.md. Fix them before you continue. The manni:fix skill says how to repair each finding.
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

The reply carries only the reports of the checks that failed. The agent fixes the file before it moves on, and the manni:fix skill says how. Warnings and notices never block.

If a check cannot run, the hook does not block, because exit 2 would blame the agent for manni’s own fault. The agent gets context instead. It names the check and the file, says the cause is in manni’s setup, and carries the whole error. It also tells the agent to report the error to you and leave manni.config.yaml as it is.

manni lint check could not check docs/limits.md. The cause is in manni's setup, not in the file. Tell the user what it says, and leave manni.config.yaml as it is.
Unknown key "bogus" under lint: in manni.config.yaml. Supported keys: allowEmpty, overrides, structure, template, templates, types.

When the agent tries to finish, the plugin runs manni check over what it changed. That is the per-file checks on every file the git working tree changed against HEAD, whether modified, added, renamed or untracked. It also checks every citation, since a source edit can drift one on a page the agent never opened. The glossary reads only collection documents, so it is checked when the agent changed or removed one, and skipped when it changed only code. The graph is never checked here. It is the slowest whole-set check, and its findings span pages, so CI checks it with a bare manni check.

When tracevals.runtime exists and you turned tracevals on, it also judges the turn that just ended. The fast in-loop model holds it to the rules your repository declares for agents. Otherwise the Stop runs the file checks alone and never reads the transcript.

With a clean working tree, no file check runs.

If errors remain, the stop is blocked once. The agent gets the report and one repair pass.

{"decision":"block","reason":"manni found errors in the files you changed. Fix them, then finish. The manni:fix skill says how to repair each finding.\n\nmeta validate\n✗ docs/limits.md\n …"}

If errors remain after that pass, the agent stops, and you see a message.

{"systemMessage":"manni still reports errors after one repair pass. Run manni check to see them."}

One pass bounds the loop. A failure that predates the session cannot hold an agent forever.

A broken rule from the rules files arrives in the same reply. The report names the rule, the file it came from, and what the turn did. The agent fixes the work, or says why the rule does not apply, and then finishes. Catch a broken rule before the agent hands back covers what counts as a rule and how the model is chosen.

For a user who turned tracevals on, a subagent that finishes runs the same check over its own transcript alone. Parallel subagents each get a judgement of their own run, and an agent definition’s rules apply to it. The repair pass works as it does for the main session. With tracevals off, no hook runs when a subagent stops.

For a user who turned tracevals on, a last hook runs manni tracevals release. It hands back the session’s hold on the local model, so the model unloads once no session needs it. With tracevals off, nothing runs at session end.

The plugin’s tracevals hooks are off until you turn them on. Off, the plugin records no manifest, fetches no model and judges no turn. A committed tracevals.runtime section says turns can be judged in this repository. Whether yours are is your own choice.

To turn the option on, open /plugin, then Installed, manni and Configure options. Check Judge each turn with tracevals. In a session, /plugin configure manni@manni opens the same option. From a shell, set it as you install the plugin.

Terminal window
claude plugin install manni@manni --config tracevals=true

Claude Code then says to restart, and your next session runs the hooks. It keeps the answer in your user settings.

~/.claude/settings.json
{ "pluginConfigs": { "manni@manni": { "options": { "tracevals": true } } } }

Claude Code ignores this option in project and local settings. Setting it in a committed settings file therefore never turns it on for anyone else.

With the option on, the plugin sets MANNI_TRACEVALS_HOOKS=1 for every manni it starts. If you run manni from hooks of your own, without the plugin, set that variable yourself. The env block of your own ~/.claude/settings.json is one place for it. Only 1 turns the hooks on. Any other value leaves them off, and manni warns once.

manni: MANNI_TRACEVALS_HOOKS is "true"; only 1 turns the tracevals hooks on.

The session briefing reads the plugin’s option. A manni status in your own terminal reads that shell’s environment instead. Unless you export the variable there, it shows tracevals as turned off here, with the next step under it.

tracevals not checked runtime evals are set up, and turned off here
Turn on the manni plugin's tracevals option, or set MANNI_TRACEVALS_HOOKS=1.

MANNI_TRACEVALS_HOOKS=1 manni status shows the row your sessions see.

The plugin ships four skills. Claude uses them when the task matches, or you can call one by name.

SkillWhat it does
manni:setupInstalls @hawkeyexl/manni as a dev dependency and writes manni.config.yaml with a collections: entry for the docs it finds. It asks which domains to set up, then ends with manni status and manni check.
manni:checkRuns manni check and walks the findings, one check at a time.
manni:fixMaps each finding to its repair. A meta finding is a page edit, or meta derive for a managed field. A cite finding is cite update for moved lines.
manni:evalsAuthors evals with docevals generate, fill and promote for pages, and tracevals fill for agent artifacts. It shows the diff for review.

manni:fix carries two rules. The agent never loosens a schema, a config key or an eval to make a check pass. It also never runs cite update --accept on a changed citation before it reads the changed source lines.

Disable the plugin and every hook stops.

Terminal window
claude plugin disable manni@manni

To keep the hooks but stop the graph server, turn plugin:manni:graph off in /mcp. Claude Code remembers that choice for the project.

To stop the tracevals hooks for yourself alone, turn the plugin’s tracevals option off and restart Claude Code. The rest of the plugin keeps running, and so does tracevals for anyone else who turned it on.

To keep the plugin but stop one domain from blocking, remove that domain’s section from manni.config.yaml. A domain counts as set up only when its config or its content says so, so the next manni status shows it as not set up.