feat: docs/ carries the operator's how-to guides, a configuration reference and the architecture and telemetry explanations carved out of README.md, INSTALL.md, INSTALL-MCP.md and EVALS.md; the last two retired (#190)
CI / verify (push) Successful in 6m17s
CI / pwsh (push) Successful in 1m59s
Release / release (push) Successful in 35s

Files changed:
- .gitea/workflows/ci.yml
- .gitignore
- .vibe/hooks.toml
- AGENTS.md
- CHANGES.md
- EVALS.md
- INSTALL-MCP.md
- INSTALL.md
- README.md
- VERSION
- docs/dev/development.md
- docs/explanation/README.md
- docs/explanation/architecture.md
- docs/explanation/language-boundaries.md
- docs/explanation/telemetry-and-evaluation.md
- docs/how-to/README.md
- docs/how-to/browse-with-obsidian.md
- docs/how-to/capture-repository-docs.md
- docs/how-to/export-guidelines.md
- docs/how-to/ingest-a-source.md
- docs/how-to/maintain-the-wiki.md
- docs/how-to/query-the-wiki.md
- docs/how-to/run-the-weekly-review.md
- docs/how-to/set-up-the-mcp-server.md
- docs/how-to/trace-and-score-sessions.md
- docs/how-to/troubleshoot.md
- docs/how-to/update-an-instance.md
- docs/reference/README.md
- docs/reference/commands/distribution-and-versioning.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/commands/telemetry.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/configuration.md
- instructions/dev/dev-setup.md
- instructions/dev/doc-pull-through.md
- instructions/dev/stack-build/SKILL.md
- instructions/dev/testing-conventions.md
- instructions/dev/version-parts.md
- instructions/setup-instance.md
- instructions/upgrade-instance.md
- reports/CONTRACT.md
- tools/.coveragerc
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/commands/dist_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/eval_cmd.py
- tools/chemenu/commands/git_publish.py
- tools/chemenu/commands/upload_cmd.py
- tools/chemenu/commands/version_cmd.py
- tools/chemenu/evals/__init__.py
- tools/chemenu/evals/scorecard.py
- tools/chemenu/evals/trajectory.py
- tools/chemenu/telemetry/policy.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_git_publish.py
- tools/chemenu/tests/test_instructions_shell.py
- tools/chemenu/tests/test_pipeline_l0.py
- tools/chemenu/toc.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Opus 5.5 committed 2026-10-07 05:13:56 +02:00
1 parent cff7df4ffa
commit f9056c04f7
61 files changed
+2169 -1772

No files matched your search

+21 -3
View File
@@ -6,6 +6,24 @@ handed to the agent. A how-to guide never retells an agent's procedure: the step
live in `instructions/`, and a guide here says what to prepare, what to ask for and where the
agent stops.
Nothing is here yet. Until the guides are written, the task-shaped parts of
[README.md](../../README.md), [INSTALL.md](../../INSTALL.md),
[INSTALL-MCP.md](../../INSTALL-MCP.md) and [EVALS.md](../../EVALS.md) still carry this material.
Working with the wiki:
- [Ingest a source](ingest-a-source.md) - a file, a folder, a web page, or a document submitted
through the MCP server
- [Capture documentation from a git repository](capture-repository-docs.md) - and keep it current
- [Export guidelines to captured repositories](export-guidelines.md)
- [Query the wiki](query-the-wiki.md) - asking, and searching yourself
- [Maintain the wiki](maintain-the-wiki.md) - lint, provenance, and the gates on the way to a publish
- [Run the weekly review](run-the-weekly-review.md) - commitments in a task tracker against the
project pages
- [Browse the wiki in Obsidian](browse-with-obsidian.md) - and how pages are named
Running the instance:
- [Update an instance](update-an-instance.md) - to a newer release of the stack
- [Troubleshoot an instance](troubleshoot.md)
- [Set up the MCP read server](set-up-the-mcp-server.md) - for a consumer that is not a terminal
on this machine
- [Trace and score agent sessions](trace-and-score-sessions.md)
Installing a new instance is [INSTALL.md](../../INSTALL.md) at the repository root.
+54
View File
@@ -0,0 +1,54 @@
# Browse the wiki in Obsidian
The wiki is a folder of Markdown files joined by `[[wikilinks]]`, so any Markdown editor can read
it. Obsidian adds a graph of the links between pages, which makes it a good way to look around.
## What you prepare
Open the instance's root directory as an Obsidian vault. Obsidian keeps its own settings in
`.obsidian/`. That folder is personal state: it is never shipped with a release, and it has no
effect on the wiki.
What you get:
- the graph view of how pages connect;
- navigation by wikilink, the way the agent writes them;
- plugins such as Dataview, which can query page frontmatter, or Marp for slides.
## Reading, not writing
Browse and read freely. Change pages through the agent, though, not by editing them in Obsidian. A
page carries generated frontmatter, cross-references and footnote citations that `tools/wikitool`
keeps consistent across the whole wiki. A hand edit can quietly break them. Renaming or moving a
file in Obsidian is the clearest case: a title is the page's only identifier, so it also appears in
every wikilink and citation that points at the page. A rename goes through
`tools/wikitool rename`, which updates every reference. The full procedure is
[instructions/page-lifecycle.md](../../instructions/page-lifecycle.md).
Generated files - `kb/index.md`, `kb/log.md`, `kb/provenance.md` and every `INDEX.md` - are
rebuilt by the tool and never edited by hand.
## How pages are named
Knowing the naming rules makes the vault easier to find your way in:
- **Titles are human-readable names with spaces:** `Hybrid Search.md`, not kebab-case. A wikilink
matches the file name exactly: `[[Hybrid Search]]`.
- **Entities are singular:** `HA Integration.md`, not `HA Integrations.md`.
- **A title follows the subject's own established name, not the wiki's language.** `Act Runner`
and `GitOps Ownership Model` keep their names in a German wiki too. Translating a title is a
rename, not an edit.
- **A title works on every operating system.** It contains none of `< > : " / \ | ? *`, is not a
reserved name such as `CON` or `Index`, does not end in a dot, and does not differ from another
page's title by case alone. A file's whole path below the instance root stays at 160 characters
or fewer, so a Windows checkout works without long paths. `wikitool new` and `wikitool rename`
refuse titles that break these rules, and `wikitool lint` reports existing ones.
[kb/CONTRACT.md](../../kb/CONTRACT.md) has the full rule.
## Letting the structure grow
- Start with a few broad categories and refine as the material arrives. Don't over-organise early.
- Use tags for concerns that cut across collections.
- Review what the agent changed from time to time - `kb/log.md` lists every operation - and tell it
when something is off. It keeps to the standards in `AGENTS.md` and the contracts, but you are the
one who notices when a page misses the point.
+59
View File
@@ -0,0 +1,59 @@
# Capture documentation from a git repository
Documentation that lives in a git repository is captured, not copied by hand. A capture records the
repository, the ref rule and the commit it came from. Later, the wiki can tell which captured
material has fallen behind its repository, and take in the new edition as a whole.
## What you prepare
Access to the repository from this machine. Git uses your own keys and credential helpers, and
nothing is stored in the instance. Then decide three things:
- the repository URL;
- the ref to follow, usually a branch such as `main`;
- which files count, as a glob, for example `docs/**/*.md`.
## Taking a repository in for the first time
You can run the capture yourself, or name the three values to the agent and let it run the
command:
```bash
tools/wikitool raw capture <repo-url> --ref main --path 'docs/**/*.md' --name <bundle>
```
This writes the matching files, byte for byte, into `incoming/<bundle>/`, together with a
`_capture.json` manifest naming the repository, the ref rule and the commit. From there it is an
ordinary source: `Ingest incoming/<bundle>` ([Ingest a source](ingest-a-source.md)), with the same
questions and the same gate.
## Keeping it current
```bash
tools/wikitool raw status
```
This lists every captured bundle that has fallen behind its repository, file by file, marking each
file as added, modified or deleted. It only reads.
To bring them in, say:
```text
Update the captured repositories
```
The agent checks every captured bundle. It takes one changed bundle per run into the wiki, replacing
the old edition as a whole, and tells you how many are still behind. Run it again for the next one.
`raw capture --update` and `raw accept --replaces-bundle` are the commands underneath. What each one
does is in the [command reference](../reference/commands/raw-material-and-uploads.md).
## Where the agent stops
The same places as any ingest: the `fidelity`/`authority` questions, the discussion of what
matters, and the Mass-Update Gate when it publishes. When one repository cannot be reached, the
agent reports the error and carries on with the others. A failure in one never stops the run.
## Related
- [Export guidelines](export-guidelines.md) - sending the wiki's guidelines back into the same
repositories.
+54
View File
@@ -0,0 +1,54 @@
# Export guidelines to captured repositories
The repositories the wiki captures documentation from can get something back: the wiki's
guidelines, as one generated `GUIDELINES.md` in each repository's root, which that repository's own
agents then read.
## What you prepare
- **Decide which pages count as guidelines.** That decision is yours, and it is written down in
`kb/CONVENTIONS.md` § Guidelines for other repositories. Usually it is a tag. To see the file
exactly as it would be written, without touching any repository:
```bash
tools/wikitool export guidelines --tag guideline
```
- **Opt each repository in, by hand, once.** A repository takes part only if it carries a
`GUIDELINES.md` whose first line is exactly:
```markdown
<!-- wikitool:export kind=guidelines -->
```
Commit that one line there. Then point the repository's own `AGENTS.md` at the file (for Claude
Code: `@GUIDELINES.md`). A `GUIDELINES.md` without that first line is hand-written, and the export
never touches it.
The repository has to be one the wiki has captured
([Capture documentation from a git repository](capture-repository-docs.md)). That is how the export
knows where the repository lives. Its capture also has to follow a branch: a repository captured by
a tag pattern is skipped, because a tag has no branch to commit onto.
## What you say
```text
Roll out the guidelines to the captured repositories
```
## Where the agent stops
The agent runs `export guidelines --push`, which stops before pushing anything. This is the
**Guideline Push Gate** (exit 42). For every repository, it prints the status, the diff it would
write and a `--confirm <token>` line. The agent puts every diff in front of you. Only your approval
pushes, and then only one commit per repository, which changes only `GUIDELINES.md` and goes
straight onto its branch.
The token is a digest of each target's branch tip and of the generated file. If a branch has moved
or a guideline was edited since you looked, the gate asks again rather than pushing something you
did not see.
## Related
- [Guideline export commands](../reference/commands/guideline-export.md)
- [Why the gates are code](../explanation/why-gates-are-code.md)
+82
View File
@@ -0,0 +1,82 @@
# Ingest a source
Turn a file, a folder or a web page into wiki pages: a source page that summarises it, and the
entity and concept pages it touches, cross-referenced and published. The agent does the work, using
the `wiki-ingest` skill ([skill catalogue](../reference/skills.md)). This page covers what you hand
it and where it waits for you.
## What you prepare
Put the material into `incoming/` at the root of the instance. You don't have to classify it or
pick a directory:
- **One file** goes in directly: `incoming/my-article.md`.
- **Several files that belong together** go into one folder, `incoming/<name>/`. The folder is one
source. It is accepted whole, and its structure is kept.
- **A web page** needs no download of your own. Name the URL to the agent, and
`tools/wikitool raw fetch` puts two files into `incoming/`: the HTML exactly as received, and a
text derived from it with a header recording where and when it was fetched.
- **A page behind a paywall or a login** is saved from your browser into `incoming/`, HTML only. The
agent derives the same text from that file with `raw fetch --html`.
Where a file ends up in `raw/` is computed by `tools/wikitool raw accept`: a `YYYY/MM` shard for the
day it was accepted. So is whether several files of one source get bundled. You never choose either.
`incoming/` itself is gitignored, so whatever waits there is never published by accident.
## What you say
```text
Ingest incoming/my-article.md
Ingest https://example.org/post
Ingest
```
A bare `Ingest` takes the oldest entry waiting in `incoming/`. `tools/wikitool raw pending` lists
what is waiting and names the entry the next bare `Ingest` will take.
## Where the agent asks you
- **Before promoting the file into `raw/`**, the agent asks how faithful the capture is
(`fidelity`) and what the material may claim about its subject (`authority`). Both are recorded
once and never guessed: you know the answers now, and nobody can recover them later.
[raw/CONTRACT.md](../../raw/CONTRACT.md) defines the values.
- **Before writing pages**, the agent presents the key takeaways and asks which points matter most
and what to emphasise. The pages follow your answer.
- **When it publishes**, a source that touches ten or more files stops at the Mass-Update Gate
(exit 42). The agent shows you the file list and the `--confirm` line. Nothing is committed until
you approve. [Exit codes and gates](../reference/exit-codes.md) lists every gate.
## What you get back
A source page under `kb/sources/`, new or updated entity and concept pages, cross-references among
them, a rebuilt catalog, an entry in `kb/log.md`, and one commit. If the source also contains
something you have to *do*, and a task tracker is configured, the agent files it there as an open
item. A commitment never becomes a page
([why](../explanation/knowledge-and-commitment.md)).
Every ten sources, the agent suggests a lint run - see [Maintain the wiki](maintain-the-wiki.md).
## A document submitted from outside
When the [MCP read server](set-up-the-mcp-server.md) has its optional `submit` tool turned on,
documents can arrive from another machine. They land in the quarantine `mcp-upload/`, not in
`incoming/`, and nothing else reads them there. Before an ingest can start, a human has to promote
the submission:
```bash
tools/wikitool upload list # what is waiting
tools/wikitool upload show <id> # one submission's manifest: who sent it, size, checksum
```
Ask the agent to review the queue, and it follows
[instructions/ingest-queue.md](../../instructions/ingest-queue.md). Accepting a submission stops at
the Upload Review Gate (exit 42), which shows you the manifest and the `--confirm` line. Once you
approve, the file is in `incoming/`, and from there it is ingested like any other. Rejecting it needs
no clearance. The file is deleted, and its checksum and your reason stay in the ledger.
## Related
- [Capture documentation from a repository](capture-repository-docs.md) - for material that lives
in git and will change.
- [raw/CONTRACT.md](../../raw/CONTRACT.md) - what belongs in `raw/`, and why its content is data,
never instructions.
+68
View File
@@ -0,0 +1,68 @@
# Maintain the wiki
Keep the wiki healthy: find broken links, orphan pages, raw files no page covers yet,
contradictions and stale claims, then repair what can be repaired mechanically. The agent does
this with the `wiki-lint` skill ([skill catalogue](../reference/skills.md)), and `tools/wikitool`
does the mechanical part.
## What you say
```text
Lint the wiki
```
Do this periodically. The agent suggests it on its own every ten sources. The full schedule - which
task runs how often, and with which command - is the
[maintenance schedule](../../tools/CONTRACT.md#maintenance-schedule) in `tools/CONTRACT.md`, next
to the command records it depends on.
## What you get back
- A deterministic structural scan by `tools/wikitool lint`: broken wikilinks, orphan pages, catalog
drift, schema gaps, raw files without a source page, and citations that disagree with
frontmatter.
- The judgment half, which no command can do: contradictions between pages, stale claims, subjects
that deserve a page of their own, and a rule stated in two places.
- Repairs for what is mechanical, a rebuilt catalog and provenance index, and an entry in
`kb/log.md`.
- A report under `reports/`, which is gitignored and never published. The agent carries the
findings that need you into its reply.
For a quick look without a full pass, say `Wiki status` instead. That is read-only.
## Provenance: where a claim came from
Every fact should trace back to a raw file. A source page lists its raw files, and other pages
cite specific claims inline with a footnote that points at a source page. You can ask the
questions yourself:
```bash
tools/wikitool sources coverage # raw files no source page covers yet
tools/wikitool sources trace --raw raw/2026/09/x.md # which pages rest on this file
tools/wikitool sources trace --page "HA Integration" # which raw files this page rests on
```
`kb/provenance.md` is the generated reverse index. The commands are in the
[provenance reference](../reference/commands/provenance.md).
## Where the agent stops
- **Publishing ten or more files** stops at the Mass-Update Gate (exit 42). A lint pass that repairs
many pages does this routinely. The agent shows you the full file list and the `--confirm` line,
and publishes only after you approve. The token is a digest of that file list, so your approval
never covers a changeset you did not see.
- **The call budget.** Every `wikitool` call counts against a per-session budget: 60 calls, or 3
identical calls in a row. Past it, calls are refused. The agent stops and summarises where it is.
If you want it to go on, tell it so. Only then may it pass `--override-budget` on the command it
needs. A refusal usually means the task should be split, not pushed through.
- **No reachable remote.** Without `--no-push`, `publish` refuses with exit 1 before committing when
the remote is not configured or cannot be reached. An instance that is meant to stay local passes
`--no-push` on every publish.
Every gate, and which command it belongs to: [exit codes and gates](../reference/exit-codes.md).
Why they are enforced in code: [why-gates-are-code.md](../explanation/why-gates-are-code.md).
## Related
- [Troubleshoot](troubleshoot.md) - when `doctor` or a command reports a problem with the instance
itself rather than with its pages.
+50
View File
@@ -0,0 +1,50 @@
# Query the wiki
Ask the wiki a question and get an answer built from its compiled pages, with the pages and sources
it came from. The agent answers using the `wiki-query` skill
([skill catalogue](../reference/skills.md)), which only reads unless you ask it to keep an answer.
## What you say
Ask the way you would ask a colleague:
```text
What projects use MQTT?
Show me the architecture of HA Integration
Compare gdeploy and plugnburn-edl
What decisions were made about E3DC integration?
```
## What you get back
An answer that names the wiki pages it came from and the sources behind them. Where the wiki has no
source for a claim, the agent says so instead of filling the gap. An answer it cannot back is never
written into the wiki either.
If an answer is worth keeping - a comparison you will want again, a synthesis across several pages -
the agent proposes filing it back as a page. It names the pages it would create before it creates
any, and nothing is written unless you agree.
## Searching yourself
The agent searches with the same command you can run:
```bash
tools/wikitool search "<text>"
tools/wikitool search --field entity_type=system
tools/wikitool search --field '!sources' # pages without a sources: list
```
`search` covers all of `kb/`, by text, by frontmatter or both. Each hit carries the page's path and
full title, so you can open it directly. It only reads, and it does not count against the
session's call budget, because looking before acting is the habit worth encouraging. The predicate forms are in the
[command reference](../reference/commands/finding-and-checking.md).
For browsing rather than searching, `kb/index.md` is the catalog map, and each collection carries
its own `INDEX.md`. All of these are generated. Read them, but never edit them. To browse the pages as
a graph, see [Browse the wiki in Obsidian](browse-with-obsidian.md).
## Asking from another machine
A consumer that is not a terminal on this machine can ask the same questions through the MCP read
server and get the same answers: [Set up the MCP read server](set-up-the-mcp-server.md).
+64
View File
@@ -0,0 +1,64 @@
# Run the weekly review
Go through your committed initiatives once a week and decide what each one needs: a next action, a
pause, a close. A machine prepares the list, so you don't have to reconstruct it from memory. The
agent runs the `gtd-weekly-review` skill ([skill catalogue](../reference/skills.md)).
## Two places, on purpose
Knowledge and commitments change at different speeds, so they live in different places:
- **A page under `kb/gtd/`** is one initiative's durable memory: its goal, who is involved, where it
stands, and why it is worth doing. It never summarises the task list.
- **The open items** live in a task tracker, which owns them.
Nothing syncs between the two. `tools/wikitool review` joins them when it reads, matching on the
project name, and stores nothing - not even a report file. Why the split runs this way rather than
syncing: [knowledge-and-commitment.md](../explanation/knowledge-and-commitment.md).
## What you prepare
A task tracker, configured for this checkout in `.wikitool-tasks.json`. Super Productivity and
CalDAV (for example Nextcloud Tasks) are supported. The file's shape, and the difference between a
tracker the wiki can only read and one it can write to, is in the
[configuration reference](../reference/configuration.md#task-tracker). Without a tracker, the
review has nothing to join against. That is a valid state, though, and the pages work without one.
## What you say
```text
Run the weekly review
```
## What you get back
The agent runs `tools/wikitool review` and walks you through its findings, one at a time:
- initiatives with no next action;
- waiting-fors past their follow-up date;
- tracker projects that have no page;
- active pages with no open item;
- someday items that have gone stale.
For each finding, the agent asks what is actually true, because "this looks stalled" can mean a
next action is missing, the initiative was paused on purpose, or it is finished. Then it proposes the
exact change: a new item in the tracker, an item marked done, or a page's state moved to `dormant`,
`completed` or `abandoned`. Nothing is written to the tracker until you confirm that exact call.
What was decided goes onto the page, and the page is published.
A quiet week is a valid result. If one of the checks could not run because the tracker did not
answer, the agent says which check was skipped. An empty result under a skipped check means nothing.
## Starting something new
Ask the agent for a new project and name it. It runs `tools/wikitool new project`, which gives the
initiative its page under `kb/gtd/` and, where a tracker is configured, a tracker project under the
same name. A CalDAV tracker gets that project created automatically. On Super Productivity the
command stops with exit 42 and asks you to create the project in the app yourself. Tell the agent
when you are done, and it continues with `--resume`. A tracker the wiki can only read refuses the
command outright.
A single open item with no page of its own - a commitment that came with a source, say - is filed
with `tools/wikitool task new`. `task list` reads a project's open items back, and `task close --id`
marks one done. Nothing deletes an item. The commands are in the
[command reference](../reference/commands/pages.md).
+263
View File
@@ -0,0 +1,263 @@
# Set up the MCP read server
Serve an instance's wiki read-only over MCP, so that a consumer that is not a terminal on this
machine - another agent, a chat client, a service - can ask it the same questions and get the same
answers. This page covers what you set up around the server: the client, the authentication in
front of it, the optional `submit` path, and how to check that it works. Installing, starting and
syncing the server itself is
[instructions/mcp-read-server.md](../../instructions/mcp-read-server.md), which an agent follows
and which is readable for you as well.
<!-- wikitool:toc -->
## Contents
- [What the server is, and what it is not](#what-the-server-is-and-what-it-is-not)
- [What you prepare](#what-you-prepare)
- [Wiring a client to it](#wiring-a-client-to-it)
- [Running it as a service](#running-it-as-a-service)
- [Authentication in front of it](#authentication-in-front-of-it)
- [Keeping the served tree current](#keeping-the-served-tree-current)
- [Optional: the submit path](#optional-the-submit-path)
- [Checking that it works](#checking-that-it-works)
- [Troubleshooting](#troubleshooting)
- [What is deliberately not here](#what-is-deliberately-not-here)
<!-- /wikitool:toc -->
## What the server is, and what it is not
The server is a second consumer of the same core as the CLI, not a second program. Both call the
same functions, and a golden test in the origin repository holds their output together. What
`tools/wikitool search --json` returns, the MCP tool `search` returns too, together with the commit
the answer was computed from.
It is not a write path into `kb/`. No tool creates, changes or publishes a page - not because a list
is filtered, but because the server imports nothing under `tools/chemenu/commands/`. Those functions
cannot be reached from its process. The one exception is opt-in: a sixth tool, `submit`, which
writes only into a quarantine no other command reads. A human decides about everything that leaves
it ([below](#optional-the-submit-path)).
| Tool | What it answers |
|---|---|
| `search` | Pages in `kb/` by text, by frontmatter (`!sources`, `tags~k8s`), or both |
| `types` | Which page types this wiki knows |
| `describe_type` | A type's full contract: fields, required values, enums |
| `lint` | Structural findings: broken wikilinks, orphans, catalog drift, schema gaps |
| `status` | A snapshot: page count, distribution over collections, finding counts |
| `submit` *(optional)* | Hands a document to the review queue - into a quarantine, never into `kb/` |
## What you prepare
- A working instance ([INSTALL.md](../../INSTALL.md)), including `tools/.venv` and ripgrep.
- For a deployment, a reverse proxy that can authenticate requests before they reach the server
([below](#authentication-in-front-of-it)).
Then ask the agent:
```text
Set up the MCP read server for this instance
```
It installs the server's optional dependency, which is deliberately not in
`tools/requirements.txt`: an instance that only uses the CLI should not have to install pydantic,
starlette, uvicorn and cryptography. It also checks that tracing cannot write into the served tree.
The server refuses to start if it could:
```text
ERROR Telemetry is on and would write into the served checkout (...). Set WIKI_TRACE=0,
or point WIKI_TRACE_DIR outside the corpus.
```
That refusal is expected, not an installation fault. Telemetry is off by default in an instance
installed from a release and on in a clone of the origin repository
([configuration](../reference/configuration.md#telemetry)). The server consults the same policy
`wikitool doctor` does. Setting `WIKI_TRACE=0` explicitly, as every example here does, is correct
either way and makes the check moot.
## Wiring a client to it
Locally, over `stdio`, the client starts the server itself: one process per consumer, no network.
The configuration follows the usual MCP client shape, with absolute paths, because the client
inherits no working directory:
```json
{
"mcpServers": {
"chemenu": {
"command": "/path/to/instance/tools/.venv/bin/python",
"args": ["-m", "chemenu.mcp"],
"cwd": "/path/to/instance/tools",
"env": {
"WIKI_TRACE": "0",
"CHEMENU_ROOT": "/path/to/instance"
}
}
}
}
```
`CHEMENU_ROOT` says **which** wiki is served. Without it, the server takes the checkout its own
package sits in. That is enough for a single instance, but with several corpora it is safer to always
set it. The process speaks MCP over stdin and stdout and prints nothing of its own, which is correct.
## Running it as a service
Deployed, the server speaks `streamable-http`, the only transport a reverse proxy can sit in front
of. The command a service unit or container runs looks like this:
```bash
WIKI_TRACE=0 CHEMENU_ROOT=/srv/chemenu \
tools/.venv/bin/python -m chemenu.mcp \
--transport streamable-http --host 0.0.0.0 --port 8000
```
The endpoint is then `http://<host>:8000/mcp`. `--host 0.0.0.0` is deliberately not the default:
without it the server binds to loopback, which is right locally and wrong in a container, where the
proxy has to reach it. If you set it, make sure something stands in front. `sse` is not offered. It
is the superseded remote transport, and building on it now would only postpone the switch.
## Authentication in front of it
**The server does not authenticate on its own, by design.** A request that is not properly
authenticated should never reach the Python process at all. The authentication is a Traefik
ForwardAuth middleware:
> **<https://gitea.nehmer.net/torben/gitea-mcp-forward-auth>**
In short, it checks `Authorization: Bearer <token>` against SHA-256 hashes of allowed tokens. It
answers `200` for a valid token and `401` for a missing or wrong one, and always leaves
`GET /healthz` open. Plaintext tokens appear neither in the configuration nor in the log, only
hashes and a short fingerprint. It is configured with `AUTH_PROXY_TOKEN_HASHES_DIR` (a directory with
one file per token hash, which suits a Kubernetes secret volume) or `AUTH_PROXY_TOKEN_HASHES` (a
comma-separated list). The README there has the details, reference manifests and a test script.
To create a token hash:
```bash
echo -n "my-token" | sha256sum | awk '{print $1}'
```
**Rate limiting belongs in the same place,** in front of the process and beside the
authentication. It does not belong in the Iteration Budget Gate. That gate stops an *agent session*
from iterating over the wiki's state unnoticed, which is why retrieval is exempt from it. Using it
as a rate limiter would water it down.
## Keeping the served tree current
The server reads the working tree, and a stale checkout answers confidently and wrongly. So **every
answer carries the commit** it was computed from:
```json
{ "commit": "<40-character SHA>", "as_of": "<ISO-8601, UTC>", "count": 3, "...": "..." }
```
A timer beside the server keeps the tree current by polling: a fetch, then a hard reset to the
remote branch. The two commands, and why the hard reset is load-bearing, are in the instruction's
sync step. Polling rather than a webhook, because it needs no inbound endpoint and no signature
check, which is a smaller attack surface than the thing a webhook would optimise. **Never add
`git clean` to that timer.** The reset leaves every gitignored path alone, which is what keeps the
`submit` quarantine and any traces intact. A clean would silently delete a submission nobody has
reviewed yet.
## Optional: the submit path
Without this, `submit` does not exist as a tool - not unused, but not registered. A file
`.wikitool-upload.json` in the served corpus turns it on:
```json
{
"schema": 1,
"identity_header": "X-Forwarded-User",
"max_bytes": 10485760,
"allowed_extensions": [".md", ".txt", ".pdf", ".html", ".csv", ".json", ".png", ".jpg"],
"quota": { "submissions_per_day": 20, "bytes_per_day": 52428800 }
}
```
Every field is required, and none has a built-in default except `identity_header`. A malformed file
stops the server from starting. It never means "no limits": the failure deliberately goes the safe
way. Size and quota are your deployment's decision, not defaults worth copying blindly.
**With `submit` on, the middleware has a second duty.** It has to set the identity header itself, on
every authenticated request, and discard any copy the client sent. The server trusts that header as
the value for `submitter`. A header the client could set itself would be a claim, not an identity.
If `identity_header` names a header the middleware does not set, every submission is refused for
lack of an identity.
Submitted files land in `mcp-upload/<id>/`, which is gitignored and read by no other command. From
there a human promotes or rejects them, in the same checkout as the server rather than in its
process. That review is part of
[ingesting a source](ingest-a-source.md#a-document-submitted-from-outside).
## Checking that it works
The quickest test without a client starts the server over stdio, lists its tools and asks one
question:
```bash
cd tools && WIKI_TRACE=0 .venv/bin/python - <<'EOF'
import asyncio, os
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(
command=".venv/bin/python", args=["-m", "chemenu.mcp"],
env={"WIKI_TRACE": "0", "PATH": os.environ["PATH"]},
)
async with stdio_client(params) as (r, w):
async with ClientSession(r, w) as s:
await s.initialize()
print("Tools:", [t.name for t in (await s.list_tools()).tools])
out = await s.call_tool("status", {})
d = getattr(out, "structuredContent", None) or out.structured_content
print("Pages:", d["pages"], "| Commit:", d["commit"])
asyncio.run(main())
EOF
```
Expected output, roughly:
```text
Tools: ['search', 'types', 'describe_type', 'lint', 'status']
Pages: 176 | Commit: 576df2cdddc96614a7e6641e562022d52112d411
```
Then cross-check against the CLI. Both have to give the same answer:
```bash
tools/wikitool search "<term>" --json
```
## Troubleshooting
**`ERROR Telemetry is on and would write into the served checkout`**: expected behaviour, not an
installation fault. Set `WIKI_TRACE=0`, or point `WIKI_TRACE_DIR` outside the corpus.
**`ModuleNotFoundError: No module named 'mcp'`**: the optional dependency is not installed, or
the client starts a different Python than the instance's. Give the client the absolute path to
`tools/.venv/bin/python`.
**`"commit": null` in every answer**: the served tree has uncommitted changes. Either the sync is not
running, or something writes into the corpus that has no business there. The five read tools never
write. `submit`, when it is on, writes only to `mcp-upload/`, which is gitignored and so is never a
cause of `"commit": null` either.
**`commit` names an old revision**: the sync timer is not running.
**The server answers differently from `wikitool`**: that is a defect, not a configuration
difference, since both go through the same functions. First check that both point at the same root.
`CHEMENU_ROOT` is easy to set for one and not the other.
**Not reachable from outside**: without `--host 0.0.0.0` the server binds to loopback. If it is
reachable but every call gets `401`, the middleware is working correctly and the token is wrong.
## What is deliberately not here
Deployment specifics - the cluster, ingress hosts, the secret store, the GitOps source - are
private infrastructure, and this repository is public.
There is no container image for the server yet. Until there is, the path above - the venv,
`python -m chemenu.mcp`, a proxy in front - is the complete one. With `submit` on, `mcp-upload/`
has to survive restarts the same way the rest of the checkout does. Otherwise a submitted file that
nobody has reviewed yet loses its quarantine.
+176
View File
@@ -0,0 +1,176 @@
# Trace and score agent sessions
Record what an agent did in a session, read the record back, and score it. A score covers the state
the session left the wiki in and how it got there: whether it repeated a refused call, or passed a
gate's flag without that gate ever refusing. Why this exists, what the trace contains, and what a
score means are covered in [telemetry-and-evaluation.md](../explanation/telemetry-and-evaluation.md).
<!-- wikitool:toc -->
## Contents
- [What you prepare](#what-you-prepare)
- [Reading a session back](#reading-a-session-back)
- [What each harness can report](#what-each-harness-can-report)
- [Claude Code](#claude-code)
- [Copilot CLI](#copilot-cli)
- [VS Code Chat](#vs-code-chat)
- [Mistral Vibe](#mistral-vibe)
<!-- /wikitool:toc -->
## What you prepare
**Turn tracing on, if it is off.** It is on in a clone of the origin repository and off in an
instance installed from a release. `tools/wikitool doctor` reports which state this checkout is in,
and why. To turn it on for one checkout, create `.wikitool-telemetry.json` at the root:
```json
{ "enabled": true }
```
The file and its other keys are in the [configuration reference](../reference/configuration.md#telemetry).
Everything stays on this machine: traces land under `reports/telemetry/`, which is gitignored, and
no exporter is configured.
**Scope the session to the task.** Every source of events joins on one session id. Setting it
yourself keeps one task in one trace:
```bash
export WIKITOOL_SESSION_ID="my-task"
```
[instructions/session-setup.md](../../instructions/session-setup.md) has the line for each shell,
and what to do on a harness that starts a fresh shell for every command.
## Reading a session back
```bash
tools/wikitool eval sessions # which sessions have a trace
# what did that session do?
jq -r '[.ts, .source, .event, (.attrs.command // .attrs.tool_name // "")] | @tsv' \
reports/telemetry/my-task/trace.jsonl
# how well did it do it?
tools/wikitool eval score --session my-task
tools/wikitool eval score --session my-task --save # also write the scorecard under reports/evals/
```
Both `eval` commands only read, and neither counts against the session's call budget: reading back
what a session did is not iterating on the wiki. Their options are in the
[telemetry command reference](../reference/commands/telemetry.md).
To see how a harness hook payload would be recorded, feed one in by hand without writing anything:
```bash
echo '{"session_id":"x","hook_event_name":"post_tool","tool_name":"bash"}' \
| tools/trace_ingest.py --source mistral-vibe --dry-run
```
## What each harness can report
`wikitool` records its own calls on every harness. What the agent did *between* those calls -
file reads, searches, shell commands, prompts - comes from the harness's hooks, and harnesses differ
a lot in what they offer. Verified against vendor documentation on 2026-08-23:
| | Claude Code | Copilot CLI | VS Code Chat | Mistral Vibe |
|---|---|---|---|---|
| Hook events | ~30 | 14 | none | 3 (`pre_tool`, `post_tool`, `post_agent`) |
| Session start/end hook | yes | yes | - | **no** |
| Prompt submit hook | yes | yes | - | **no** |
| Which instructions loaded | yes (`InstructionsLoaded`) | no | no | no |
| Block / rewrite a tool call | yes | yes | - | yes |
| OTel to your own collector | yes | via MDM `telemetry` | no | **no** - `enable_otel` targets Mistral Studio only |
| `TRACEPARENT` to subprocesses | yes | undocumented | - | undocumented |
| Programmatic mode | `-p`, `stream-json` | `-p` | no | `-p`, `--output streaming` |
| Isolated config home | to be confirmed | `COPILOT_HOME` | no | `VIBE_HOME` |
| Local session log | `~/.claude/projects/*.jsonl` (unstable format) | `~/.copilot/session-state/<id>/events.jsonl` | chronicle SQLite | `$VIBE_HOME/logs/` (no format guarantee) |
| Wired up here | **partial** - `.claude/settings.json` (`UserPromptSubmit` + a `permissions.ask` rule on the clearing publish) | **yes** - `.github/hooks/wiki-trace.json` | **yes** - `tools/import_chronicle.py` | **yes** - `.vibe/hooks.toml` |
A rule that needs an event a harness cannot report is shown as *skipped*, not as passed or failed.
### Claude Code
`.claude/settings.json` wires `UserPromptSubmit` to `tools/trace_ingest.py`, through
`tools/trace-hook` like every hook here. That gives the trace its turn boundaries, which the rule
`clearance-ended-the-turn` needs. Without a `prompt.submitted` event it reports "cannot say"
instead of a verdict.
**No `PreToolUse` decision hook is wired, and that is a finding, not an oversight.** Verified against
the live CLI on 2026-08-27: a `PreToolUse` hook that returns
`hookSpecificOutput.permissionDecision: "ask"` does **not** override a matching `permissions.allow`
rule. Permissions are evaluated before the hook's decision, so a hook cannot force a confirmation
prompt on a command that is already allowed. A `permissions.ask` rule *does* win, which is why
`.claude/settings.json` carries one on the `--confirm` form of `publish`
(`Bash(tools/wikitool publish --confirm:*)`). The clearing call prompts, and ordinary publishes below
the threshold do not. It is a prefix match, so it depends on `--confirm` coming first, which is why
`publish` always prints its re-run line in that order. Treat it as a useful second line, not a
guarantee: an agent that reorders the flags gets around it.
### Copilot CLI
[.github/hooks/wiki-trace.json](../../.github/hooks/wiki-trace.json) is committed, so a clone
brings its own tracing. Eleven events go to `tools/trace_ingest.py`. `disableAllHooks` in
`.github/copilot/settings.json` turns them off without deleting anything. `userPromptSubmitted` is
among them, so `clearance-ended-the-turn` can be scored on Copilot CLI with no change.
A `preToolUse` entry that forces a decision on the clearing call would first need Copilot's own
decision-document schema verified against a live CLI. That has not happened, and an adapter that
cannot be verified is not written.
Four details of the hook configuration matter. The first two hold for all three harnesses that have
hooks:
- **The interpreter is the venv's, never the script's shebang.** Each `bash` command is
`./tools/trace-hook ...`, which runs `trace_ingest.py` with `tools/.venv`'s Python in either venv
layout. Each `powershell` command names `tools\.venv\Scripts\python.exe` directly. On Windows in
Git Bash, the shebang's `python3` was the Microsoft Store alias, which made every hook a silent
no-op. Before the preflight has created the venv, `trace-hook` records nothing and exits 0.
- **`./tools/trace-hook` has a PowerShell twin, `tools/trace-hook.ps1`.** PowerShell on Windows
resolves the string to the `.ps1` first. Without the twin, it hands the sh script to a file
association, and Windows asks which app should open it - on every hook event. Copilot CLI also
reads `.claude/settings.json` and runs its single `command` under PowerShell on Windows, so a
`bash` field alone does not keep the string out of PowerShell. The twin follows the sh script's
rules - venv Python, silent without a venv, exit 0 whatever happens - and stays parseable by
Windows PowerShell 5.1, because VS Code starts hooks under 5.1.
- **Every command ends in `|| true`.** `preToolUse` hooks fail closed: a non-zero exit denies the
tool call. Without the guard, a missing interpreter would turn the observer into a blocker that
refuses every tool call in the session. Timeouts fail open, so the 5-second `timeoutSec` is no
hazard.
- **The event name is passed explicitly.** Copilot serves two payload dialects: camelCase event
names give camelCase fields, and PascalCase names give the snake_case shape that VS Code and Claude
use. `--event` means the mapping does not depend on which one a configuration chose.
### VS Code Chat
VS Code Chat has no hooks, so nothing observes a session while it runs. Its chronicle store
(`session-store.db`, the same schema as Copilot CLI's) keeps sessions, turns and touched files, and
[tools/import_chronicle.py](../../tools/import_chronicle.py) rebuilds a trace from it afterwards:
```bash
tools/import_chronicle.py --dry-run # this repository's sessions
tools/import_chronicle.py --session 0f1e2d3c
```
The store starts empty, and `/chronicle reindex` fills it. That command also syncs session data to
your GitHub account, so run it deliberately rather than from a script. A rebuilt trace marks itself
`reconstructed: true`, and its `completeness` names `tool.post` but not `tool.pre`: the store records
that a file was touched, not that a tool was about to run.
### Mistral Vibe
[.vibe/hooks.toml](../../.vibe/hooks.toml) declares the three hooks Vibe has, and
[.vibe/config.toml](../../.vibe/config.toml) keeps the telemetry settings in the repository rather
than in someone's shell profile. Both were checked against the installed CLI's own loader
(`mistral-vibe 2.24.2`) rather than against its documentation. What that turned up:
- **A failing hook cannot block anything.** With `strict = false`, the default, a crash or a timeout
is only a warning. Only a hook that opts into `strict` can deny a tool call - the mirror image of
Copilot.
- **`post_agent` carries no response text,** only the session context. That is why it maps to
`turn.end` rather than `assistant.message`.
- **`enable_telemetry` defaults to `true`.** Turning it off is a real change, and it also gates OTel
export, which needs both flags on. This repository leaves both off: Vibe's `enable_otel` sends
prompts to Mistral Studio.
- **Vibe already reads `.agents/skills/` and `AGENTS.md`.** The directory that
`wikitool instructions sync` publishes is a project-scope skill source for Vibe, so working with
Vibe needs no adaptation, only a trusted folder.
+88
View File
@@ -0,0 +1,88 @@
# Troubleshoot an instance
What a common failure means, and what to do about it. During installation itself, the preflight
names its own stops - what is missing, why, and the command that fixes it - and
[INSTALL.md](../../INSTALL.md) explains them. This page covers an instance that is already set up.
For the MCP read server, see the troubleshooting section of
[Set up the MCP read server](set-up-the-mcp-server.md#troubleshooting).
## `tools/wikitool` ends with `STOP - this checkout is not set up yet` (exit 42)
The preflight has not run in this checkout yet, or not since the last update. Run it:
```bash
tools/preflight.sh
```
In PowerShell 7 on Windows, the call is
`pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1`. The `-ExecutionPolicy Bypass`
applies to that one process only and changes no setting.
[instructions/preflight.md](../../instructions/preflight.md) is the procedure, and its output says
what to install if anything is missing.
## The agent offers no skills (`wiki-ingest`, `wiki-query`, ...)
`.agents/skills/` and `.claude/skills/` are generated and not committed. Run:
```bash
tools/wikitool instructions sync
```
Then restart the agent session. A harness reads its skills only when the session starts.
## `doctor` reports `personalization: FAIL`
`USER.md` or `SOUL.md` is missing, or still carries the sentinel line from its template. A renamed
template does not count as a filled one. Ask the agent to run the personalization step of
[instructions/setup-instance.md](../../instructions/setup-instance.md). For a further clone of an
older instance, that is the only step to catch up on.
## `doctor` reports `environment: WARN`
`ENVIRONMENT.md` exists but still carries the template's sentinel line. Fill it in (the template is
`ENVIRONMENT.md.template`) and remove the line, or delete the file. It is optional, and `absent` is
a valid state.
## `new` stops with "No author configured"
Neither `$WIKI_AUTHOR` nor `git config user.name` is set. Run `git config user.name "<Name>"`, or
export `WIKI_AUTHOR` ([configuration](../reference/configuration.md)).
## `publish` ends with exit 42 (Mass-Update Gate)
Expected when ten or more files are counted, for example on the very first commit of a new instance
or after a lint pass. It is not an error. It asks that a human see the output: the complete file
list and the exact `--confirm <token>` line that publishes once you approve.
[instructions/gates.md](../../instructions/gates.md) has the details.
## `publish` ends with exit 42 (Publish-Remote Gate)
This checkout has a `.wikitool-remotes.json`, and the remote being pushed to is not listed in it.
Also not an error: the output names the push URL it would write to and the URLs that are allowed.
Unlike the Mass-Update Gate, there is **no token and no flag** here. If the target really is right,
you add its URL to the file yourself. An agent that edits the file to get past the refusal is
opening a gate on its own initiative, which it must never do.
## Setup or an update fails, and you want to report it
Ask the agent to follow [instructions/bug-report.md](../../instructions/bug-report.md), or call
`tools/bugreport` yourself. It works the same from Bash, Git Bash and PowerShell. It finds a
Python 3.8 or newer by itself, skipping the Store aliases `python3` points at on Windows. It needs
neither the preflight nor a working `wikitool`, and if it finds no Python, it says why.
The script writes a bundle to `reports/bugreport-<timestamp>/`, with a zip beside it.
- **What is removed:** secrets, and page titles from everything the script itself produces.
`--titles` keeps the titles.
- **What may still be in it:** the session trace is included by default (`--no-trace` leaves it
out). Like the chronology and the transcripts, it can contain page content and titles. The bundle
also contains machine, user and path names.
- **Pseudonymising:** `--pseudonymise` replaces user, host, path, git and remote names with
placeholders that keep their length, spaces, hyphens and path depth. The agent can then add more
names - people, companies, customers, projects - with `--bundle … --candidates …`. That is a
model's judgment, and it leaves a remainder: read the bundle before you share it. The mapping,
the checklist and the candidates file contain the originals, and they sit beside the bundle,
never in it.
Nothing is uploaded anywhere. You choose the channel.
+145
View File
@@ -0,0 +1,145 @@
# Update an instance
Bring an instance installed from a release up to a newer release of the stack: the tools, types,
instructions and contracts. Your pages stay yours. The agent carries out the upgrade by following
[instructions/upgrade-instance.md](../../instructions/upgrade-instance.md), which sets the order of
the steps, what each one decides, and where the session has to restart. This page covers the
decision before it, and what you'll see along the way.
<!-- wikitool:toc -->
## Contents
- [What is running, and is there something newer?](#what-is-running-and-is-there-something-newer)
- [Read the notes first](#read-the-notes-first)
- [What you say](#what-you-say)
- [Where the agent stops](#where-the-agent-stops)
- [Where the release comes from, and whom you trust](#where-the-release-comes-from-and-whom-you-trust)
- [Two first-time cases](#two-first-time-cases)
- [Pitfalls](#pitfalls)
<!-- /wikitool:toc -->
## What is running, and is there something newer?
Every instance carries the version of the **stack**, not of its content. It is in `VERSION`, and
`.wikitool-release.json` beside it records where the release came from and when it was exported.
```bash
tools/wikitool version # what runs here, and where it came from
tools/wikitool version check # is there a newer release?
tools/wikitool version notes # what the newest release changes
```
`version check`, `version notes` and `dist upgrade --latest` are the only commands that go to the
network. All three ask the same release feed: the one named in the stamp, unless
`WIKITOOL_UPDATE_URL` overrides it. A feed that cannot be reached is reported as an error, **never**
as "up to date" and never as "no notes".
## Read the notes first
What a version number promises: two versions are compatible when they agree in the *leftmost
non-zero part*. `0.1.3 → 0.1.4` is a safe update and `0.1.3 → 0.2.0` is not. From `1.0.0` on, the
same rule reads as the familiar "a MAJOR breaks". `version check` says which case you are in
(`state: update` or `state: migration`).
That number only answers whether the new machinery can simply be copied over the old. Whether your
*content* has to be migrated is a second, independent question. So the release notes of a MAJOR
carry two separate lines, and both are worth reading before you start:
- **Breaking Change:** what stops working, and what this instance has to do about it.
- **Migration:** whether and how the pages are rewritten (`none required` when they are not).
`version notes` prints both. On an instance it reads them from the release feed, because an
instance's own `CHANGES.md` is a stub that an update never overwrites. It always asks for the
**newest** release, so while `VERSION` still names the old one, it answers with a version other than
the one installed, and says so on stderr. If the feed cannot be reached, the error names the release
page recorded as `release_url` in `.wikitool-release.json`. `--offline` sends you there directly.
Why the two questions are kept apart: [version-model.md](../explanation/version-model.md).
## What you say
```text
Update this instance to the latest release
```
## Where the agent stops
In the order you'll meet them:
- **The release notes.** The agent shows you both lines before it touches anything. A migration the
instance already owes from an earlier release is finished first.
- **A stack file you changed by hand.** `dist upgrade` lists every file that differs from what the
installed release shipped, instead of overwriting it silently. For each one, you decide: take the
release's version, or keep yours. Keeping yours means the file is reported again at every future
upgrade.
- **The preflight.** A new release may need other tools than the old one. If something is missing,
the preflight stops with exit 42 and says what to install, exactly as during installation.
- **The first publish.** The swapped machinery is published on its own, before any content changes.
That is far more than ten files, so the Mass-Update Gate (exit 42) shows you the list and the
`--confirm` line.
- **The session restart.** The running session still holds the *old* `AGENTS.md`, contracts and
skills, so the agent asks you to restart it before migrating content. The new session picks up
where the old one stopped.
- **An offered migration.** A required migration is simply carried out. An *offered* one changes a
file the instance owns, so taking it or declining it is your decision.
- **The second publish,** of the migrated content, behind the same gate.
If a step fails for a reason that isn't obvious, the agent stops and offers you a bug report rather
than improvising ([Troubleshoot](troubleshoot.md)).
`doctor` warns as long as the content's version (`kb_version` in `.wikitool-kb.json`) is behind
`VERSION` and migrations are still open. An instance older than `.wikitool-kb.json` has no such file
at all. In that case `tools/wikitool migrate baseline <version>` is called once, and nothing is
guessed.
## Where the release comes from, and whom you trust
`tools/wikitool dist upgrade --latest --expect <version>` asks the release feed for its newest
release, downloads the tarball and its `.sha256` into a scratch directory, checks one against the
other and applies the release. The scratch directory is removed on every exit, including
`--dry-run`. `--expect` is the version `version notes` showed you: the feed only knows its *newest*
release, and `--expect` refuses **before** downloading if a newer one has appeared since, rather
than applying something nobody read. Pre-releases (`-beta.N`) need `--pre`.
**What the checksum does, and what it does not do.** The `.sha256` comes from the same feed as the
tarball. It protects against a damaged transfer, not against a compromised feed. Whether a release
is genuine rests on your trust in the host whose feed the instance asks (`update_url` in the stamp).
Nothing forces https. A configured `http://` feed is a deliberate choice. `WIKITOOL_UPDATE_TOKEN`
is sent only to downloads on the same host as the feed.
**Offline, or with a tarball from someone else:** put the tarball and its `.sha256` side by side,
and the agent passes the file instead of `--latest` (`dist upgrade <tarball>`). A tarball has to
unpack to exactly one top-level directory, which is the shape the release workflow builds.
## Two first-time cases
**The first jump to a release that knows `--latest`:** the option does not exist in the instance
yet, because the instruction there belongs to the release being left. Fetch the tarball and the
`.sha256` once from the release page, put them side by side, and run `dist upgrade <tarball>`. From
the next release on, the instance has `--latest` itself.
**The first jump to `4.5.0` or later:** `dist upgrade` itself does not exist in the instance yet,
because it arrived with `4.5.0`. Use the tool from the unpacked *new* tarball and point it at the old
instance:
```bash
tar -xzf chemenu-stack-<version>.tar.gz
CHEMENU_ROOT="$PWD" chemenu-stack-<version>/tools/wikitool \
dist upgrade chemenu-stack-<version>.tar.gz --dry-run
```
`CHEMENU_ROOT` tells the package which corpus to work on ([configuration](../reference/configuration.md)).
Without it, the package would take the unpacked tarball itself for the instance. From the second
upgrade on, the instance carries the command and the instruction itself.
## Pitfalls
- An instance without a local `.wikitool-release.json`, or with one that has no `files` block (from
before `4.5.0`), gives `dist upgrade` nothing to detect a local change against, so it refuses
the swap. There is no repair for that today.
- Given a tarball or directory, `dist upgrade` downloads nothing. Only `--latest` downloads.
- To follow the swap by hand anyway, perhaps with a tool `wikitool` cannot run, use the file list in
the `files` block of `.wikitool-release.json`. The exceptions - `kb/CONVENTIONS.md`, every
`kb/*/COLLECTION.md`, `.wikitool-kb.json` - are in the
[`dist upgrade` record](../reference/commands/distribution-and-versioning.md#dist-upgrade), along
with what the command classifies and refuses.