# tools/ - Compiler Contract `wikitool` is a deterministic CLI for Chemenu, used by agents (and humans) so mechanical wiki operations - frontmatter, index statistics, cross-references, log formatting, git publishing - never have to be re-derived by an LLM. The root [`AGENTS.md`](../AGENTS.md) holds the invariants that say when using these commands is mandatory; this file is the full reference. Changes to `wikitool` itself are tracked in the repo root [`CHANGES.md`](../CHANGES.md), not here. **Look up the command you need; do not read this file end to end.** Every command carries one data record - name, synopsis, properties, exit status, and so on - attached to its function in code (`tools/chemenu/cli_contract.py`). Three views for an agent render from that one source: ```bash tools/wikitool raw accept -h # the full record, for one command tools/wikitool -h | grep '^raw accept' # the index line, for a cross-command question grep -n '^#### `raw accept`' tools/CONTRACT.md # this file's own copy, generated ``` The § Commands section below is `wikitool docs contract`'s output: an index (one line per command, `grep`-stable) followed by each `###` group's commands as `#### ` records, in the same order `wikitool -h` prints. Reading the file end to end is for changing the CLI itself. A fourth view is for a person: `wikitool docs reference` renders the same records into one page per group under [`docs/reference/commands/`](../docs/reference/commands/README.md), leaving out ON FAILURE and NEVER, which address an agent. ## Contents - [Setup (one time)](#setup-one-time) - [Usage](#usage) - [Commands](#commands) - [Pages](#pages) - [Links and citations](#links-and-citations) - [Catalog and log](#catalog-and-log) - [Finding and checking](#finding-and-checking) - [Provenance](#provenance) - [Raw material and uploads](#raw-material-and-uploads) - [Git](#git) - [Exports](#exports) - [Workshop runs and session budget](#workshop-runs-and-session-budget) - [Types, instructions and docs](#types-instructions-and-docs) - [Telemetry](#telemetry) - [Distribution and versioning](#distribution-and-versioning) - [Content migrations](#content-migrations) - [Configuration](#configuration) - [Instance health](#instance-health) - [Design notes](#design-notes) - [Tests](#tests) - [Maintenance schedule](#maintenance-schedule) - [Future considerations (not implemented)](#future-considerations-not-implemented) ## Setup (one time) Full bootstrap for a fresh clone - including publishing the skills, which are not committed - is [`instructions/bootstrap.md`](../instructions/bootstrap.md). The environment alone is the preflight ([`instructions/preflight.md`](../instructions/preflight.md)), from the repo root: ```bash tools/preflight.sh ``` From PowerShell 7 on Windows, the twin: `pwsh -NoProfile -ExecutionPolicy Bypass -File tools/preflight.ps1`. Until it has passed, every `tools/wikitool` call exits 42 and names it. A release also carries both scripts as assets (`preflight.sh`, `preflight.ps1`) for the very first install, before there is a tree: run from a folder with no `tools/prerequisites.txt` beside them, they download and verify the release tarball, unpack it into the folder they lie in (or `--into `), and run the preflight inside the unpacked tree. A part of the stack that will not move into place is retried for about 30 seconds; if it still fails, they take back what they moved and exit 1, leaving the folder as it was. ## Usage Run from the repo root: ```bash tools/wikitool -h ``` `-h` and `--help` are identical and TTY-independent - the same plain, unframed text either way, for a human or an agent. Bare `tools/wikitool -h` prints the index below plus a pointer back to this form. A call that ends through `_util.fail()` (exit 1) prints its `ERROR` line to stdout as before, then its record's ON FAILURE reaction(s) to stderr, in the same ` -> ` form `-h` prints - so the reaction is in front of the caller without a second `-h` call. A record with no exit-1 cause of its own falls back to a bare `see: wikitool -h` pointer. ## Commands ``` new write non-idempotent budget:counted exit:0,1,42 Scaffold a new wiki page of any type. task new write non-idempotent budget:counted exit:0,1,42 Create one open item in the configured task tracker - never a kb/ page. task list read idempotent budget:counted exit:0,1,42 List a project's open items - id, title, and whether each carries the WAITING status. task close write idempotent budget:counted exit:0,1,42 Mark one tracker item done - never delete it. touch write idempotent budget:counted exit:0,1 Bump a page's `modified:` date and optionally rewrite its other frontmatter fields. rename write idempotent budget:counted exit:0,1 Rename a page, or repoint references that name a page that never existed. rm write non-idempotent budget:counted exit:0,1 Delete a page and mechanically de-link it from the rest of the wiki. move write idempotent budget:counted exit:0,1 Move a page (or every misplaced page) to the directory its type-spec computes. xref add write idempotent budget:counted exit:0,1 Declare that A B. xref remove write idempotent budget:counted exit:0,1 Remove a cross-reference: the inverse of `xref add`. xref link-source write idempotent budget:counted exit:0,1 Batch-link a source page to every entity/concept it mentions. links show read idempotent budget:exempt exit:0,1 Show the edges out of and into a page. cite id read idempotent budget:exempt exit:0 Print the deterministic footnote id `cite add` would use for this (title, file) pair. cite add write idempotent budget:counted exit:0,1 Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region. cite sync write idempotent budget:counted exit:0,1 Reconcile each page's footnotes region against its actual `[^id]` references. index rebuild write idempotent budget:counted exit:0,1 Regenerate the catalog from every page's frontmatter. log append write non-idempotent budget:counted exit:0,1 Append a formatted entry to `kb/log.md`. log status read idempotent budget:counted exit:0 Read-only: count `ingest` entries logged since the last `lint` entry. lint write idempotent budget:counted exit:0,1 Run structural lint checks against kb/. search read idempotent budget:exempt exit:0,1 Find pages in `kb/` by text and/or frontmatter. review read idempotent budget:exempt exit:0,1,42 The GTD weekly review. sources coverage read idempotent budget:counted exit:0 List raw files with no source page, broken `raw_files:` references, and legacy directory/URL-only source pages. sources trace read idempotent budget:counted exit:0,1 Trace provenance in either direction: raw file, or page. sources rebuild-index write idempotent budget:counted exit:0,1 Regenerate the `kb/provenance.md` reverse index. raw fetch write non-idempotent budget:counted exit:0,1,42 Capture a web page the user names into `incoming/`: the HTML as received plus a derived text, for `raw accept` to promote. Pages of a known website are fetched in the website's standard form and cleaned of its page furniture; a host the instance routes so is fetched with the user's own session or through an external fetcher. raw login write idempotent budget:counted exit:0,1,42 Let the user sign in to a website once, in a visible browser window, and store the session it leaves as a secret outside the working tree, for `raw fetch` to fetch that website's pages with. Only the user runs it. raw derive write non-idempotent budget:counted exit:0,1 Derive reading text from a PDF, e-mail, Office file or web page in `incoming/`, or unpack a ZIP, and leave the original beside it, for `raw accept` to promote. raw capture write non-idempotent budget:counted exit:0,1 Capture documentation from a git repository into `incoming//`, with a manifest naming the repository, ref rule and commit, for `raw accept` to promote. raw status read idempotent budget:counted exit:0 Report which captured bundles under `raw/` have fallen behind their repository, with each changed file as `A`/`M`/`D`. raw pending read idempotent budget:counted exit:0 List what waits in `incoming/`, oldest first, and name the entry an ingest without an argument takes next. raw accept write non-idempotent budget:counted exit:0,1 Promote one or more files, or one folder, from `incoming/` into `raw/`. upload collect write non-idempotent budget:counted exit:0,1 Pull material from the configured `folder` and `command` intake channels into the quarantine - never past it. upload list read idempotent budget:counted exit:0 List every submission currently waiting in the quarantine (`quarantine/`), whatever channel it came through. upload show read idempotent budget:counted exit:0,1 Print one submission's manifest in full. upload accept write non-idempotent budget:counted exit:0,1,42 **Upload Review Gate:** promote a submission's file from quarantine into `incoming/`. upload reject write non-idempotent budget:counted exit:0,1 Delete a submission's material, keeping only its ledger trail. sync write idempotent budget:counted exit:0,1,42 Fetch `/` and bring the local branch up to date with it. publish write non-idempotent budget:counted exit:0,1,42 Reconcile with `/`, then stage all changes, commit, and push. backup init write idempotent budget:counted exit:0,1 Create the backup target's bare repository and make it `origin` when there is none. backup run write idempotent budget:counted exit:0,1 Mirror this machine's files that are not in git into `//`. export guidelines write idempotent budget:counted exit:0,1,42 Render the guideline pages into one generated `GUIDELINES.md`, or push it into the captured repositories that opted in (**Guideline Push Gate**). export run write idempotent budget:counted exit:0,1 Write the derived, read-only reading copy of `kb/` into the folders the `export` section configures - the selected pages as Markdown, or converted by a program of this checkout. work new write non-idempotent budget:counted exit:0,1 Scaffold `work//` for one workshop run. work close write non-idempotent budget:counted exit:0,1 Delete a finished workshop. budget status read idempotent budget:exempt exit:0 Show the current session's `wikitool` call count and recent command history. budget reset write non-idempotent budget:counted exit:0,1 Clear the current session's (or every session's) iteration budget state. types list read idempotent budget:counted exit:0 List every type-spec under `types/`. types describe read idempotent budget:counted exit:0,1 Print one type's full contract. instructions sync write idempotent budget:counted exit:0,1 Publish every `instructions//SKILL.md` into the harness skill directories. instructions verify read idempotent budget:counted exit:0,1 Check the instruction layer. instructions list read idempotent budget:counted exit:0 List the flat instructions with their descriptions. docs verify read idempotent budget:counted exit:0,1 Check the docs that mirror the code. docs toc write idempotent budget:counted exit:0 Create, refresh or remove the generated table-of-contents region. docs prerequisites write idempotent budget:counted exit:0,1 Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`. docs contract write idempotent budget:counted exit:0,1 Regenerate `tools/CONTRACT.md`'s `` region. docs reference write idempotent budget:counted exit:0,1 Regenerate the human reference under `docs/reference/`. eval sessions read idempotent budget:exempt exit:0 List the sessions that have a trace under `reports/telemetry/`. eval score read idempotent budget:exempt exit:0,1 Score one traced session. dist export write idempotent budget:counted exit:0,1 Write a contentless, distributable copy of this repo's machinery. dist adopt write idempotent budget:counted exit:0,1 Take shipped templates as this instance's own: copy each to its unsuffixed name. dist upgrade write non-idempotent budget:counted exit:0,1 Apply a stack update `dist export` produced - the write half of `version check`. version show read idempotent budget:exempt exit:0,1 Print this instance's stack version and where it came from. version check read idempotent budget:exempt exit:0,1 Ask the origin's release feed whether a newer stack exists. version notes read idempotent budget:exempt exit:0,1 Print one version's release notes. version bump write non-idempotent budget:counted exit:0,1 Raise or continue the one running candidate between two releases. version regrade write non-idempotent budget:exempt_without_args exit:0,1 List the running candidate's bump titles with their impact grade, or change one or more of them. version release write non-idempotent budget:counted exit:0,1 Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHANGES.md` entry. migrate list read idempotent budget:exempt exit:0 List every migration document under `instructions/migrations/`. migrate status read idempotent budget:exempt exit:0,1 Show the migrations this instance still owes, in the order they must run. migrate verify read idempotent budget:exempt exit:0,1 Compare `kb/` against a git revision on the invariants a content migration must not change. migrate done write non-idempotent budget:counted exit:0,1 Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json`. migrate baseline write idempotent budget:counted exit:0,1 Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. config set write idempotent budget:counted exit:0,1,42 Set one value of the tiered settings, in the file its tier names. config migrate write idempotent budget:counted exit:0,1 Move the settings out of the per-feature files of earlier versions into the tiered settings. doctor read idempotent budget:exempt exit:0,1 Check that this instance is correctly configured. ``` ### Pages #### `new` Scaffold a new wiki page of any type. **SYNOPSIS** - `wikitool new --name "" [--type ] [--set field=value ...]` - Any type; its type-spec decides fields, directory, title prefix and template. - `wikitool new entity --name "" --set entity_type= [--set tags=a,b] [--set related=X,Y] [--set sources="Source - Z"] [--set provenance=sourced|general|mixed]` - Writes `kb/entities//.md` - `` from `entity_type` via the type-spec's `layout:` - `wikitool new concept --name "" --set concept_type= ...` - Writes `kb/concepts//.md` - `` from `concept_type` via the type-spec's `layout:` - `wikitool new source --name "" --set source_type= --set raw_files=raw/notes/x.md,raw/notes/y.md --set fidelity= --set authority= [--set source_url=] [--set entities=A,B] [--set concepts=C,D]` - Writes `kb/sources//Source - .md` (prefix added automatically; `` from `source_type` via the type-spec's `layout:`) with a `raw_files:` list; rejects paths that don't exist - `wikitool new comparison --name "X vs Y" --set entities=X,Y` - Writes `kb/comparisons/X vs Y.md` - `wikitool new project --name "" --set responsibility= [--resume]` - Writes `kb/gtd//.md` and, with a task tracker configured, a same-named tracker project **PROPERTIES** - effect: write - idempotent: no - atomic: `new `: Yes - single file write. `new project`: **No** for the tracker-configured case - a tracker-project write (or its human-clearance request) happens before the kb/ page write, so a failure between the two leaves a tracker project with no page (a state `review`'s check 3 already reports), never a page with no tracker project. Still a single file write when no tracker is configured - budget: counted - network: no **EXAMPLES** - `tools/wikitool new entity --name "Docker" --set entity_type=tool --set tags=containers` - `tools/wikitool new source --name "Docker Cheatsheet" --set raw_files=raw/2026/09/docker-cheatsheet.md --set fidelity=verbatim --set authority=reporting` - `tools/wikitool new project --name "Homelab migration" --set responsibility=infrastruktur --resume # re-run after exit 42, once the user created the tracker project` **EXIT STATUS** - 0 success - 1 A page with this title already exists, the type is unknown, or a `--set` value is invalid - 1 The title is not a valid file name (forbidden character, control character, reserved name such as `CON` or `Index`, trailing dot or space, empty), collides with another page by case or Unicode normalization, the target file already exists, or the target path is over the 160-character path budget - 1 A `raw_files` path does not exist - 1 A capture field the type-spec requires is missing, or set to `unknown` - 1 `--resume` with a type other than `project` - 1 new project: The name is already taken in the tracker (case-insensitively; for `caldav` against every list in the account) - 1 new project: The configured access path has no write path (Super Productivity's `access: "snapshot"`) - 1 new project: The page write failed after the tracker project was confirmed to exist - 42 new project: The provider cannot create the project itself (Super Productivity's `access: "api"`); the output says what a human has to create **ON FAILURE** - A page with this title already exists, the type is unknown, or a `--set` value is invalid -> Not transient - fix the argument and retry once - The title is not a valid file name (forbidden character, control character, reserved name such as `CON` or `Index`, trailing dot or space, empty), collides with another page by case or Unicode normalization, the target file already exists, or the target path is over the 160-character path budget -> Not transient - choose another (for the budget: a shorter) title and retry once. Nothing was created, and for `new project` no tracker project either - A `raw_files` path does not exist -> Not transient - fix the path and retry once - A capture field the type-spec requires is missing, or set to `unknown` -> Pass it explicitly (e.g. `--set fidelity=verbatim --set authority=reporting`), then retry once - `--resume` with a type other than `project` -> Drop `--resume` and retry once - new project: The name is already taken in the tracker (case-insensitively; for `caldav` against every list in the account) -> Not transient - choose another name. If an earlier run of this exact command asked a human to create the project and they did, re-run with `--resume` - new project: The configured access path has no write path (Super Productivity's `access: "snapshot"`) -> Not transient - point at the `access: "api"` instance the error names. A `--resume` retry refuses the same way, since nothing about the config changes by asking again - new project: The page write failed after the tracker project was confirmed to exist -> Fix the write error, then re-run with `--resume` - a plain re-run is refused as a tracker collision - new project: The provider cannot create the project itself (Super Productivity's `access: "api"`); the output says what a human has to create -> Show the user the command's full output verbatim and stop. Once they have created the project, re-run the same command with `--resume`; it re-verifies and exits 42 again, unchanged, if the tracker still does not have it **NEVER** - Never hand-craft the page, or its frontmatter, instead. **NOTES** - A title becomes a file name, so it must be valid and unique on Windows and macOS as well as Linux, whichever platform runs the command and whichever root the type writes to. The rule is `kb/CONTRACT.md` § Titles are identifiers; it is checked on the full title, after `title_prefix`. - The target's path below the instance root may be at most 160 characters, counted in UTF-16 code units the way Windows counts MAX_PATH, so a Windows checkout without long paths keeps working. A longer one is refused, for every root, naming the length and how much shorter it has to get. - `new` never overwrites: a file already at the target - or one a case-insensitive file system would treat as the same file - is refused for every root, `instructions/` included. - The type-spec drives everything: fields, directory (`base_dir`/`layout`), title prefix, and template. `types list`/`types describe` show what a type requires. - The body skeleton is `types/..md` when that file exists, `` being the page's subtype field value (e.g. `types/entity.person.md` for `entity_type=person`) - it replaces the type-spec's `## Template` block whole. Without such a file, and for a type with no subtype field, the `## Template` block is used as before. - A schema `default:` is materialized only for a field the schema also lists in `required:`. - `--set` is repeatable, and comma-separated values fill array fields. An element that itself contains a comma is written `\,`, or passed as its own repeated `--set` for that field - repeating an array field appends. - A capture field the type-spec requires (a source's `fidelity`/`authority`) must be passed with `--set`; `new` never guesses it and refuses `unknown` for it. - Produces structurally correct frontmatter and a body skeleton only - the prose (Description, Summary, judgment calls about relationships) is written afterwards. - `new project`: with the `tasks` section of the settings configuring a task tracker, also makes sure a same-named tracker project exists - one name, one identity. No tracker configured is a legitimate, explicitly announced state: page only. - `new project`: tracker before page. The tracker side is settled first, so a failure past that point leaves a tracker project with no page - a state `review`'s check 3 reports - never a page with no tracker project. - `new project`: a name already taken, case-insensitively, in `kb/` or the tracker is refused outright, naming where it was found, and creates nothing. For `caldav` the tracker check covers every list in the account, not only the ones counted as projects. - `new project`: a provider whose configured access path has no write path (Super Productivity's `access: "snapshot"`) refuses entirely with exit 1, naming the `access: "api"` instance to use instead - neither the tracker project nor the page is created, and `--resume` behaves the same. - `new project`: a provider that could write but has no project-creation call of its own (Super Productivity's `access: "api"` - `GET /projects` exists, `POST /projects` does not) prints instructions for a human and exits 42, creating nothing. That is not one of the named gates, but the same exit code and the same handling. `caldav` never does this: `MKCALENDAR` creates the list, so a valid, non-colliding name always creates it. - `--resume` is how a later run tells the command a human has done what that message asked: it re-verifies through the tracker's read path before continuing to page creation, rather than trusting the claim, and exits 42 again if the tracker still does not have the project. `--resume` on any other type is refused. **SEE ALSO** - `wikitool types describe ` - what a type requires and where it lands - `wikitool touch` - changes a page's own frontmatter afterwards - `wikitool task new` - a tracker item without a page - `docs/explanation/knowledge-and-commitment.md` - why a project is a page and a tracker project #### `task new` Create one open item in the configured task tracker - never a kb/ page. **SYNOPSIS** - `wikitool task new --title "" (--project "<Name>" | --inbox) [--waiting [--follow-up-at YYYY-MM-DD]] [--notes "..."]` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes - a single API call, made only once every precondition (the project's own id, the WAITING tag's own id) is confirmed to exist, so a missing one never leaves a half-written item behind - budget: counted - network: yes **EXAMPLES** - `tools/wikitool task new --title "Renew the TLS certificate" --project "Homelab migration"` - `tools/wikitool task new --title "Quote from the electrician" --project "Homelab migration" --waiting --follow-up-at 2026-10-05` - `tools/wikitool task new --title "Read the qmd README" --inbox --notes "from [[Source - qmd - GitHub Repository]]"` **EXIT STATUS** - 0 success - 1 No `tasks` section in the settings - no tracker configured - 1 `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken - 1 Neither or both of `--project`/`--inbox`, or a `--follow-up-at` without `--waiting` or not `YYYY-MM-DD` - 1 A `--project` name matching no tracker project - 1 `--waiting` against a provider with no way to represent it right now (Super Productivity: the `waiting` tag does not exist) - 1 A read-only access path (Super Productivity's `access: "snapshot"`) - 42 The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking **ON FAILURE** - No `tasks` section in the settings - no tracker configured -> Not transient - configure a tracker first - `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken -> Not transient - fix the path or unset the variable - Neither or both of `--project`/`--inbox`, or a `--follow-up-at` without `--waiting` or not `YYYY-MM-DD` -> Fix the argument and retry once - A `--project` name matching no tracker project -> Create the tracker project first, or fix the name, then retry once - `--waiting` against a provider with no way to represent it right now (Super Productivity: the `waiting` tag does not exist) -> Create the tag in the tracker first, then retry once - A read-only access path (Super Productivity's `access: "snapshot"`) -> Point at the `access: "api"` instance the error names, then retry once - The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking -> Show the output to the user verbatim and stop - `tools/wikitool config migrate` or the unlock is theirs to run **NEVER** - Never fall back to `--inbox`, or to another project, when `--project` does not match. **NOTES** - Creates one open item in the configured task tracker; never touches `kb/`. - Exactly one of `--project` or `--inbox` is required; an omitted `--project` refuses rather than silently falling into the inbox. - `--project` names an existing tracker project, matched case-insensitively - never created, and never searched or guessed. - `--inbox` files into the tracker's own inbox. An item filed there never appears in `review`, since every one of its checks reaches items through a project name. - `--waiting` sets the WAITING status `review`'s waiting-overdue check reads. `--follow-up-at` is refused without `--waiting` - it is never a due date on its own. - `--notes` carries a freetext backref (e.g. to the `kb/` source page the item came from), stored verbatim, never parsed. - The tracker configuration is read from the `tasks`, `superproductivity` and `caldav` sections of the settings, or from the profile `WIKITOOL_TASKS_CONFIG` names when that variable is set (so one checkout can be run against several trackers in turn). A set variable that names no file is an error, never "no tracker configured". - No `tasks` section fails immediately with a "no tracker configured" message. - A provider whose configured access path has no write path (Super Productivity's `access: "snapshot"`) refuses entirely with exit 1, naming the `access: "api"` instance to use instead. - **Exits 42 only for the settings** (the tracker settings file of an earlier version, a locked secret source). A `--project` matching no tracker project, or `--waiting` against a provider that cannot represent it right now (Super Productivity: the `waiting` tag does not exist yet, and its API cannot create tags), are ordinary exit-1 refusals that create nothing. - Not idempotent: every successful run creates another item. **SEE ALSO** - `wikitool task list` - a project's open items and their ids - `wikitool task close` - marks an item done - `wikitool new project` - a project page and its tracker project - `docs/explanation/knowledge-and-commitment.md` - why commitments live in the tracker, not in `kb/` #### `task list` List a project's open items - id, title, and whether each carries the WAITING status. **SYNOPSIS** - `wikitool task list --project "<Name>"` **PROPERTIES** - effect: read - idempotent: yes - atomic: Yes - read-only, nothing to leave half-written - budget: counted - network: yes **EXAMPLES** - `tools/wikitool task list --project "Homelab migration"` **EXIT STATUS** - 0 success - 0 A `--project` matching no tracker project - prints "No open items", not an error - 1 No `tasks` section in the settings - no tracker configured - 1 `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken - 42 The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking **ON FAILURE** - No `tasks` section in the settings - no tracker configured -> Not transient - configure a tracker first, then retry once - `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken -> Not transient - fix the path or unset the variable - The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking -> Show the output to the user verbatim and stop - `tools/wikitool config migrate` or the unlock is theirs to run **NOTES** - Lists one tracker project's open items: id, title, and whether each carries the WAITING status. - The id source `task close` and `review`'s `waiting_overdue`/`someday_stale` findings need, without running `review` first. - Works on every access path a provider offers, read-only ones included. - A `--project` matching no tracker project prints "No open items": the tracker read does not distinguish an empty project from an unknown one. - Read-only. **SEE ALSO** - `wikitool task close` - closes an item by the id listed here - `wikitool review` - the findings that name items #### `task close` Mark one tracker item done - never delete it. **SYNOPSIS** - `wikitool task close --id <item-id>` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - a single API call; an unknown id is rejected by the provider itself (Super Productivity: `404 TASK_NOT_FOUND`) before anything is written - budget: counted - network: yes **EXAMPLES** - `tools/wikitool task close --id <item-id>` **EXIT STATUS** - 0 success - 1 No `tasks` section in the settings - no tracker configured - 1 `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken - 1 An `--id` matching no tracker item right now - 1 A read-only access path (Super Productivity's `access: "snapshot"`) - 42 The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking **ON FAILURE** - No `tasks` section in the settings - no tracker configured -> Not transient - configure a tracker first - `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken -> Not transient - fix the path or unset the variable - An `--id` matching no tracker item right now -> Get a current id from `task list` or `review`, then retry once - A read-only access path (Super Productivity's `access: "snapshot"`) -> Point at the `access: "api"` instance the error names, then retry once - The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking -> Show the output to the user verbatim and stop - `tools/wikitool config migrate` or the unlock is theirs to run **NEVER** - Never pass a title as `--id`. **NOTES** - Marks one tracker item done. It never deletes or moves an item - the only closing write this stack makes. - `--id` is the provider's own item id, from `task list` or a `review` finding - never a title. - The tracker configuration is read from the `tasks`, `superproductivity` and `caldav` sections of the settings, or from the profile `WIKITOOL_TASKS_CONFIG` names when that variable is set (so one checkout can be run against several trackers in turn). A set variable that names no file is an error, never "no tracker configured". - No `tasks` section fails with a "no tracker configured" message. - A provider whose configured access path has no write path (Super Productivity's `access: "snapshot"`) refuses entirely with exit 1, naming the `access: "api"` instance to use instead. - **Exits 42 only for the settings** (the tracker settings file of an earlier version, a locked secret source). **SEE ALSO** - `wikitool task list` - where the id comes from - `wikitool review` - findings that carry item ids #### `touch` Bump a page's `modified:` date and optionally rewrite its other frontmatter fields. **SYNOPSIS** - `wikitool touch --page "<Title>" [--summary "..."] [--provenance <v>] [--date YYYY-MM-DD] [--set field=value ...] [--add field=value ...] [--remove field=value ...] [--no-date] [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - single file write, and every refusal happens before it - budget: counted - network: no **EXAMPLES** - `tools/wikitool touch --page "Docker" --summary "Container runtime and image format"` - `tools/wikitool touch --page "Docker" --add tags=containers --no-date` - `tools/wikitool touch --page "Source - Docker Cheatsheet" --date 2026-09-01 --dry-run` **EXIT STATUS** - 0 success - 1 Page not found - 1 An invalid value for a field it writes, or a `raw_files:` path that doesn't exist - 1 A field owned by another command (`type:`, a page-ref array) or absent from the type's schema - 1 `--add`/`--remove` on a non-array field **ON FAILURE** - Page not found -> Fix the title and retry once - An invalid value for a field it writes, or a `raw_files:` path that doesn't exist -> Fix the argument and retry once - A field owned by another command (`type:`, a page-ref array) or absent from the type's schema -> Use the command the error names, or a field from the list it prints - `--add`/`--remove` on a non-array field -> Use `--set` for a scalar field, then retry once **NEVER** - Never edit `type:` or a page-ref array (`related:`/`sources:`/`entities:`/`concepts:`) by hand - `type:` changes go through the page-lifecycle procedure, page-ref arrays through `xref`. **NOTES** - Bumps `modified:` to today and optionally rewrites any other field the page's type declares. - `--summary`/`--provenance` are shorthands; `--set` reaches every other field and **replaces** its value, while `--add`/`--remove` change single elements of an array field (removing an absent element succeeds and says so). - Repeating `--set` for one array field appends *within the call*, and `\,` is a literal comma. - Refused, naming the command that owns them instead: `type:` (the page-lifecycle procedure) and the page-ref arrays `related:`/`sources:`/`entities:`/`concepts:` (`xref`). Everything else the schema declares is settable, and an unknown field is refused with the list of fields the page actually has. - Schema-validates the fields it writes; `raw_files:` entries must exist on disk. - A source page declares `date:` instead of `modified:`: the *publication* date of the raw material. It is never bumped to today and changes only when `--date` names a value explicitly. - `--no-date` changes only the given fields and leaves the date alone; `--dry-run` previews the new frontmatter without writing. - Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it. **SEE ALSO** - `wikitool xref add` / `wikitool xref remove` - the page-ref arrays - `instructions/page-lifecycle.md` - changing a page's `type:` #### `rename` Rename a page, or repoint references that name a page that never existed. **SYNOPSIS** - `wikitool rename --from "<Old>" --to "<New>" [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - one write per referencing page, then the file move - budget: counted - network: no **EXAMPLES** - `tools/wikitool rename --from "act_runner" --to "Act Runner" --dry-run` - `tools/wikitool rename --from "Docker Engine" --to "Docker"` **EXIT STATUS** - 0 success - 1 `--from` equals `--to` - 1 Neither `--from` nor `--to` is a page - 1 The `--to` title is already taken - also by a page that differs only in case or Unicode normalization, or by a file in the page's directory - or is not a valid file name, or would put the page's path over the path budget (see `kb/CONTRACT.md` § Titles are identifiers) - 1 A page write failed partway; nothing was renamed on disk **ON FAILURE** - `--from` equals `--to` -> Fix the arguments and retry once - Neither `--from` nor `--to` is a page -> Create the page first with `wikitool new`, or drop the reference with `wikitool xref remove` - The `--to` title is already taken - also by a page that differs only in case or Unicode normalization, or by a file in the page's directory - or is not a valid file name, or would put the page's path over the path budget (see `kb/CONTRACT.md` § Titles are identifiers) -> Choose another, or a shorter, title and retry once. Checked under `--dry-run` too - A page write failed partway; nothing was renamed on disk -> Check `git status`, resolve the write failure (permissions/disk), then re-run the full command - safe, since each page's rewrite is idempotent **NEVER** - Never fix up references by hand instead. **NOTES** - Renames a page and repoints every reference to it: body `[[wikilinks]]` (aliases and anchors preserved), a `[^cite-id]` whose id was derived from the old title (refreshed to match the new one, both in its Footnotes definition and every reference to it), the page's own H1, and every page-ref frontmatter array declared by the type's `page_ref_fields:`. - If `--from` is *not* a page but is referenced, it instead repoints those references onto the existing `--to` page and moves nothing - the fix for a reference spelled `act_runner` when the page is `Act Runner`. - Each page's rewrite is idempotent, so a re-run as-is is safe. If a write fails midway, nothing is renamed on disk and the error lists what was updated. - `--dry-run` lists every page it would change; run it first to see the blast radius. - Only `--to` is checked against the title rule and the path budget (160 UTF-16 code units for the whole path below the instance root). A page whose current title breaks either (`lint`'s Unportable Titles and Long Paths) can always be renamed away from it, and a title that differs from the page's own only by case (`Foo` to `FOO`) is allowed. **SEE ALSO** - `instructions/page-lifecycle.md` - renaming, moving and deleting a page - `wikitool move` - changes a page's directory, not its title - `wikitool rm` - deletes a page #### `rm` Delete a page and mechanically de-link it from the rest of the wiki. **SYNOPSIS** - `wikitool rm --page "<Title>" [--yes] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - one write per referencing page, then the delete - budget: counted - network: no **EXAMPLES** - `tools/wikitool rm --page "Old Draft" --dry-run` - `tools/wikitool rm --page "Old Draft" --yes # after the user approved the inbound list` **EXIT STATUS** - 0 success - 1 Page not found - 1 Other pages still reference it and `--yes` was not passed - 1 A de-link write failed partway; the page was not deleted **ON FAILURE** - Page not found -> Fix the title and retry once - Other pages still reference it and `--yes` was not passed -> Show the user the inbound list, get approval, then re-run with `--yes` - A de-link write failed partway; the page was not deleted -> Check `git status`, resolve the write failure, then re-run **NEVER** - Never pass `--yes` before the user has seen the inbound list and approved it. **NOTES** - Deletes a page and de-links it from the rest of the wiki: strips page-ref array entries and bare `- [[Title]]` / `- **label:** [[Title]]` bullets pointing at it. - Leaves prose references and inline citations in place and reports them afterwards; those are an editorial fix, not a reason to retry. - Refuses without `--yes` while other pages still reference it, listing them. - If a de-link write fails midway, the page is not deleted, so nothing is orphaned and a re-run is safe. - `--dry-run` lists what would change, without writing. - Not idempotent: once the page is gone, a second run finds nothing to delete. **SEE ALSO** - `instructions/page-lifecycle.md` - when a page is deleted rather than renamed - `wikitool rename` - repoints references instead of removing them #### `move` Move a page (or every misplaced page) to the directory its type-spec computes. **SYNOPSIS** - `wikitool move --page "<Title>"` - `wikitool move --reconcile [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: `--page`: yes, a single file move. `--reconcile`: no - one file move per page, each idempotent - budget: counted - network: no **EXAMPLES** - `tools/wikitool move --page "Docker"` - `tools/wikitool move --reconcile --dry-run` - `tools/wikitool move --reconcile` **EXIT STATUS** - 0 success - 1 Neither or both of `--page`/`--reconcile` given - 1 The named page is not found, or has no `type:` to compute a placement from - 1 The destination already holds an entry with the same name, or one that differs only in case or Unicode normalization (a pre-existing duplicate-stem collision), or its path would be over the path budget - refused rather than silently skipped - 1 `--reconcile` failed partway **ON FAILURE** - Neither or both of `--page`/`--reconcile` given -> Fix the arguments and retry once - The named page is not found, or has no `type:` to compute a placement from -> Fix the title, or give the page its `type:`, then retry once - The destination already holds an entry with the same name, or one that differs only in case or Unicode normalization (a pre-existing duplicate-stem collision), or its path would be over the path budget - refused rather than silently skipped -> Resolve the collision, or `wikitool rename` the page to a shorter title, then retry - `--reconcile` failed partway -> Safe to retry as-is - `--reconcile` only re-moves what is still misplaced **NEVER** - Never choose a directory by hand instead. **NOTES** - Moves a page to the directory its type-spec computes for its current frontmatter (`base_dir` + `layout`, the same rule `new` places a page by) - never to a hand-chosen destination; there is no `--to <dir>`. - A destination whose path would be over the path budget (160 UTF-16 code units below the instance root) is refused, and `--reconcile` skips such a page and names it, as it does for an occupied destination - `wikitool rename` the page to a shorter title. - `--reconcile` applies it corpus-wide: every misplaced page moves in one call, and a second run reports nothing left to do. It fixes `lint`'s `Misplaced Pages` (advisory) and `Nested Pages` (hard) findings. - Neither mode touches a body or a frontmatter field, and the page's title - its only identity in the wiki - never changes; only the file moves. - A directory a move empties is removed with it, so a page nested below its area leaves no leftover directory behind. - A page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced, so a re-run is safe. - `--dry-run` (with `--reconcile`) lists the moves; run it first to see the blast radius. - Run `wikitool index rebuild` afterwards. **SEE ALSO** - `wikitool lint` - reports Misplaced and Nested Pages - `wikitool index rebuild` - run after moving - `instructions/page-lifecycle.md` - moving, renaming and deleting a page ### Links and citations #### `xref add` Declare that A <rel> B. **SYNOPSIS** - `wikitool xref add --a "<A>" --b "<B>" --rel <label> [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - a single write to A; B is never touched, and every refusal happens before it - budget: counted - network: no **EXAMPLES** - `tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on` - `tools/wikitool xref add --a "Gitea Actions" --b "Act Runner" --rel depends-on --dry-run` **EXIT STATUS** - 0 success - 1 Page A or B not found - 1 A's type declares no `related:` field - 1 `<label>` is not authorised for this collection pair, or the source collection authorises no labels into the target's collection at all **ON FAILURE** - Page A or B not found -> Fix the title and retry once - re-running never duplicates a link - A's type declares no `related:` field -> For a source page use `xref link-source`; otherwise there is nothing to link from this page - `<label>` is not authorised for this collection pair, or the source collection authorises no labels into the target's collection at all -> Pick a label from the authorised set the refusal lists, then retry once **NEVER** - Never create the missing page just to force the link through. - Never hand-write a reference field the type does not declare. - Never edit a collection's `outbound:` block just to get past a label refusal - authorising a further label is a deliberate edit of its own. **NOTES** - Declares **one** edge, `A <label> B`: written into A's `related:` as `- <label>: B` and rendered into A's generated links region. - B is not touched and does not point back - its inbound view is rendered from the graph. - Idempotent; re-running with a different label *relabels* rather than appending, since one page asserts one thing about another. - Refuses before writing when A's type does not declare `related:` - a source page declares `entities:`/`concepts:` instead, and the refusal names them and points at `xref link-source`. - Refuses before writing when `<label>` is not authorised by the source collection's `outbound:` block for the target's collection; the refusal lists the authorised set and points at `instructions/link-taxonomy.md`. - `--dry-run` reports the edge without writing. **SEE ALSO** - `wikitool xref remove` - removes a reference - `wikitool links show` - the edges out of and into a page - `instructions/link-taxonomy.md` - the labels and what each asserts #### `xref remove` Remove a cross-reference: the inverse of `xref add`. **SYNOPSIS** - `wikitool xref remove --a "<A>" --b "<B>" [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - writes A then B, both idempotent - budget: counted - network: no **EXAMPLES** - `tools/wikitool xref remove --a "Gitea Actions" --b "act_runner"` - `tools/wikitool xref remove --a "Gitea Actions" --b "Act Runner" --dry-run` **EXIT STATUS** - 0 success - 1 Page A not found (B is allowed not to exist) **ON FAILURE** - Page A not found (B is allowed not to exist) -> Fix the title; safe to retry freely **NEVER** - Never hand-edit a page-ref array to remove a reference. **NOTES** - Clears the reference in **both** directions: the cleanup command for a deleted or hand-renamed page rather than the strict inverse of a one-directional `xref add`. - Clears `<B>` from every page-ref frontmatter field `<A>`'s type declares (`related:`, `sources:`, `entities:`, `concepts:`) plus the matching bullets. - Also sweeps a page-ref field the type does *not* declare but some other type does, and drops that key once empty. - `--b` need not still exist as a page, so this clears a reference left by a hand-deleted or hand-renamed page without hand-editing frontmatter. - Idempotent: removing an absent link is a no-op. `--dry-run` reports without writing. **SEE ALSO** - `wikitool xref add` - declares an edge - `wikitool rm` - deletes a page and de-links it #### `xref link-source` Batch-link a source page to every entity/concept it mentions. **SYNOPSIS** - `wikitool xref link-source --source "Source - X" --entities A,B,C [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - one write per entity plus one for the source page, idempotent per page - budget: counted - network: no **EXAMPLES** - `tools/wikitool xref link-source --source "Source - Docker Cheatsheet" --entities Docker,Podman --dry-run` - `tools/wikitool xref link-source --source "Source - Docker Cheatsheet" --entities Docker,Podman` **EXIT STATUS** - 0 success - 1 Source page not found - 1 A page in `--entities` does not exist, or a target could not be written; the others were linked - 1 A page in `--entities` is itself a source page; the others were linked - 1 The source page itself could not be written after its targets were **ON FAILURE** - Source page not found -> Fix the title and retry once - A page in `--entities` does not exist, or a target could not be written; the others were linked -> Fix the named pages and re-run - safe, since every write is idempotent. `sources trace --page "<Title>"` shows who is already linked - A page in `--entities` is itself a source page; the others were linked -> Drop it from `--entities` and record the citation with `cite add --page "<citing source>" --source "<cited source>"` instead - The source page itself could not be written after its targets were -> Fix the write failure and re-run (idempotent) **NOTES** - Each target gets the source page in its `sources:`, and the source page records each target in its own `entities:` or `concepts:`. No body bullet is written on either side - `sources:` *is* the record. - Which field a target lands in follows its collection (`kb/entities/` -> `entities:`), so a new collection needs no code change. - A target whose collection matches no reference field the source type declares is linked one way and named in the output. - A target that does not exist is skipped and named; the others are still linked, and the run then exits 1. - A target that is itself a source page is refused, named with the `cite add` command to use instead, and handled like a missing page: nothing is written for it, the others are still linked, and the run exits 1. `link-source` means "A mentions X" and writes `X.sources += A`, which for X = another source would claim the citation in the wrong direction; a citation between two sources has a direction only `cite add` records. - Idempotent in both directions. `--dry-run` reports what would be linked. **SEE ALSO** - `wikitool sources trace` - who a source is already linked to - `wikitool xref add` - one labelled edge between two pages - `wiki-ingest` skill - where a source is linked after it is written #### `links show` Show the edges out of and into a page. **SYNOPSIS** - `wikitool links show --page "<Title>" [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool links show --page "Act Runner"` - `tools/wikitool links show --page "Act Runner" --json` **EXIT STATUS** - 0 success - 1 Page not found **ON FAILURE** - Page not found -> Check the exact title with `search`; a wikilink target is not always the page's stem **NOTES** - Shows the declared graph around one page in both directions: the edges it asserts (from its own `related:`, with labels) and the edges other pages assert about it. - The inbound half is computed across the corpus on every call, never stored, so it is complete. - `--json` prints both halves as JSON. - Read-only; exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool xref add` / `wikitool xref remove` - change the outbound edges - `wikitool search` - finds the exact title #### `cite id` Print the deterministic footnote id `cite add` would use for this (title, file) pair. **SYNOPSIS** - `wikitool cite id --title "Source - X" [--file <qualifier>]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool cite id --title "Source - Docker Cheatsheet"` **EXIT STATUS** - 0 success **NEVER** - Never paste an id from here into a page by hand - `cite add` writes the definition and prints the marker to paste. **NOTES** - Prints the footnote id `cite add` would use for this (title, file) pair. - A preview only: it does not check that the id is free on any given page. - Never fails; safe to retry freely. Read-only and exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool cite add` - writes the definition #### `cite add` Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region. **SYNOPSIS** - `wikitool cite add --page "<Title>" --source "Source - X" [--file <qualifier>] [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - single file write - budget: counted - network: no **EXAMPLES** - `tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet"` - `tools/wikitool cite add --page "Docker" --source "Source - Docker Cheatsheet" --file part-2` **EXIT STATUS** - 0 success - 1 The page is not found - 1 The source page is not found - citing it would be a dangling reference **ON FAILURE** - The page is not found -> Fix the title and retry once - The source page is not found - citing it would be a dangling reference -> Fix the source title, or create the source page first, then retry once **NEVER** - Never compute or type a `[^cite-id]` or its definition by hand - paste the marker this prints. **NOTES** - Upserts a `[^cite-id]: [[Source - X]]` definition in the page's generated footnotes region, creating the region between `<!-- wikitool:footnotes -->` markers if absent. - Reuses the id when the page already cites this exact source/file pair, so a repeat changes nothing. - Adds `Source - X` to the page's frontmatter `sources:` - except on `Source - X` itself: a source page citing one of its own raw files (`--file`) gets the definition and leaves `sources:` untouched, since a page never lists its own title there. - A source page may cite another source page this way - `cite add` is the command that records a citation between two sources, in the direction it was made. - Prints the `[^cite-id]` marker; pasting it into the prose is a manual, editorial step. - `--dry-run` reports without writing. **SEE ALSO** - `wikitool cite sync` - prunes and re-orders the region - `wikitool cite id` - previews an id #### `cite sync` Reconcile each page's footnotes region against its actual `[^id]` references. **SYNOPSIS** - `wikitool cite sync [--page "<Title>" | --all] [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - one write per page, each idempotent - budget: counted - network: no **EXAMPLES** - `tools/wikitool cite sync --page "Docker"` - `tools/wikitool cite sync --all --dry-run` **EXIT STATUS** - 0 success - 1 Neither or both of `--page`/`--all` given, or the page is not found - 1 A page write failed partway **ON FAILURE** - Neither or both of `--page`/`--all` given, or the page is not found -> Fix the arguments and retry once - A page write failed partway -> Resolve the write failure and re-run - each page's re-render is idempotent **NOTES** - Prunes definitions nothing references any more, re-renders the region in first-reference order, and reports every `[^id]` reference left with no definition. - An undefined-reference report is not a failure: fix the reference, or run `cite add`, and re-run. - A page still carrying the pre-4.0.0 undelimited footnote block is converted to a marked region in the same pass. - Each page's re-render is idempotent; safe to retry freely. `--dry-run` reports without writing. **SEE ALSO** - `wikitool cite add` - writes a definition ### Catalog and log #### `index rebuild` Regenerate the catalog from every page's frontmatter. **SYNOPSIS** - `wikitool index rebuild [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - each catalog file (the map plus one shard per collection/area) is rewritten independently, then stale shards are removed; an interruption can leave some regenerated and others not - budget: counted - network: no **EXAMPLES** - `tools/wikitool index rebuild` - `tools/wikitool index rebuild --dry-run` **EXIT STATUS** - 0 success - 1 An I/O error while writing or removing a catalog file (rare) **ON FAILURE** - An I/O error while writing or removing a catalog file (rare) -> Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges **NEVER** - Never hand-edit `kb/index.md` or an `INDEX.md` - re-run this command instead. **NOTES** - Rewrites `kb/index.md` as a map: statistics, one row per collection and per area, and links to the shards - no page rows. - Writes the page tables to a generated `INDEX.md` in each collection. An area with more than 50 rows gets its own `INDEX.md` in its directory. - Deletes stale shards - an `INDEX.md` of a collection or area that no longer exists - in the same pass. - Warns about every page nested more than one directory below its collection; it is still catalogued, folded into its area. - `--dry-run` prints every file it would write and every stale shard it would remove, and writes nothing. **SEE ALSO** - `wikitool search` - finds a page without reading the catalog - `wikitool lint` - its Nested Pages finding is what the warning previews - `instructions/publish-cycle.md` - where a write session runs this #### `log append` Append a formatted entry to `kb/log.md`. **SYNOPSIS** - `wikitool log append --op ingest|query|lint|create|update|delete|rename|move --title "..." [--body "..."|--body-file path]` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes - single append - budget: counted - network: no **EXAMPLES** - `tools/wikitool log append --op ingest --title "raw/articles/docker-cheatsheet.md" --body "Created [[Docker]]; updated [[Container]]."` - `tools/wikitool log append --op lint --title "2026-09-26" --body-file lint-summary.md` **EXIT STATUS** - 0 success - 1 `--op` is not one of ingest, query, lint, create, update, delete, rename, move - 1 `--body-file` is missing, not a readable file, or not valid UTF-8 **ON FAILURE** - `--op` is not one of ingest, query, lint, create, update, delete, rename, move -> Nothing was written - fix the argument and retry once - `--body-file` is missing, not a readable file, or not valid UTF-8 -> Nothing was written - fix the path and retry once **NEVER** - Never re-run after an uncertain outcome without first checking the tail of `kb/log.md` - a second run appends a second entry. **NOTES** - Appends one entry to `kb/log.md`: a `## [YYYY-MM-DD] <op> | <title>` heading, the body if one is given, and a `---` separator. - Not idempotent: every successful run appends a new entry, including a repeated one. **SEE ALSO** - `wikitool log status` - counts the ingests logged since the last lint - `instructions/publish-cycle.md` - where a write session runs this #### `log status` Read-only: count `ingest` entries logged since the last `lint` entry. **SYNOPSIS** - `wikitool log status` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool log status` **EXIT STATUS** - 0 success - 0 `kb/log.md` is missing or empty - reported as nothing logged, not a failure **NOTES** - Counts the `ingest` entries in `kb/log.md` after the most recent `lint` entry, or since the start of the log if it was never linted, and the total number of entries. - At 10 or more it prints that the `wiki-lint` skill is due next - the every-10-sources full lint of the Maintenance Schedule. - Read-only; safe to retry freely. **SEE ALSO** - `wikitool log append` - writes the entries this counts - `wiki-lint` skill - what the threshold asks for - `tools/CONTRACT.md` § Maintenance schedule - the cadence this reports on ### Finding and checking #### `lint` Run structural lint checks against kb/. **SYNOPSIS** - `wikitool lint [--json] [--markdown out.md] [--full] [--fail-on-error]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Writes one report file (single atomic write) unless `--json` - budget: counted - network: no **EXAMPLES** - `tools/wikitool lint` - `tools/wikitool lint --json` - `tools/wikitool lint --fail-on-error` **EXIT STATUS** - 0 success - 1 Only with `--fail-on-error`: hard findings exist **ON FAILURE** - Only with `--fail-on-error`: hard findings exist -> Act on the findings - exit 1 here means "act on the findings", not "the tool is broken". Re-running is safe, but only to re-*measure* after a fix **NEVER** - Never re-run just to re-read the findings - the printed path holds the full report. **NOTES** - Structural and provenance checks over `kb/`: broken wikilinks, wikilinks wrapped across a line break, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, uncovered raw files, broken `raw_files:` refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, and unbalanced generated-region markers. - Unportable Titles is a hard finding, and hard at every `kb_version`: a page whose title is not a valid file name on Windows and macOS (forbidden character, reserved name, trailing dot or space), or that collides with another page by case or Unicode normalization. `wikitool rename` is the fix. - Wrapped Wikilinks is a hard finding: a `[[...]]` with a line break inside it. The link graph reads it as the title it folds to (the break and its indentation become one space), so it is not also a broken link unless that title is missing; the fix is to put it back on one line. - Broken Anchors is advisory: a `[[Page#Section]]` whose page exists but has no heading the anchor names (any level, compared without case, inline-code backticks or extra whitespace; every segment of a nested `[[Page#A#B]]`). The link still reaches the page, so nothing else reports it - typically a section that was promoted to a page of its own or renamed. A missing page is Broken Wikilinks instead. - Pages nested more than one directory below their collection are a hard finding - the generated catalog folds these into their area silently rather than merely reading it. - Edges whose label is missing or not authorised by the source collection's `outbound:` are both hard once `kb_version` has reached the release that introduced labelled edges, and advisory below it. - Advisory only: `see-also` edges whose reverse direction already carries a specific label - never migration-gated. - Advisory only: a collection past the catalog's per-area shard threshold that has no areas to shard, reported with the split its subtype field would produce, and only when that split puts every resulting area at or under the threshold. - Advisory only: source pages sitting in the `unclassified` catalog slot. - Advisory only: Long Paths - a file under `kb/` or `raw/` whose path below the instance root is over 160 UTF-16 code units, the budget that keeps a Windows checkout without long paths working. Reported as `{path, length}`; a corpus over the budget breaks no lint run. `wikitool rename` is the fix for a page. - Advisory only: quote-limit overages (>2 blockquotes/page). - Advisory only: Unfilled Template Sections - a page with at least one `##` section whose non-blank lines are all template placeholders (content starting with `TODO`, after an optional list marker, checkbox, table cell or bold field label), reported as `{page, sections}`. Code, generated regions and footnote definitions do not count; an empty section, or one placeholder beside written lines, is no finding. Writing the section from the page's sources or retiring the page is the fix - not a mechanical one. - Prints only the sections that found something and always writes the full report to `reports/Lint Report <date>.md` (or `--markdown`), naming the path. `--full` prints everything; `--json` prints the findings and writes nothing. - Exits 0 whatever it finds unless `--fail-on-error` is passed. **SEE ALSO** - `wiki-lint` skill - the procedure that runs this - `wikitool move --reconcile` - fixes Misplaced and Nested Pages - `wikitool rename` - fixes Unportable Titles and, for a page, Long Paths - `wikitool log status` - whether a full lint is due #### `search` Find pages in `kb/` by text and/or frontmatter. **SYNOPSIS** - `wikitool search ["<text>"] [--field <predicate> ...] [--kind/--subtype/--collection/--tag <v>] [--regex] [--limit N] [--sort [-]<field>] [--backend <name>] [--matches] [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool search "act runner"` - `tools/wikitool search --field entity_type=system --field '!sources'` - `tools/wikitool search "docker" --collection entities --limit 10 --json` **EXIT STATUS** - 0 success - 1 `rg` is not installed - 1 `rg` did not finish within 30 s - 1 A malformed `--field` predicate, or an unknown `--backend` - 1 An unknown field name; the error lists the fields that exist **ON FAILURE** - `rg` is not installed -> Not transient - install `rg`, then retry - `rg` did not finish within 30 s -> A timeout is a pathological pattern or an unresponsive corpus directory, not a slow answer - narrow the query or drop `--regex` rather than retrying it unchanged - A malformed `--field` predicate, or an unknown `--backend` -> Fix the argument and retry - An unknown field name; the error lists the fields that exist -> Pick a field from that list and retry **NEVER** - Never retry a timed-out query unchanged. - Never grep `kb/` yourself instead - it can add no page this misses, only the generated files it excludes. **NOTES** - Finds pages in `kb/` without reading the catalog. Text search runs through a pluggable backend (`rg` today). - `--field` predicates are evaluated on frontmatter - `f=v`, `f~substring`, `'f>=v'`, `'f:*'` (present), `'!f'` (absent) - repeatable and ANDed. With no text this is a pure structured query. - One hit per line, ` | `-separated as `score | kind/subtype | title | path | summary`. **Title and path are never truncated** - the title is the identifier `touch`/`xref`/`cite` take. The summary, the one lossy field and the only one that may contain the separator, goes last, so splitting on `" | "` with `maxsplit=4` is unambiguous. - Scope is pages: the backend walks `kb/` but drops the kb-root meta files, every `COLLECTION.md` and every generated `INDEX.md` - a hand-run grep over `kb/` can add none of them but those. - `--limit` defaults to 50 (`0` for no limit), and **a truncated result says so**: `50 of 182 result(s)` in the table, `total`/`truncated`/`limit` beside `count` in `--json`, where `count` stays the number of results in the payload. `api.search` and the MCP `search` tool carry the same default and the same fields. - A page whose frontmatter does not parse can match no positive predicate, so it is **named** rather than dropped: `--json` always carries an `unreadable` list of `{path, reason}` (usually empty), and the table form writes the same lines to stderr. - An unknown field name is reported with the list of fields that do exist - never answered with an empty result, which would read as "no such pages". - `--regex` is applied by `rg` alone, whose engine is linear; the ranking boosts for title and summary are literal-containment only, so a non-literal pattern is ranked by match count. - `rg` is killed after 30 s and reported as a failure. - Read-only, and **exempt from the Iteration Budget Gate**. **SEE ALSO** - `wikitool links show` - the edges around a page once found - `wiki-query` skill - answering a question from the wiki #### `review` The GTD weekly review. **SYNOPSIS** - `wikitool review [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: yes **EXAMPLES** - `tools/wikitool review` - `tools/wikitool review --json` **EXIT STATUS** - 0 success - 1 No `tasks` section in the settings, or a malformed one - a clear "no tracker configured" message - 1 `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken - 1 The provider was reachable at config-parse time but a read call failed mid-run; the full report (findings plus which checks ran) was printed first - 42 The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking **ON FAILURE** - No `tasks` section in the settings, or a malformed one - a clear "no tracker configured" message -> Not fixed by retrying unchanged - configure or repair the `tasks`, `superproductivity` and `caldav` sections first - `WIKITOOL_TASKS_CONFIG` names a profile that does not exist or is broken -> Not transient - fix the path or unset the variable - The provider was reachable at config-parse time but a read call failed mid-run; the full report (findings plus which checks ran) was printed first -> Start the unreachable provider (e.g. the tracker app), then retry plainly - The tracker settings file of an earlier version is still there, or a secret source (1Password) needs unlocking -> Show the output to the user verbatim and stop - `tools/wikitool config migrate` or the unlock is theirs to run **NEVER** - Never present a report that exited 1 as complete. **NOTES** - Joins the configured task-tracker provider against `kb/gtd/` project pages over the case-normalized project name, at read time, storing nothing - not even a `reports/` file. - **stalled**: a tracker project with zero open items whose `kb/` page is `state: active`; `dormant`/`completed`/`abandoned` never fire. - **waiting-overdue**: a `WAITING` item whose `follow_up_at` is older than `thresholds.stalled_waiting_days`. - **unpaged-project**: a tracker project with no matching `kb/` page, older than `thresholds.unpaged_project_weeks`. - **no-open-loop**: a `kb/` page `state: active` with no matching tracker project, or one with zero open items - the reverse direction of unpaged-project, so a rename on either side surfaces on both. - **someday-stale**: a someday/maybe item untouched for longer than `thresholds.someday_stale_months`. - A value a provider genuinely cannot supply - a `WAITING` item with no `follow_up_at`, a tracker project with no determinable creation date - is its own finding (`waiting_no_follow_up`/`project_age_unknown`) rather than a silent skip. - Thresholds come from the tracker configuration, never from the schema. - The tracker configuration is read from the `tasks`, `superproductivity` and `caldav` sections of the settings, or from the profile `WIKITOOL_TASKS_CONFIG` names when that variable is set (so one checkout can be run against several trackers in turn). A set variable that names no file is an error, never "no tracker configured". - The tracker settings file of an earlier version exits 42 and names `tools/wikitool config migrate`; so does a secret source that needs unlocking. - Text output is one `[check] project: message` line per finding, preceded by a `Source:` line naming which access path answered and, for `superproductivity`'s `access: "snapshot"`, the snapshot's age. `--json` carries the same findings plus `checks_run`/`checks_skipped`/`kb_project_count`/`complete`/`source` (`{"kind": ..., "detail": ...}` or `null`). - A provider that cannot be reached mid-run degrades only the checks that needed the failing call; the report is printed in full, then exit 1 follows - never rendered as if it were complete. - Re-reads everything fresh on every call, so nothing is ever stale to re-fetch. - Read-only, and **exempt from the Iteration Budget Gate**. **SEE ALSO** - `gtd-weekly-review` skill - turns the findings into decisions - `wikitool task list` / `wikitool task close` - act on an item - `wikitool new project` - a project page and its tracker project ### Provenance #### `sources coverage` List raw files with no source page, broken `raw_files:` references, and legacy directory/URL-only source pages. **SYNOPSIS** - `wikitool sources coverage [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool sources coverage` - `tools/wikitool sources coverage --json` **EXIT STATUS** - 0 success **NOTES** - Lists raw files with no source page, broken `raw_files:` references, and legacy directory/URL-only source pages. - `--json` prints the same lists as JSON. - Never fails; read-only and safe to retry freely. **SEE ALSO** - `wikitool sources trace` - follows one file or page - `wiki-ingest` skill - turns an uncovered raw file into a source page #### `sources trace` Trace provenance in either direction: raw file, or page. **SYNOPSIS** - `wikitool sources trace --raw <path> | --page "<Title>"` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool sources trace --page "Docker"` - `tools/wikitool sources trace --raw "raw/articles/llm-wiki.md"` **EXIT STATUS** - 0 success - 1 Neither or both of `--raw`/`--page` given, or `--page` names an unknown page - 1 `--raw` names a file no source page covers - reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed rejection **ON FAILURE** - Neither or both of `--raw`/`--page` given, or `--page` names an unknown page -> Fix the argument and retry - `--raw` names a file no source page covers - reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed rejection -> Nothing to retry: the file is uncovered. Ingest it, or check the path **NOTES** - `--raw <path>`: raw file -> the source page(s) covering it -> the pages citing those. - `--page "<Title>"`: page -> its sources -> their raw files. - Read-only. **SEE ALSO** - `wikitool sources coverage` - every uncovered raw file at once - `wikitool xref link-source` - links a source page to what it mentions #### `sources rebuild-index` Regenerate the `kb/provenance.md` reverse index. **SYNOPSIS** - `wikitool sources rebuild-index [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - the single provenance file is regenerated from scratch - budget: counted - network: no **EXAMPLES** - `tools/wikitool sources rebuild-index` - `tools/wikitool sources rebuild-index --dry-run` **EXIT STATUS** - 0 success - 1 An I/O error while writing `kb/provenance.md` (rare) **ON FAILURE** - An I/O error while writing `kb/provenance.md` (rare) -> Safe to retry freely **NEVER** - Never hand-edit `kb/provenance.md` - re-run this command instead. **NOTES** - Regenerates the `kb/provenance.md` reverse index (raw file -> source page -> citing pages) from scratch. - `--dry-run` prints the result instead of writing `kb/provenance.md`. **SEE ALSO** - `wikitool index rebuild` - the page catalog, rebuilt alongside - `instructions/publish-cycle.md` - where a write session runs this ### Raw material and uploads #### `raw fetch` Capture a web page the user names into `incoming/`: the HTML as received plus a derived text, for `raw accept` to promote. Pages of a known website are fetched in the website's standard form and cleaned of its page furniture; a host the instance routes so is fetched with the user's own session or through an external fetcher. **SYNOPSIS** - `wikitool raw fetch <url> [--name <stem>]` - Fetch the page and write `incoming/<stem>.html` and `incoming/<stem>.md` - `wikitool raw fetch <url> --scope [--name <stem>]` - Fetch the page and the subpages its site handler offers, and write the folder `incoming/<stem>/` - a `.html` + `.md` pair per page and the manifest `_scope.json` - `wikitool raw fetch --html incoming/<file>.html --url <url>` - Derive the `.md` from a page a human saved from their browser - no network access **PROPERTIES** - effect: write - idempotent: no - atomic: Yes for what it leaves behind - one or two new files in `incoming/`, created exclusively; a failure on the second removes the first. With `--scope`: all of the folder or none of it - the pages are fetched into a temporary place and moved in as a whole - budget: counted - network: yes **EXAMPLES** - `tools/wikitool raw fetch https://example.org/blog/post` - `tools/wikitool raw fetch https://example.org/ --name example-start` - `tools/wikitool raw fetch https://learn.microsoft.com/en-us/powershell/module/oslicense/ --scope` - `tools/wikitool raw fetch --html incoming/post.html --url https://example.org/blog/post` **EXIT STATUS** - 0 success - 1 The URL is not `http`/`https` (also after a redirect), or neither or both of `<url>` and `--html` were given - 1 The `sites` section of the settings is malformed, `disabled` names a handler that does not exist, or a file in `sites/` cannot be used and names this URL's host (or none can be read) - 1 A target file already exists in `incoming/` - 1 The server answered with an HTTP error, could not be reached, took longer than 30 s, or sent more than 25 MiB - 1 The `fetch` section is malformed, routes the host through the session though no site handler of the host names a login page, or names a fetcher this checkout does not define - 1 The external fetcher could not be started, timed out, ended with an exit other than 0, printed nothing or more than 25 MiB - 1 raw fetch --scope: No site handler offers subpages for the URL, the page offers none or more than `sites.max_pages`, `robots.txt` forbids a subpage or cannot be read, a subpage cannot be fetched, answers something other than HTML or leaves the host, the folder `incoming/<stem>/` exists, or a path would be over the budget - 1 raw fetch --html: `--url` is missing, the file is not directly in `incoming/` or does not exist, or `incoming/<stem>.md` already exists - 42 The host is fetched with the user's session and none is stored, or the page arrived without the sign of being signed in that its site handler names (the session expired) - 42 The site handler declares `render` and the optional browser component is not installed **ON FAILURE** - The URL is not `http`/`https` (also after a redirect), or neither or both of `<url>` and `--html` were given -> Fix the call and retry once. A local file is dropped into `incoming/` by hand, not fetched - The `sites` section of the settings is malformed, `disabled` names a handler that does not exist, or a file in `sites/` cannot be used and names this URL's host (or none can be read) -> Nothing was fetched or written. Fix the section or the handler file named in the message - `tools/wikitool config set`, the settings file or the file in `sites/` (`wikitool doctor` lists the files) - then retry once - A target file already exists in `incoming/` -> Nothing was written or overwritten. Accept or remove what is there, or pass `--name <stem>`, then retry once - The server answered with an HTTP error, could not be reached, took longer than 30 s, or sent more than 25 MiB -> Nothing was written. An HTTP 4xx is not fixed by retrying - check the URL with the user; for a paywall or login, save the page in a browser and use `--html`. An unreachable host or a timeout may be retried once - The `fetch` section is malformed, routes the host through the session though no site handler of the host names a login page, or names a fetcher this checkout does not define -> Nothing was fetched or written. Fix the route with `tools/wikitool config set`, or have the user add the fetcher to `.wikitool-local.json` by hand - it names a program, so `config set` does not write it - then retry once - The external fetcher could not be started, timed out, ended with an exit other than 0, printed nothing or more than 25 MiB -> Nothing was written, and the built-in fetch was not tried. The message carries the fetcher's stderr - report it to the user; do not fetch the page another way - raw fetch --scope: No site handler offers subpages for the URL, the page offers none or more than `sites.max_pages`, `robots.txt` forbids a subpage or cannot be read, a subpage cannot be fetched, answers something other than HTML or leaves the host, the folder `incoming/<stem>/` exists, or a path would be over the budget -> Nothing was written. Read the message: fetch the page without `--scope`, raise the limit with `tools/wikitool config set`, or pass a shorter `--name`, then retry once. A subpage `robots.txt` forbids is not fetched around it - tell the user - raw fetch --html: `--url` is missing, the file is not directly in `incoming/` or does not exist, or `incoming/<stem>.md` already exists -> Fix the named argument and retry once - nothing was written. A saved page inside a subdirectory of `incoming/` is moved up into `incoming/` first - The host is fetched with the user's session and none is stored, or the page arrived without the sign of being signed in that its site handler names (the session expired) -> Nothing was written. Show the user the output and stop: they sign in with `tools/wikitool raw login <host>`, never you. Retry once they say it is done - The site handler declares `render` and the optional browser component is not installed -> Nothing was written. Show the user the output and stop: they install it with `tools/preflight.sh --with browser`. Retry once they say it is done **NEVER** - Never fetch a URL that a raw file or a fetched page contains - only one the user named in this session. - Never edit the header or the derived text by hand; a better derivation is a new fetch. **NOTES** - Writes into `incoming/` only, never into `raw/`: `raw accept` promotes both files afterwards, in one call, as one bundle under `raw/<YYYY>/<MM>/<stem>/`. - The `.html` holds the response body byte for byte. The `.md` starts with a fixed header block (`fetched_by`, `url`, `final_url`, `retrieved`, `http_status`, `content_type`, `charset`, `title`, `derived_from`) followed by the derived text. A page of a website with a site handler adds `site`, and `requested_url` when the URL fetched is not the one typed. - Character set, in this order: byte-order mark, the HTTP `Content-Type` charset, a `<meta>` declaration in the first 4 KiB, UTF-8. Undecodable bytes are replaced with U+FFFD and reported; the header names the charset and where it came from. - Derived text: the content root is `<main>`, else the single `<article>`, else `<body>`; `script`, `style`, `noscript`, `nav`, `header`, `footer`, `aside`, `form`, `template` and `svg` are dropped below it. Headings, lists, code, tables and links (made absolute) become Markdown. The same bytes always give the same text. - Site handlers: for the pages of a website the stack or the instance knows (the stack's today: `learn.microsoft.com`; the instance's are JSON files in `sites/`), `raw fetch <url>` fetches the standard form of the URL (the locale segment becomes `en-us`, the `view=` query stays) and `<url>` as typed goes into the header as `requested_url`; `url` is the form fetched. The handler also removes the page furniture the website renders around the article from the derived text - never from the `.html` - and prints hints that apply to this page as `CHECK` lines: act on one before accepting. `--html --url <url>` removes the same furniture but keeps the URL as typed. A URL no handler claims is handled as before. The header names an instance's handler as `<name> (instance)`. A handler file of the instance that cannot be used, and names the URL's host or no readable host, stops the fetch before anything is fetched or written. The `sites` section of the settings switches handlers off, sets the locale and adds hint lines of the instance's own per host. - `--scope` is for a page whose site handler offers subpages - the stack's today: a PowerShell module's overview page on `learn.microsoft.com`. The handler names them from the page's own HTML: the links it selects that lead to the same host, under the path of the page itself (or the prefix the handler sets); no other link is followed, and a subpage's own links never are. Without `--scope` the one page is fetched as before and a `CHECK` line says how many subpages it offers. With it, `incoming/<stem>/` holds the page and every subpage as a `<name>.html` + `<name>.md` pair - the `.html` the response byte for byte, the `.md` derived as for a single page, `<name>` from the subpage's URL - and `_scope.json`: the URL, the retrieval time and the `sha256` of the `.html` of each page. `raw accept incoming/<stem>` takes the folder as one bundle. - `--scope` is bounded. The `robots.txt` of the host is honoured for the subpages (not for the page itself): one it forbids refuses the whole fetch and names the URL. More subpages than `sites.max_pages` (200) refuses it before the first subpage is requested; `sites.pause_seconds` (1) is the pause before each subpage request. Both are settings of the `instance` tier. Everything is checked before the first write: the path budget under `raw/<YYYY>/<MM>/`, a folder of that name already in `incoming/` or `raw/`. A failing request leaves `incoming/` as it was. - The stem is the URL's last path segment without its extension, else its host name - ASCII, lowercase, `-`-separated, at most 60 characters; `--name` overrides it. - A `text/plain` or `text/markdown` response is stored as received as `.txt`/`.md`; any other non-HTML response (PDF, image, ...) as received under the extension of its content type. Neither gets a derived file or a header. - Only `http`/`https`, on every redirect hop too. 30 s for the whole transfer, 25 MiB at most. No cookies and no JavaScript unless the host is routed or its handler says so (below): a derived text under 200 characters is written, with a warning to read it before accepting. - Fetch routes: the `fetch` section of the settings sends a host's pages another way. `session` fetches them with the cookies of the session the user stored with `raw login`, still with the built-in fetch and the bytes as received; the header adds `session: <host>` and never the session's content. The host's site handler names how a page shows it arrived signed in: no stored session, or a page without that sign (an expired session, a teaser), stops with exit 42 and writes nothing. Any other route names an external fetcher of this checkout: run without a shell with the URL as its last argument (120 s, 25 MiB), its stdout is the `.html` byte for byte, and the header says `fetched_by: wikitool raw fetch (fetcher: <name>)` and carries no `http_status`. A fetcher that fails is never replaced by the built-in fetch. A host no route names is fetched as before. - Rendered pages: a site handler that declares `render` has its pages fetched through a headless browser - with the session, when the host has one - and the `.html` is the page as the browser drew it, not the bytes the server sent; the header says `rendered: true`. No page is rendered without that declaration, and an external fetcher's page never is. It needs the optional browser component; without it the fetch stops with exit 42. `--scope` fetches every page of the bundle the same way, and `_scope.json` names the `session`, `fetcher` or `rendered` it used. - `--html` derives the `.md` beside a page saved to `incoming/` from a logged-in browser - the way past a paywall or a script-rendered page. Its header carries `derived` (when the text was derived) instead of `retrieved`, `final_url`, `http_status` and `content_type`, and the `.html` is left untouched. - Length: a `CHECK` line names every `.md` this run wrote - and a `text/plain` or `text/markdown` response stored as received - that is longer than 50,000 characters, the threshold of `raw derive`: more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). - Success prints the `raw accept` line for the written files and the `source_url` for the source page. **SEE ALSO** - `raw/CONTRACT.md` "Getting a URL in: `raw fetch`" - the rules and why - `sites/CONTRACT.md` - the instance's own handler files - `docs/reference/configuration.md` - the `sites` section, which tunes the site handlers, and the `fetch` section, which routes a host - `wikitool raw login` - the user signs in once for a host routed through the session - `wikitool raw accept` - promotes the written files into `raw/` - `instructions/wiki-ingest/SKILL.md` - where a URL to ingest starts #### `raw login` Let the user sign in to a website once, in a visible browser window, and store the session it leaves as a secret outside the working tree, for `raw fetch` to fetch that website's pages with. Only the user runs it. **SYNOPSIS** - `wikitool raw login <host>` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - the secrets file is replaced in one rename, with every other entry kept - budget: counted - network: yes **EXAMPLES** - `tools/wikitool raw login www.heise.de` **EXIT STATUS** - 0 success - 1 The host is not routed through the session in `fetch.routes`, no enabled site handler of the host names a login page, or the `fetch` settings are malformed - 1 The browser could not be started, or the window closed before any state could be read - 42 The optional browser component is not installed **ON FAILURE** - The host is not routed through the session in `fetch.routes`, no enabled site handler of the host names a login page, or the `fetch` settings are malformed -> Nothing was stored. Route the host (`tools/wikitool config set fetch.routes '{"<host>": "session"}'`), add a `session` block to its handler (`sites/CONTRACT.md`), or fix the section, then retry once - The browser could not be started, or the window closed before any state could be read -> Nothing was stored. Read the browser's message - on Linux it may name system libraries the browser needs, which whoever administers the machine installs - then retry once - The optional browser component is not installed -> Nothing was stored. The user installs it with `tools/preflight.sh --with browser`, then runs this command again **NEVER** - Never run this command as an agent - it waits for a human at a browser window. When `raw fetch` asks for a sign-in, show the user its output and let them run it. - Never read, print or copy the stored session. **NOTES** - Opens the login page the host's site handler names (its `session` block) in a visible browser window and waits until the user has signed in - captcha and second factor included - and closed the window, at most 15 minutes. - Stores only the browser state the window leaves (cookies and storage), never a password, under `session_<host>` (every run of other characters as one `_`) in the secrets file of the instance, outside the working tree and readable by its owner only. `WIKITOOL_SECRET_SESSION_<HOST>` answers before the file, as for any named secret. The session is never written to 1Password. - Needs the host routed through the session first (`fetch.routes`, set with `wikitool config set`), which also gives the instance the id its secrets file is found by. - Needs the optional browser component, which the user installs with the preflight's `--with browser` option. - Prints no part of the session. Signing in again replaces the stored session. **SEE ALSO** - `wikitool raw fetch` - fetches the pages of a routed host with the stored session - `raw/CONTRACT.md` "Getting a URL in: `raw fetch`" - fetching behind a login - `sites/CONTRACT.md` - a handler's `session` block - `docs/reference/configuration.md` - the `fetch` section #### `raw derive` Derive reading text from a PDF, e-mail, Office file or web page in `incoming/`, or unpack a ZIP, and leave the original beside it, for `raw accept` to promote. **SYNOPSIS** - `wikitool raw derive incoming/<file> [--name <stem>]` - Move the file into the new folder `incoming/<stem>/` as `original.<ext>` and write `incoming/<stem>/derived.md` - `wikitool raw derive incoming/<folder>` - Write a `<stem>.md` beside every file in the folder that has a filter **PROPERTIES** - effect: write - idempotent: no - atomic: Yes for what it leaves behind - everything is read, derived and checked before the first write; the files are created exclusively, and a failure part-way puts back what this call changed - budget: counted - network: no **EXAMPLES** - `tools/wikitool raw derive incoming/report.docx` - `tools/wikitool raw derive incoming/Angebot-XYZ.xlsx --name angebot-xyz` - `tools/wikitool raw derive incoming/meeting-files` - `tools/wikitool raw derive incoming/scans.zip` **EXIT STATUS** - 0 success - 1 The path is not directly in `incoming/`, does not exist, or no filter reads the file's extension - 1 The target folder, a `.md` or a workbook's `.sheets` folder already exists, or the path would be over the path budget - 1 The document cannot be read (damaged, encrypted), a container breaks one of its rules (a ZIP inside a ZIP, an unsafe path, a limit) or the filter's package is not installed **ON FAILURE** - The path is not directly in `incoming/`, does not exist, or no filter reads the file's extension -> Nothing was written. Fix the argument and retry once - a file in a subfolder is moved up, or its folder is derived whole - The target folder, a `.md` or a workbook's `.sheets` folder already exists, or the path would be over the path budget -> Nothing was written. Remove what is there or pass `--name <shorter stem>`, then retry once. An existing `.md` is never overwritten - The document cannot be read (damaged, encrypted), a container breaks one of its rules (a ZIP inside a ZIP, an unsafe path, a limit) or the filter's package is not installed -> Nothing was written. A missing package is reported with the preflight hint - show it to the user and stop, never install it. Any other filter keeps working **NEVER** - Never edit `derived.md` or its header by hand. - Never derive a file the user did not put into `incoming/` for this purpose. **NOTES** - Writes into `incoming/` only, never into `raw/`, and reads no network: `raw accept` promotes the folder afterwards as one source under `raw/<YYYY>/<MM>/<stem>/`. - A single file directly in `incoming/` becomes a folder named after its stem (or `--name`) holding `original.<ext>` - the original bytes, moved - and `derived.md`. The fixed names keep the path inside the path budget; the header of `derived.md` records the file's own name as `original_name`. - A folder is derived in place: each file with a filter gets `<stem>.md` beside it, nothing is renamed, and the files without a filter are named in the output. - `derived.md` starts with a fixed header (`derived_by`, `filter`, `engine`, `derived`, `derived_from`, `original_name` for a renamed file, `title`, then the fields of the format) followed by the derived text. The same bytes always give the same text; only `derived` differs between runs. `engine` names the tool version and, where a package reads the format, its version. - Formats: web pages (`.html`), Word (`.docx`), PowerPoint (`.pptx`), Excel (`.xlsx`, `.xlsm`), e-mail (`.eml`) and ZIP archives (`.zip`), read with the standard library only; PDF (`.pdf`, `pypdf` and `pdfminer.six`) and Outlook messages (`.msg`, `olefile`), each read by packages in `tools/requirements.txt` that are imported only when such a file is derived. A missing package refuses that format alone, with the preflight hint; `wikitool doctor` shows one `import-filter-<name>` line per package-backed format. A PDF gets `<!-- page N -->` markers and a WARN when it yields almost no text (a scan; there is no OCR); an encrypted PDF is refused. A `.msg` is a container like an `.eml`; an embedded message inside it is named, not read. The text of a mail (`.eml` and `.msg` alike) is its plain text, unless that is under 10% of the length of the HTML text - a "plain text version not available" placeholder - in which case it is the HTML text and a WARN names both lengths. The other version of the text in a `multipart/alternative` is not an attachment. - A PDF also gets, where the document has them: an `outline` header field (one entry per outline item: title, level, page and the line of `derived.md` where that page's marker is), and markings in the text that follow the visible form only, in plain Markdown: `~~…~~` for strike-through, `**…**` for a highlighter stroke, bold type or a colour clearly unlike the running text (not under a link), `*…*` for italic. Bold, italic and colour count in running text only, never in program text, a table's grid or a shape the document repeats on its pages. A `markings` header field is the legend (at most three lines: form, places, pages; the `**` line names the highlighter colours); removing the markings gives the text the PDF has without them, and a tag never starts or ends on whitespace. A CHECK names the pages with a picture or drawing the text does not contain - not a background, a filler or a picture repeated across the document - and an `images` header field names the same pages, so that they outlast the output. An `unmatched` header field names the pages a WARN reports as left without markings because their text did not match the page layout. Both are written only when not empty, as page ranges (`6-8, 10`). A PDF without outline, markings, images and unmatched pages is derived as before. - A workbook (`.xlsx`, `.xlsm`) gets its cells as files: beside its `.md` a folder `<md stem>.sheets/` (`derived.sheets/` for a single file) holds one `NN-<sheet>.tsv` per sheet - tab-separated per RFC 4180, UTF-8, row 1 of the file is row 1 of the sheet - and, for a sheet with formulas, `NN-<sheet>.formulas.tsv` listing cell, formula and last stored value. The `.md` is the overview: one section per sheet with its range, the file names and a preview. Formulas are not recalculated; a shared formula is written out per cell, and one that cannot be is named `shared:<master cell>` with a WARN. The folder counts against the path budget and is accepted with the rest; an existing one is never overwritten. - A ZIP and a mail are containers. A ZIP at the top of `incoming/` becomes the folder `incoming/<stem>/` holding its files under their own names, each with a `<stem>.md` where a filter reads it; the ZIP is removed and the output names it with its sha256. A mail becomes a folder with `original.eml`, `derived.md` and its attachments under their own names; a ZIP attached to it is unpacked once into a folder of its own, and an attached mail is quoted in `derived.md`, not unpacked. A ZIP inside a ZIP, an absolute or `..` path, a symbolic link, an encrypted entry, more than 1000 entries, a file over 25 MiB or more than 250 MiB in total refuse the whole call. Hidden entries (`__MACOSX/`, `.DS_Store`, anything starting with a dot) are named, not written. A name taken twice, or reserved (`original.*` and `derived.md` at the top of a mail's folder, `_capture.json` anywhere), gets `-2`, `-3` before its extension, and the output lists each rename. - A name already taken under `raw/` is a WARN, not a refusal: whether the folder is a later edition of that source or a separate one is the human's call. A later edition of a derived source is replaced file by file with `raw accept --replaces`; there is no `--replaces-bundle` for it. - After a success the output prints the warnings the filters raised, at most three `CHECK` lines per format that was derived in this run - what to look at before the text is trusted - and the `raw accept` line for the result. - Length: one more `CHECK` line names every `.md` this run wrote that is longer than 50,000 characters, counted in the file as written, header included - more than one ingest reads in one pass, so it is read in parts (`instructions/ingest-large-tree.md`). Characters, not tokens: they do not depend on a model's tokenizer. `raw fetch` and `raw pending` measure against the same value. - Never edit `derived.md` by hand: a better derivation is a new `raw derive` of the original. **SEE ALSO** - `raw/CONTRACT.md` "Getting a document in: `raw derive`" - the rules and why - `wikitool raw accept` - promotes the folder into `raw/` - `instructions/wiki-ingest/SKILL.md` - where a PDF, mail or Office file to ingest starts #### `raw capture` Capture documentation from a git repository into `incoming/<bundle>/`, with a manifest naming the repository, ref rule and commit, for `raw accept` to promote. **SYNOPSIS** - `wikitool raw capture <repo-url> --ref <branch|tag-pattern> --path <glob> [--path <glob> ...] --name <bundle> --fidelity <v> --authority <v>` - First capture of a repository - `wikitool raw capture --update <raw-bundle> [--fidelity <v>] [--authority <v>]` - Capture the current state of an already-accepted bundle, for `raw accept --replaces-bundle` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes for what it leaves in `incoming/` - the bundle is written into a hidden staging directory and renamed into place in one step; a failure removes the staging directory. The git cache under `tools/.wikitool_capture/` is updated either way - budget: counted - network: yes **EXAMPLES** - `tools/wikitool raw capture ssh://git@example.org/team/service.git --ref main --path 'docs/**/*.md' --path README.md --name service-docs --fidelity verbatim --authority normative` - `tools/wikitool raw capture https://example.org/team/lib.git --ref 'v*' --path docs --name lib-docs --fidelity verbatim --authority reporting` - `tools/wikitool raw capture --update raw/2026/10/service-docs` **EXIT STATUS** - 0 success - 1 The URL is not `ssh://`, `https://` or `user@host:path`, or carries a password; the `--ref` rule, a `--path` glob or `--name` is unusable; or an argument of the other variant was mixed in - 1 `--fidelity`/`--authority` is missing on a first capture, or names `unknown` or a value outside the schema's enum - 1 The repository could not be reached, asked for credentials, or timed out; or no branch or tag matches `--ref` - 1 `incoming/<bundle>` already exists, the name is already taken under `raw/`, nothing matches the globs, or a path would be over the budget - 1 raw capture --update: The path is not a bundle under `raw/` with a readable `_capture.json`, or its manifest names a URL that is refused **ON FAILURE** - The URL is not `ssh://`, `https://` or `user@host:path`, or carries a password; the `--ref` rule, a `--path` glob or `--name` is unusable; or an argument of the other variant was mixed in -> Fix the call and retry once. A credential goes into git's credential helper, never into the URL - `--fidelity`/`--authority` is missing on a first capture, or names `unknown` or a value outside the schema's enum -> Pass both with a valid value, then retry once - The repository could not be reached, asked for credentials, or timed out; or no branch or tag matches `--ref` -> Nothing was written to `incoming/`. Check the URL, the ref rule and the host's git credentials with the user; an unreachable host may be retried once - `incoming/<bundle>` already exists, the name is already taken under `raw/`, nothing matches the globs, or a path would be over the budget -> Nothing was written. Accept or remove what is in `incoming/`; for a name taken by a captured bundle use `--update`; widen or narrow the globs; then retry once - raw capture --update: The path is not a bundle under `raw/` with a readable `_capture.json`, or its manifest names a URL that is refused -> Show the message to the user - a manifest under `raw/` is never edited by hand to make this pass **NEVER** - Never put a password or token into the repository URL. - Never edit a `_capture.json`, or the files of a captured bundle, by hand. - Never capture a repository the user has not named in this session. **NOTES** - Writes into `incoming/<bundle>/` only, never into `raw/`: `raw accept incoming/<bundle>` promotes it, as one folder, to `raw/<YYYY>/<MM>/<bundle>/`. - `--ref` is a branch name (`main`) or a tag pattern (`v*`, any of `*?[`), which names the newest matching tag by version order. It resolves to one commit - for an annotated tag, the commit it points at. - The commit is fetched by ref name, shallowly, into a bare cache repository per URL under `tools/.wikitool_capture/` (gitignored). Files are read from it as blobs, never through a checkout, so they are byte-identical to the repository's - no line-ending conversion, no filter. - `--path` globs (repeatable) select files by their path in the repository, as git's `:(glob)` pathspec does: `*`, `?` and `[...]` do not cross `/`; `**` as a whole segment spans any number of directories; a glob without wildcards also takes everything below it as a directory. Files keep their repository paths inside the bundle. - Never captured, each named in the output with its reason: a file whose first line starts with `<!-- wikitool:export` or a leading frontmatter block with a `wikitool_export:` key, a symlink, a submodule, a file over 25 MiB, a path with a segment starting with `.`, a file named `_capture.json`, and a Git LFS pointer. - `incoming/<bundle>/_capture.json` records `schema`, `repo`, `ref`, `commit`, `paths`, `captured` (UTC), `fidelity`, `authority` and `files`. It is the only declaration that and how this instance follows the repository. - Only `ssh://`, `https://` and the scp form `user@host:path`; a URL with a password or token in it is refused, since the manifest is committed. Git runs with the host's own credentials and configuration, restricted to those protocols, and never prompts: a repository that asks for credentials is reported as unreachable. Each git call times out after 120 s. - `--update <raw-bundle>` takes URL, ref rule and globs from that bundle's manifest and writes the current state to `incoming/<same bundle name>/`; `--fidelity`/`--authority` default to the manifest's values, and passing one is the way to correct it. - Refused before anything is written: `incoming/<bundle>` already exists, the name is already taken under `raw/` (a first capture), nothing matches the globs, or a target path - measured at its later place under `raw/` - is over the path budget. - Success prints the commit and the `raw accept` line that comes next. **SEE ALSO** - `raw/CONTRACT.md` "Getting a repository in: `raw capture`" - the rules and why - `wikitool raw accept` - promotes the bundle; `--replaces-bundle` for a new edition - `wikitool raw status` - which captured bundles have fallen behind their repository #### `raw status` Report which captured bundles under `raw/` have fallen behind their repository, with each changed file as `A`/`M`/`D`. **SYNOPSIS** - `wikitool raw status [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only for the tree - writes nothing but the git cache under `tools/.wikitool_capture/` - budget: counted - network: yes **EXAMPLES** - `tools/wikitool raw status` - `tools/wikitool raw status --json` **EXIT STATUS** - 0 success - 0 A repository could not be reached, its manifest or URL was refused, or no ref matches its rule **ON FAILURE** - A repository could not be reached, its manifest or URL was refused, or no ref matches its rule -> Reported on that bundle's line, the others are still checked; check the URL and the host's git credentials with the user **NEVER** - Never edit a `_capture.json` to silence a line - a new edition goes through `raw capture --update` and `raw accept --replaces-bundle`. **NOTES** - Reads every `_capture.json` under `raw/` and resolves its ref rule with `git ls-remote`. Only where the commit moved is the new commit fetched, and compared - with the same globs and exclusions `raw capture` applies - against the bundle's files under `raw/`. - A bundle whose commit moved without a change inside its globs is reported as unchanged, not listed. A tag pattern follows new matching tags only, never new commits on a branch. - Each changed bundle prints `old -> new` and its files as `A`/`M`/`D`, grouped by owning source page with its citing pages, followed by the `raw capture --update` and `raw accept --replaces-bundle` lines that take the new edition in. - An unreachable repository, a refused URL or an unreadable manifest is one line beside the others, never an abort; the exit status is 0 as long as the command itself ran. - `--json` prints one object per bundle: `bundle`, `repo`, `ref`, `old`, `new`, `changed`, `files` (`[{path, status}]`), `error` (or null). - Git runs exactly as for `raw capture`: the host's credentials, only `ssh`/`https`, never a prompt, 120 s per call. **SEE ALSO** - `wikitool raw capture` - `--update` captures the new edition - `wikitool raw accept` - `--replaces-bundle` takes it in - `raw/CONTRACT.md` "Getting a repository in: `raw capture`" #### `raw pending` List what waits in `incoming/`, oldest first, and name the entry an ingest without an argument takes next. **SYNOPSIS** - `wikitool raw pending [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool raw pending` - `tools/wikitool raw pending --json` **EXIT STATUS** - 0 success **NOTES** - A candidate is a top-level entry of `incoming/`: a single `file`, a `bundle` of top-level files sharing a stem (a `raw fetch` pair), or a `folder` with every file below it. Dotfiles, empty directories and their contents are none; `quarantine/` is outside `incoming/` and never listed. - Order: oldest first by modification time. A bundle or folder counts as new as its newest file; a tie goes by name. The mtime is when a document last changed only if it was copied with its timestamps kept (`cp -p`, `rsync -a`, an unpacked archive) - for a download or a `raw fetch` it is merely when it was dropped. - Each candidate shows its path(s), kind, file count and mtime, and whether `raw accept` would take it as it stands - the same checks, minus `--fidelity`/`--authority`. One it would refuse is listed with the reason and skipped: it needs a human. - The default is the first candidate `raw accept` would take, and the output names it. - `--json` prints the same candidates in the same order: `kind`, `paths`, `files`, `mtime`, `acceptable`, `reason`, `default`, `hint`, `length`. - A lone file that a `raw derive` filter reads (a PDF, mail, Office file or web page) is listed with the `raw derive` line for it, in text output and as `hint` in JSON (`null` for every other candidate). The hint changes nothing about the order or the default. - A lone `.md` or `.txt` file longer than the length threshold of `raw derive` and `raw fetch` is listed with its length in characters, in text output and as `length` in JSON (`null` for every other candidate) - that text reaches a session with no derivation in front of it to print the length `CHECK`. Like the hint, it changes nothing about the order or the default. - Reads directory listings and `lstat`, and a file's content only to measure a lone `.md`/`.txt` whose size in bytes is over the threshold - a UTF-8 text has no more characters than bytes; an empty `incoming/` is exit 0 with nothing to do. **SEE ALSO** - `wikitool raw accept` - promotes the chosen candidate - `instructions/wiki-ingest/SKILL.md` - ingest without an argument starts here - `raw/CONTRACT.md` "Getting a file in: incoming/" - candidates and order, and why #### `raw accept` Promote one or more files, or one folder, from `incoming/` into `raw/`. **SYNOPSIS** - `wikitool raw accept <file> [<file> ...] --fidelity <v> --authority <v> [--page "<Title>"] [--dry-run]` - Promote one or more files from `incoming/` into today's `raw/<YYYY>/<MM>/` shard - `wikitool raw accept incoming/<folder> --fidelity <v> --authority <v> [--dry-run]` - Promote a whole folder as one source, its structure kept, into `raw/<YYYY>/<MM>/<folder>/` - `wikitool raw accept <file> --replaces <raw-path> [--fidelity <v>] [--authority <v>] [--dry-run]` - Overwrite one existing raw file in place with a new edition - `wikitool raw accept incoming/<bundle> --replaces-bundle <raw-bundle> [--dry-run]` - Replace a captured bundle as a whole with the new edition `raw capture --update` wrote **PROPERTIES** - effect: write - idempotent: no - atomic: `raw accept`: No - one filesystem move per file, then (with `--page`) one page write. With a folder: No - one move per file, then one `rmdir` per emptied directory; a half-accepted folder is not resumed. `raw accept --replaces`: No - one `unlink()` + one `rename()`, plus (if `--fidelity`/`--authority` was given) one page write. `raw accept --replaces-bundle`: No - one `unlink()` per removed file, one `rename()` per added or modified file, then the manifest, then one `rmdir` per emptied directory, plus one page write per owning page whose capture fields change; a half-replaced bundle is not resumed - budget: counted - network: no **EXAMPLES** - `tools/wikitool raw accept incoming/docker-cheatsheet.md --fidelity verbatim --authority reporting` - `tools/wikitool raw accept incoming/part-2.md --fidelity verbatim --authority reporting --page "Source - Docker Cheatsheet"` - `tools/wikitool raw accept incoming/projekt-x --fidelity verbatim --authority reporting` - `tools/wikitool raw accept incoming/cluster.md --replaces raw/documents/cluster.md` - `tools/wikitool raw accept incoming/chemenu --replaces-bundle raw/2026/10/chemenu` **EXIT STATUS** - 0 success - 1 raw accept: A file does not exist or is not directly in `incoming/`; two files in one call share a filename; a target path already exists; or a target path would be over the path budget (160 UTF-16 code units below the instance root) - 1 raw accept incoming/<folder>: The folder is not directly in `incoming/`, is combined with another argument, `--page` or `--replaces`, holds no file, or holds a hidden entry, a symlink or a special file; a target path is over the budget; or the folder name is already occupied under `raw/` - 1 A file - given directly, inside a folder, or as `--replaces`/`--replaces-bundle` material - is an export of this wiki: it carries the export marker (a first line starting with `<!-- wikitool:export`, or a `wikitool_export:` key in a leading frontmatter block), or it is byte for byte a file in the manifest of a configured `export` target - 1 raw accept: `--fidelity`/`--authority` is missing, or names `unknown` or a value outside the schema's enum - 1 raw accept: The target name is already occupied anywhere under `raw/` by something the call does not own - 1 raw accept: `--page` names an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is missing on disk, a file to be moved has more than one owning page, or `--page` would overwrite an already-set `fidelity`/`authority` with a different value - 1 raw accept --replaces: More than one incoming file, or `--page` also given - 1 raw accept --replaces: The incoming file does not exist or is not directly in `incoming/`, its filename differs from the target's, or the target does not lie under `raw/` or does not exist - 1 raw accept --replaces: `--fidelity`/`--authority` names `unknown` or a value outside the schema's enum, or the target has more than one owning source page - 1 raw accept --replaces / --page: The target, or a file the page already has, lies in a captured bundle - 1 raw accept incoming/<bundle>: A captured folder is given `--fidelity`/`--authority`, its `_capture.json` cannot be read or names an invalid value, or its files differ from the ones the manifest lists - 1 raw accept --replaces-bundle: Either side has no `_capture.json`, the two manifests name different repositories, the folder's name differs from the bundle's, `--page`, `--replaces`, `--fidelity` or `--authority` was also given, the folder does not match its manifest, a new path is over the budget, or a file of the bundle has more than one owning source page **ON FAILURE** - raw accept: A file does not exist or is not directly in `incoming/`; two files in one call share a filename; a target path already exists; or a target path would be over the path budget (160 UTF-16 code units below the instance root) -> Fix the named argument and retry once. A file inside a subdirectory of `incoming/` is accepted with its whole folder (`raw accept incoming/<folder>`) or moved up into `incoming/` first. For a path over the budget, rename the file in `incoming/` to something shorter - the refusal comes before anything moves, so `incoming/` and `raw/` are unchanged - raw accept incoming/<folder>: The folder is not directly in `incoming/`, is combined with another argument, `--page` or `--replaces`, holds no file, or holds a hidden entry, a symlink or a special file; a target path is over the budget; or the folder name is already occupied under `raw/` -> Nothing moved. Fix what the message names and retry once. For an occupied name held by a captured bundle, the folder is its new edition: `raw accept incoming/<bundle> --replaces-bundle <raw-bundle>`. Any other occupied name: rename the folder in `incoming/` - such a folder has no replacement form - A file - given directly, inside a folder, or as `--replaces`/`--replaces-bundle` material - is an export of this wiki: it carries the export marker (a first line starting with `<!-- wikitool:export`, or a `wikitool_export:` key in a leading frontmatter block), or it is byte for byte a file in the manifest of a configured `export` target -> Not fixed by retrying: it is generated from a wiki's `kb/` and never goes back into `raw/`. Remove it from `incoming/`; nothing was moved - raw accept: `--fidelity`/`--authority` is missing, or names `unknown` or a value outside the schema's enum -> Pass both with a valid value, then retry once - raw accept: The target name is already occupied anywhere under `raw/` by something the call does not own -> Not fixed by retrying: the refusal names `--replaces` (same source, new edition) and renaming in `incoming/` (a separate source) as the two routes, and neither is the tool's to pick. Show the message to the user and wait - raw accept: `--page` names an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is missing on disk, a file to be moved has more than one owning page, or `--page` would overwrite an already-set `fidelity`/`authority` with a different value -> Fix the named argument and retry once; a different capture value on an existing page is a new edition - `--replaces` - raw accept --replaces: More than one incoming file, or `--page` also given -> A replacement is one file for one file - fix the call and retry once - raw accept --replaces: The incoming file does not exist or is not directly in `incoming/`, its filename differs from the target's, or the target does not lie under `raw/` or does not exist -> Fix the named argument and retry once - every check runs before the filesystem is touched, so both files are exactly as they were - raw accept --replaces: `--fidelity`/`--authority` names `unknown` or a value outside the schema's enum, or the target has more than one owning source page -> Fix the named argument and retry once; nothing was touched - raw accept --replaces / --page: The target, or a file the page already has, lies in a captured bundle -> Not fixed by retrying: a captured bundle changes only as a whole - `raw capture --update <raw-bundle>`, then `raw accept --replaces-bundle` - raw accept incoming/<bundle>: A captured folder is given `--fidelity`/`--authority`, its `_capture.json` cannot be read or names an invalid value, or its files differ from the ones the manifest lists -> Nothing moved. Drop the two flags and retry once; a manifest that does not match its folder is fixed by removing the folder and capturing it again, never by editing either - raw accept --replaces-bundle: Either side has no `_capture.json`, the two manifests name different repositories, the folder's name differs from the bundle's, `--page`, `--replaces`, `--fidelity` or `--authority` was also given, the folder does not match its manifest, a new path is over the budget, or a file of the bundle has more than one owning source page -> Nothing under `raw/` or in `incoming/` changed. Fix what the message names and retry once; different repositories or a different name are a separate source, not a new edition - show the message to the user **NEVER** - Never choose the destination under `raw/` by hand, and never move a file into `raw/` yourself. - Never pick between `--replaces` and renaming on your own initiative after a name-occupied refusal - the user tells the two intents apart. **NOTES** - Promotes files from `incoming/` into `raw/<YYYY>/<MM>/`, computed from the accept date rather than chosen by hand. A file argument must sit directly in `incoming/`; a file inside a subdirectory is refused, since a subdirectory is a source of its own. - A folder argument (`incoming/<folder>`) is one source: every file below it moves to `raw/<YYYY>/<MM>/<folder>/` at the same relative path, and the directories left empty are removed - `incoming/<folder>` no longer exists afterwards. The folder name is the bundle name; two `README.md` in different subfolders are no conflict. - A folder is accepted alone - no other argument, no `--page`, no `--replaces` - with one `--fidelity`/`--authority` pair for all of it. It is refused, before anything moves, if it is empty, or if a hidden entry (name starting with `.`), a symlink or a special file sits anywhere below it; the refusal names each one. - One file promoted alone lands with no directory of its own; several files in one call nest under `raw/<YYYY>/<MM>/<stem>/`, named after the first file's stem. - `--fidelity`/`--authority` are required on a plain accept (`types describe source` lists the values); `unknown` is refused - it is backfill-only. - `--page "<Title>"` additionally extends that existing source page's `raw_files:` in the same call and writes both capture fields onto it - refused if it already carries a different value, since a capture field is fixed once. - If `--page` raises the page past one file, its already-promoted file is folded into a bundle at *its own* parent directory, not today's shard, so a bundle never mixes an old and a new capture date - after checking that file has no other owner. - Every name occupied anywhere under `raw/` - file stems and bundle directory names alike, old type directories and date shards together - stays unique: a promote whose target name already belongs to something this call does not itself own is refused, naming `--replaces` and renaming in `incoming/` as the two routes, without recommending either. - `--replaces <raw-path>` overwrites that file in place with the single incoming file (same filename required), leaves every page's `raw_files:` untouched and writes no `kb/` page; the previous edition survives only in `git log --follow <raw-path>`. - With `--replaces`, `--fidelity`/`--authority` are optional, and passing one overwrites the owning page's already-set value - the one path the fixed-once rule does not block. - `--replaces` refuses a target with more than one owning source page; with none, it replaces anyway and says so. It cannot be combined with `--page` or with more than one incoming file. - `--replaces` prints the source page (if any) and its citing pages, so their update lands in the same commit as the replacement. - A file already at its computed destination is what "already exists" reports, not a partial prior run to resume - safe to retry as-is once a cause is fixed. - A folder carrying `_capture.json` at its top is a captured bundle (`raw capture`): `--fidelity`/`--authority` come from that manifest only and are refused on the command line; the folder must hold exactly the files the manifest lists. `_capture.json` goes in no `raw_files:`. - `--replaces-bundle <raw-bundle>` replaces a captured bundle under `raw/` with the new edition in `incoming/<bundle>` at the same address: files gone from the new edition are removed, emptied directories `rmdir`ed, and afterwards the bundle holds exactly the new manifest's files plus `_capture.json`. `raw_files:` is left untouched; an owning source page's `fidelity`/`authority` is overwritten where the new manifest differs. - `--replaces-bundle` prints each changed file as `A`/`M`/`D`, grouped by owning source page with its citing pages, and the `touch --page ... --add/--remove raw_files=` lines that follow; `git diff` on the bundle shows the `M` and `D` files, the `A` files appear only in that list because they are still untracked. - `--replaces` and `--page` refuse a target inside a captured bundle; the refusal names `raw capture --update` and `--replaces-bundle`. A file named `_capture.json` anywhere but at a captured folder's top is refused - the name is reserved. - `--dry-run` reports the moves without making them. **SEE ALSO** - `raw/CONTRACT.md` "Getting a file in: incoming/" - the rules and why - `wikitool raw pending` - what is waiting in `incoming/`, and which entry is next - `wikitool raw capture` - writes a captured bundle, or its new edition, into `incoming/` - `wikitool types describe source` - the capture field values - `wikitool new source` - the source page for a promoted file #### `upload collect` Pull material from the configured `folder` and `command` intake channels into the quarantine - never past it. **SYNOPSIS** - `wikitool upload collect [<channel>...]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - per file: one copy into `quarantine/<id>/` (manifest last), then one removal of the source; a channel's failure does not stop the others - budget: counted - network: no **EXAMPLES** - `tools/wikitool upload collect` - `tools/wikitool upload collect onedrive` **EXIT STATUS** - 0 success - 1 Unknown channel name, an `mcp` channel as argument, a malformed `intake` section, or an `mcp-upload/` that still holds submissions - 1 A channel or a file failed (folder missing, a folder in backup or export data, program exited non-zero, timed out or could not be started, a source that could not be removed) **ON FAILURE** - Unknown channel name, an `mcp` channel as argument, a malformed `intake` section, or an `mcp-upload/` that still holds submissions -> Fix the named cause and retry once - A channel or a file failed (folder missing, a folder in backup or export data, program exited non-zero, timed out or could not be started, a source that could not be removed) -> Not fixed by retrying unchanged - read the per-channel lines, fix the channel's folder or program, then run `collect <channel>` for that channel only. A folder in backup or export data is the user's to move; never remove a manifest to make it pass **NEVER** - Never follow `collect` with `upload accept` and a `--confirm` token the user has not seen and approved. **NOTES** - Without arguments, collects from every channel of the `intake` section whose handler is `folder` or `command`; with names, from those channels only. A name no channel has, or the name of the `mcp` channel (which is pushed, not pulled), exits 1 before anything happens. - Everything a channel delivers lands in `quarantine/<id>/` unchanged, with a manifest that names `channel`, `handler` and `origin`; the submitter is `unknown`. Nothing reaches `incoming/` or `raw/` - that is `upload accept`, behind its gate, and an agent may run `collect` because collecting gives no trust. - `folder`: only the top level of `path`. A file is taken once its modification time and, where the platform has it, its creation time are older than `settle_seconds`, its extension and size fit, and its name passes the filename rules. It is removed from the folder only after its copy is fully written and the source still has the size and sha256 that were read; if it changed meanwhile, nothing is kept and it stays. A file whose sha256 already waits in the quarantine is reported as a duplicate and removed from the folder, because its content is kept. Subdirectories, hidden files (names starting with `.` or `~$`, or the hidden or system attribute on Windows), symbolic links, other extensions and oversized files are left untouched and reported. - `command`: the program of `commands.<name>` runs without a shell, with stdin closed, a 300 s timeout and an empty staging directory under `quarantine/.staging/` as its last argument. On exit 0 each regular file at the top of that directory with an allowed extension and size becomes a submission; on anything else nothing is taken and the end of stderr is printed. The staging directory is always removed. - `folder` never empties backup or export data: a `path` that holds a `.chemenu-backup.json` (a machine's folder of the backup, any machine's) or a `.wikitool-export.json` (an export), is a bare git repository, or lies in one of these, is that channel's error - nothing is read and nothing removed. A `path` above such folders is fine, since only its top level is read. - Channels are independent: when one fails the others still run, and the exit status is 1 if any channel or file failed. With no pulled channel it prints `nothing to collect` and exits 0. - Exits 1, before it does anything, while an `mcp-upload/` of an earlier version still holds submissions, and when the `intake` section is malformed. - Not idempotent: a second run finds the folder emptied. **SEE ALSO** - `wikitool upload list` - what waits for review afterwards - `wikitool upload accept` - the gate out of the quarantine - `docs/how-to/collect-from-a-synced-folder.md` #### `upload list` List every submission currently waiting in the quarantine (`quarantine/`), whatever channel it came through. **SYNOPSIS** - `wikitool upload list [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool upload list` - `tools/wikitool upload list --json` **EXIT STATUS** - 0 success **NOTES** - Lists every submission waiting in `quarantine/`, oldest id first: id, filename, size, channel, submitter. - Only ever non-empty when the `intake` section of `.wikitool-local.json` declares a channel. - Exits 1, before it does anything, while an `mcp-upload/` of an earlier version still holds submissions - the message names the command that moves it. - A submission directory with a corrupt manifest is silently skipped. - Never fails; read-only and safe to retry freely. **SEE ALSO** - `wikitool upload show` - one submission's manifest - `docs/how-to/set-up-the-mcp-server.md` § "Optional: the submit path" #### `upload show` Print one submission's manifest in full. **SYNOPSIS** - `wikitool upload show <id> [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool upload show <id>` **EXIT STATUS** - 0 success - 1 Unknown or malformed submission id **ON FAILURE** - Unknown or malformed submission id -> Fix the id (see `upload list`) and retry **NOTES** - Prints one submission's manifest in full: filename, size, sha256, submitter, submitter source (the header name or the handler, not a claim the header was honest), submission time, and where it came from: channel, handler, origin (the source file's path, `command:<name>` or `mcp:submit`). - A submitter of `unknown` means the channel has no sender to trust - read channel and origin instead. - What a reviewer reads before `upload accept`. - Read-only. **SEE ALSO** - `wikitool upload accept` - promotes it after review - `wikitool upload reject` - declines it #### `upload accept` **Upload Review Gate:** promote a submission's file from quarantine into `incoming/`. **SYNOPSIS** - `wikitool upload accept <id> [--confirm TOKEN]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - one filesystem move, one directory delete, one ledger append; the gate check runs first, before any of them - budget: counted - network: no - gates: upload-review **EXAMPLES** - `tools/wikitool upload accept <id>` - `tools/wikitool upload accept <id> --confirm <token> # re-run after exit 42, once the user approved the manifest` **EXIT STATUS** - 0 success - 1 Unknown or malformed submission id, or the submission's file is missing from `quarantine/<id>/` - 1 `incoming/<filename>` already exists - 42 Upload Review Gate: `--confirm` is absent or does not match the manifest's current token **ON FAILURE** - Unknown or malformed submission id, or the submission's file is missing from `quarantine/<id>/` -> Fix the named argument and retry once - `incoming/<filename>` already exists -> Not fixed by retrying unchanged - rename or clear it first - Upload Review Gate: `--confirm` is absent or does not match the manifest's current token -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state **NEVER** - Never pass a `--confirm` token the user has not seen and approved. **NOTES** - Promotes a submission's file from `quarantine/<id>/` into `incoming/`, deletes the quarantine directory, and appends an `accepted` event to `quarantine/ledger.jsonl`. - Upload Review Gate: without a matching `--confirm`, exits 42 and prints the manifest in full plus the exact re-run line - one submission at a time. The gate check runs before anything is moved. - The token digests id, filename, size, sha256 and submitter, so an edited or superseded manifest invalidates it. - An occupied `incoming/<filename>` is an ordinary validation error, not the gate. **SEE ALSO** - `wikitool upload show` - the manifest to review - `wikitool raw accept` - the next step for the file in `incoming/` - `instructions/gates.md` - the gate procedure #### `upload reject` Delete a submission's material, keeping only its ledger trail. **SYNOPSIS** - `wikitool upload reject <id> --reason "<why>"` **PROPERTIES** - effect: write - idempotent: no - atomic: No - one ledger append, then one recursive delete; the ledger write happens first, so an interruption still leaves the reason on record - budget: counted - network: no **EXAMPLES** - `tools/wikitool upload reject <id> --reason "duplicate of raw/articles/llm-wiki.md"` **EXIT STATUS** - 0 success - 1 Unknown or malformed submission id, or an empty `--reason` **ON FAILURE** - Unknown or malformed submission id, or an empty `--reason` -> Fix the argument and retry once. After a first successful call, "unknown id" is confirmation, not a failure **NOTES** - Appends an append-only `rejected` event naming the reason and the sha256 of what was declined, then deletes the submission's material. - The ledger write happens first, so an interruption still leaves the reason on record. - No gate - rejecting needs no clearance, only accepting does. - Not idempotent: a second call with the same id reports "unknown id", which confirms the first call worked. **SEE ALSO** - `wikitool upload show` - the manifest to review ### Git #### `sync` Fetch `<remote>/<branch>` and bring the local branch up to date with it. **SYNOPSIS** - `wikitool sync [--remote origin] [--branch main] [--confirm-rebase TOKEN]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - fetch, then at most one merge/rebase attempt, aborted cleanly on failure with the working tree restored; a success that merged generated files then rewrites the catalog - budget: counted - network: yes - gates: rebase-review **EXAMPLES** - `tools/wikitool sync` - `tools/wikitool sync --confirm-rebase <token> # re-run after exit 42, once the user approved` **EXIT STATUS** - 0 success - 0 No remote configured - reported and skipped, not a failure - 0 The remote cannot be reached - reported and skipped, not a failure - 1 The automatic rebase hit a real conflict outside the generated files (git failed), or `kb/log.md` was edited rather than appended to; it is aborted cleanly and the working tree restored - 1 An uncommitted change - or an untracked file - sits on a path the incoming commits also change; nothing was changed - 42 Rebase-Review Gate: `<remote>/<branch>` moved and both sides changed the same file, generated files aside; the output lists the upstream commits, the overlapping files and their diff **ON FAILURE** - The automatic rebase hit a real conflict outside the generated files (git failed), or `kb/log.md` was edited rather than appended to; it is aborted cleanly and the working tree restored -> Do not retry and do not force - resolve the conflict manually, then re-run - An uncommitted change - or an untracked file - sits on a path the incoming commits also change; nothing was changed -> Show the user the file it names. Once that change is committed or moved out of the way, re-run once - Rebase-Review Gate: `<remote>/<branch>` moved and both sides changed the same file, generated files aside; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state **NEVER** - Never retry a conflict unchanged, and never force past it. - Never pass a `--confirm-rebase` token the user has not seen and approved. **NOTES** - Fetches `<remote>/<branch>`, then: fast-forwards when only the remote moved; rebases the local commits on top when both sides moved but touched disjoint files; exits 42 (Rebase-Review Gate) when both sides touched the same file. - Files `wikitool` generates (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `kb/**/INDEX.md`) never count as an overlap and never reach the gate. Where they are what stops git - uncommitted ones in the way of a fast-forward or rebase, or a rebase conflict in them - they are merged mechanically: `kb/log.md` becomes the incoming log plus the entries only this side appended, and the catalog and `kb/provenance.md` are regenerated from the merged pages and left as an uncommitted change. A reconcile git can do on its own is unchanged and regenerates nothing. - Uncommitted changes that no incoming commit touches do not stop a rebase: they are carried through it (`git rebase --autostash`) and come back unchanged. - Before anything is set aside, the working tree is stored as a commit under `refs/wikitool/reconcile-backup`. A successful reconcile drops it; a failed one restores the working tree and names the ref. - A refused call performs no rebase attempt and leaves the branch where it was. - The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides, generated files excluded; either one moving makes it stale. - Makes no commit, no push, and no forced operation of any kind. - Three messages for a fetch that fails: no remote of that name, a remote that answers but has no such branch yet (a new, empty repository), and a remote that cannot be reached. All three exit 0 here; `publish` stops on the first and the last. - Run it once at the start of a writing session. **SEE ALSO** - `wikitool publish` - runs the same reconcile before it commits and pushes - `instructions/session-setup.md` - where a session runs `sync` - `instructions/gates.md` - the gate procedure #### `publish` Reconcile with `<remote>/<branch>`, then stage all changes, commit, and push. **SYNOPSIS** - `wikitool publish --message "<op>: <desc>" [--no-push] [--confirm TOKEN] [--confirm-rebase TOKEN] [--threshold N] [--remote origin] [--branch main] [--path P ...]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - sequential git operations. Every gate runs before staging, except on the one retry of a rejected push, where the Rebase-Review Gate can exit 42 after the commit: nothing is pushed, and the re-run with `--confirm-rebase` pushes that commit - budget: counted - network: yes - gates: mass-update, publish-remote, rebase-review **EXAMPLES** - `tools/wikitool publish --message "ingest: docker-cheatsheet"` - `tools/wikitool publish --confirm <token> --message "ingest: docker-cheatsheet" # re-run after a Mass-Update exit 42, once the user approved` - `tools/wikitool publish --confirm-rebase <token> --message "ingest: docker-cheatsheet" # re-run after a rebase-review exit 42, once the user approved` **EXIT STATUS** - 0 success - 0 The machine-data backup after the push failed: a `WARN` line names the reason, and the commit and the push stand - 0 The export after the commit failed: a `WARN` line names the target and the reason, and the commit stands - 1 git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase (a conflict outside the generated files, an edited `kb/log.md`, or an uncommitted change on a path the incoming commits change) - 1 The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case - 1 No `--no-push`, and the remote is not configured or cannot be reached; nothing was committed - 1 `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error - 1 The settings files do not pass their check, `remotes.allowed_push_urls` is not a list of strings, the `backup` section is malformed, or the `export` section is malformed while a target sets `after_publish`; nothing was committed - 42 The remotes settings file of an earlier version is still there - 42 Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset - 42 Rebase-Review Gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff - 42 Publish-Remote Gate: `remotes.allowed_push_urls` is set and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL **ON FAILURE** - The machine-data backup after the push failed: a `WARN` line names the reason, and the commit and the push stand -> Show the `WARN` line to the user. Do not publish again - once the cause is fixed, `tools/wikitool backup run` repeats the backup - The export after the commit failed: a `WARN` line names the target and the reason, and the commit stands -> Show the `WARN` line to the user. Do not publish again - once the cause is fixed, `tools/wikitool export run` repeats the export - git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase (a conflict outside the generated files, an edited `kb/log.md`, or an uncommitted change on a path the incoming commits change) -> Do not retry and do not force - report and ask the user. `publish` has already made its one retry of a rejected push itself, where a reconcile resolved the rejection - The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case -> Check out the branch you mean to publish, or pass `--branch <checked-out branch>`, then retry once - No `--no-push`, and the remote is not configured or cannot be reached; nothing was committed -> Show the message to the user and ask whether to commit locally with `--no-push`. Never push by hand - the next `publish` that reaches the remote sends that commit - `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error -> Drop it. The Mass-Update Gate is cleared only with `--confirm <token>` from the gate's own refusal output - The settings files do not pass their check, `remotes.allowed_push_urls` is not a list of strings, the `backup` section is malformed, or the `export` section is malformed while a target sets `after_publish`; nothing was committed -> Show the error to the user and stop - a malformed section is not permission, and fixing it is theirs to do - The remotes settings file of an earlier version is still there -> Show the output to the user verbatim and stop; `tools/wikitool config migrate` moves it, and the user decides when - Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state - Rebase-Review Gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state - Publish-Remote Gate: `remotes.allowed_push_urls` is set and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL -> Show the user the push URL it names and the allowed ones, and stop. This gate has no token and no flag: only the user resolves it, by adding the URL to `.wikitool-local.json` **NEVER** - Never retry a failed git step unchanged, and never force (`--force`, `--force-with-lease`). - Never pass a `--confirm` or `--confirm-rebase` token the user has not seen and approved. - Never edit `remotes.allowed_push_urls` to get past a Publish-Remote refusal - that is opening a gate on your own initiative. **NOTES** - Order: branch check and Publish-Remote Gate, then the reconcile with `<remote>/<branch>`, then the Mass-Update Gate, then `git add -A`, commit and push. `--no-push` skips all but the Mass-Update Gate and the commit. - Without `--no-push`, a remote that is not configured or cannot be reached ends the call with exit 1 at the reconcile - before the gate, `git add` and the commit, and also on a clean tree. Nothing is committed, the index and the working tree are unchanged, and the message names `--no-push` as the way to a local commit. The next `publish` that reaches the remote pushes that commit along with whatever is new. A remote that answers but has no `<branch>` yet (a new, empty repository) is not this case: the first publish of an instance commits and pushes as before. - Reconcile: fetches `<remote>/<branch>`, fast-forwards when only the remote moved, rebases the local commits on top when both sides moved but touched disjoint files, and exits 42 (Rebase-Review Gate) when both sides touched the same file. A refused reconcile performs no rebase attempt. The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides. - Generated files are never an overlap: where they are all that stops the reconcile, it merges them as `sync` does (both sides' log entries kept, the catalog and `kb/provenance.md` regenerated) and carries uncommitted changes no incoming commit touches through the rebase. The proactive reconcile runs before staging, so this publish commits the regenerated files - with `--path` too, see there; on the retry after a rejected push they get a commit of their own before the second push. - The push target must be the checked-out branch; this is checked before anything is staged. The unborn branch of a fresh `git init -b main` counts as checked out, so the first publish of a new instance works; a real detached HEAD is refused. - With nothing new to stage, a local commit the remote lacks is still pushed: one left behind by an earlier publish whose push failed, or every commit when the remote answers but does not have the branch yet (a new, empty remote repository). - A rejected push that finds the remote unreachable on its one retry reports the original push error. - A rejected push gets exactly one more reconcile-and-push; never more than one. - Mass-Update Gate: counts the files that would be committed, refuses with exit 42 at `--threshold` (default 10) or more, and prints a review report - a scale line (file count, total lines added/removed, status breakdown), attention notes where they apply (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn. The list is what the commit will hold: it is computed from a scratch copy of the index after `git add -A`, so a path that is staged as deleted and back in the working tree is not counted twice, and a rename counts as its old path deleted plus its new path added. The real index and the working tree are not touched, so a refused publish leaves both byte-identical. - Never counted and never shown for approval, but committed like everything else: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md` under `kb/`). The refusal line accounts for both, by reason. - The `--confirm` token covers each counted path, the blob id of its contents and the publish target: a different file list or edited contents need a new clearance. - Publish-Remote Gate: when `.wikitool-local.json` carries `remotes.allowed_push_urls` and the push URL of `--remote` is not listed in it, exits 42 before the reconcile fetches anything. The URL is read with `git remote get-url --push`, so a repointed remote does not pass on its name. An absent section means unrestricted; a malformed one is an error, not permission. - The remotes settings file of an earlier version is not read: while it exists this exits 42 and names `tools/wikitool config migrate`, so a restriction is never lost silently. - `--path` (repeatable) scopes the whole operation - gate count, staging and commit - to that subtree, with one exception: uncommitted generated files outside `--path` (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md` under `kb/`) are staged and committed too, and a line names them - whether a reconcile or the change itself wrote them. They stay out of the gate count, and the `--confirm` token and the re-run line cover the `--path` given. Every other file outside `--path` is left as it is. - Commit message: `--message`, then a `Files changed:` paragraph listing every committed path. When `--message` ends in a paragraph git reads as a trailer block (`Co-Authored-By:` and the like), that block stays the last paragraph and the list goes in front of it, since git reads trailers from the last paragraph only. `git interpret-trailers` decides whether there is such a block, and the list moves only when git reads the same trailers from the result; otherwise it is appended at the end. `--message` is not part of any gate token. - Machine-data backup: with a `backup` section in `.wikitool-local.json` whose `machine_data` is true, a call that pushed then mirrors this machine's files that are not in git into `<target>/<machine>/` - the same step as `backup run`. A failure there prints a `WARN` line with the reason and still exits 0, since the commit and the push stand. No backup with `--no-push`, after a failed push, or when there was nothing to push. A malformed `backup` section stops the call with exit 1 before anything is committed, `--no-push` included. - Export after publish: every target of the `export` section with `after_publish: true` is refreshed once a commit was made - the same step as `export run`, `--no-push` included, since the export reads the committed `kb/`. A failure there prints a `WARN` line with the reason and still exits 0, since the commit stands; `export run` repeats it. A malformed `export` section stops the call with exit 1 before anything is committed, but only when some target sets `after_publish`. - After a successful commit or push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md` or a path ending in `CONTRACT.md`, prints one reminder line: the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. It is not a gate: no exit code change, nothing to clear, and silent for an ordinary content publish. **SEE ALSO** - `wikitool sync` - the same reconcile on its own, without committing or pushing - `instructions/gates.md` - the gate procedure - `instructions/setup-instance.md` - the first publish of a new instance - `wikitool backup run` - the machine-data backup on its own - `wikitool export run` - the page export on its own #### `backup init` Create the backup target's bare repository and make it `origin` when there is none. **SYNOPSIS** - `wikitool backup init` **PROPERTIES** - effect: write - idempotent: yes - atomic: Every precondition is checked first; a refusal changes nothing - budget: counted - network: no **EXAMPLES** - `tools/wikitool backup init` **EXIT STATUS** - 0 success - 1 No `backup` section in `.wikitool-local.json`, or a malformed one - a target that is not absolute or lies inside the instance, a `machine_name` that cleans to nothing or to `git`, an `extra` path that resolves outside the instance or into `.git/` - 1 `<target>/git` exists and is not a bare repository; nothing was changed - 1 `origin` already pushes to another URL; the message names both and nothing was changed **ON FAILURE** - No `backup` section in `.wikitool-local.json`, or a malformed one - a target that is not absolute or lies inside the instance, a `machine_name` that cleans to nothing or to `git`, an `extra` path that resolves outside the instance or into `.git/` -> Show the message to the user and stop. The section is theirs to write or fix by hand - `config set` refuses its fields; docs/how-to/back-up-to-a-synced-folder.md has the procedure - `<target>/git` exists and is not a bare repository; nothing was changed -> Show the message to the user - whether that folder moves, or the target does, is their decision - `origin` already pushes to another URL; the message names both and nothing was changed -> Show both URLs to the user and stop. Never repoint `origin` yourself - `git remote set-url` is theirs to run **NEVER** - Never add the printed URL to `remotes.allowed_push_urls` yourself - that field is a gate, and editing it is the user's. **NOTES** - Reads the `backup` section of `.wikitool-local.json` and creates `<target>/` and `<target>/git`, the latter as a bare repository (`git init --bare -b main`). An existing bare repository there, or an empty directory, is used as it is. - Sets `origin` to `<target>/git` when the checkout has no `origin`. An `origin` that already pushes there is left as it is; one that pushes anywhere else stops the command. - When `remotes.allowed_push_urls` exists and lacks the URL `origin` pushes to, prints the `remotes` section with that URL added, for the user to copy into `.wikitool-local.json`. It never writes that file: the field is a gate. - Pushes nothing: the next `publish` sends the history to the new `origin`. **SEE ALSO** - `wikitool backup run` - the machine-data half of the backup - `wikitool publish` - pushes to the bare repository and runs the machine-data backup after it - `docs/how-to/back-up-to-a-synced-folder.md` - the whole procedure, restoring included #### `backup run` Mirror this machine's files that are not in git into `<target>/<machine>/`. **SYNOPSIS** - `wikitool backup run` **PROPERTIES** - effect: write - idempotent: yes - atomic: Per file - each is written to a temporary file and renamed, the manifest last; an interrupted run leaves whole files and the previous manifest - budget: counted - network: no **EXAMPLES** - `tools/wikitool backup run` **EXIT STATUS** - 0 success - 1 No `backup` section in `.wikitool-local.json`, or a malformed one - a target that is not absolute or lies inside the instance, a `machine_name` that cleans to nothing or to `git`, an `extra` path that resolves outside the instance or into `.git/` - 1 `machine_data` is false - 1 `<target>/<machine>/` is not empty and has no manifest, or its manifest cannot be read; nothing was written - 1 The target is not a directory (the synced folder is missing or not mounted), or a write failed **ON FAILURE** - No `backup` section in `.wikitool-local.json`, or a malformed one - a target that is not absolute or lies inside the instance, a `machine_name` that cleans to nothing or to `git`, an `extra` path that resolves outside the instance or into `.git/` -> Show the message to the user and stop. The section is theirs to write or fix by hand - `config set` refuses its fields; docs/how-to/back-up-to-a-synced-folder.md has the procedure - `machine_data` is false -> Show the message to the user; turning it on is their edit of `.wikitool-local.json` - `<target>/<machine>/` is not empty and has no manifest, or its manifest cannot be read; nothing was written -> Show the message to the user - the folder is not one this backup wrote. They set another `machine_name` or empty it - The target is not a directory (the synced folder is missing or not mounted), or a write failed -> Show the message to the user; once the cause is fixed, run `backup run` again **NEVER** - Never point `extra` at a path outside the instance to back something else up - the command refuses it, and the secrets belong in no synced folder. **NOTES** - Mirrors `.wikitool-local.json`, `ENVIRONMENT.md` if present, every file in `incoming/` but `.gitkeep`, and the `extra` paths of the `backup` section into `<target>/<machine>/`, then writes the manifest `.chemenu-backup.json` there: time, stack version, machine name, and per file its relative path, size and sha256. - Never mirrors `quarantine/`, `reports/`, `.wikitool-tools.json`, `tools/.venv/`, the published skills or `.git/` - unless an `extra` path names one of the first ones. The secrets file lies outside the instance and is never read. - Writes and deletes only under `<target>/<machine>/`. Deletes only a file the previous manifest lists and the instance no longer has; a file there the manifest does not list stays as it is. A file whose size and sha256 match the previous manifest is not rewritten. - A symbolic link or junction inside a mirrored directory is skipped and named in the output, never followed. - `publish` runs the same step after every push when `machine_data` is true. **SEE ALSO** - `wikitool backup init` - sets up the target's bare repository - `wikitool doctor` - the `backup` line reports the age of the last run - `docs/how-to/back-up-to-a-synced-folder.md` - the whole procedure, restoring included ### Exports #### `export guidelines` Render the guideline pages into one generated `GUIDELINES.md`, or push it into the captured repositories that opted in (**Guideline Push Gate**). **SYNOPSIS** - `wikitool export guidelines [--field <predicate> ...] [--kind/--subtype/--collection/--tag <v>]` - Prints the file to stdout; no network. - `wikitool export guidelines <predicates> --push [--bundle <raw-bundle> ...] [--confirm TOKEN]` - Writes it into the captured repositories, behind the gate. **PROPERTIES** - effect: write - idempotent: yes - atomic: Per target - one commit pushed without force, or nothing; targets already written stay written when a later one fails - budget: counted - network: yes - gates: guideline-push **EXAMPLES** - `tools/wikitool export guidelines --tag guideline` - `tools/wikitool export guidelines --tag guideline --push` - `tools/wikitool export guidelines --confirm <token> --push --field tags=guideline # re-run after exit 42, once the user approved every diff` **EXIT STATUS** - 0 success - 0 A target was skipped - tag rule, not reachable, not opted in, hand-written file, another instance's file - 1 No predicate, no page matches, a malformed predicate or an unknown field - 1 A page under `kb/` has unreadable frontmatter, `kb/` or `types/` has uncommitted changes, or the instance is not a git checkout - 1 `--push` with no `user.name` or `user.email` in the instance checkout's git configuration - checked before any target is fetched - 1 `--bundle` names no captured bundle under `raw/`, or `--bundle`/`--confirm` was given without `--push` - 1 A target rejected the push (its branch moved since the fetch) or a git call failed - the other targets were still served - 42 Guideline Push Gate: `--push` without `--confirm`, or with a token that does not match the current set of targets **ON FAILURE** - No predicate, no page matches, a malformed predicate or an unknown field -> Use the filter `kb/CONVENTIONS.md` names for this instance's guidelines, and retry once - A page under `kb/` has unreadable frontmatter, `kb/` or `types/` has uncommitted changes, or the instance is not a git checkout -> Fix the named pages, or publish the changes first, then retry once - `--push` with no `user.name` or `user.email` in the instance checkout's git configuration - checked before any target is fetched -> Show the message to the user; the identity is theirs to set, never yours - `--bundle` names no captured bundle under `raw/`, or `--bundle`/`--confirm` was given without `--push` -> Fix the argument and retry once - A target rejected the push (its branch moved since the fetch) or a git call failed - the other targets were still served -> Show the lines to the user. A rejected target is served by running the whole command again, which asks for clearance again - Guideline Push Gate: `--push` without `--confirm`, or with a token that does not match the current set of targets -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state **NEVER** - Never pass a `--confirm` token the user has not seen and approved. - Never edit a `GUIDELINES.md` in a target repository by hand to make it match - the next export overwrites it; change the guideline page instead. **NOTES** - Selects pages with `search`'s own predicates and no text: `--field` (repeatable, AND), `--kind`, `--subtype`, `--collection`, `--tag`. Which filter selects this instance's guidelines is written down in `kb/CONVENTIONS.md`. - Renders mechanically: line 1 is `<!-- wikitool:export kind=guidelines instance=<origin URL without userinfo, or local> commit=<newest commit touching a selected page> - generated, do not edit by hand -->`; then each page sorted by title, as `# <title>` and its text without frontmatter, the generated links and footnote blocks, citation markers (`[^id]`, legacy `^[[...]]`), with `[[Title|Text]]` as `Text` and `[[Title]]` as `Title`. A leading H1 of the page is replaced by the title heading. Code is never rewritten. LF endings, no timestamp: the same pages give the same bytes. - Refuses before printing or pushing anything: no predicate, no match, an unknown field, any page under `kb/` with unreadable frontmatter, uncommitted changes in `kb/` or `types/` (untracked files included), or no git checkout. - `--push` targets every captured repository - each `(repo, ref)` from a `_capture.json` under `raw/`, `--bundle` narrowing it. A target is skipped with its reason when its ref rule is a tag pattern, it cannot be reached or has no such branch, its root has no `GUIDELINES.md` (not opted in), its `GUIDELINES.md` has no export header (hand-written), or the header names another `instance=`. A header line alone is the opt-in. - Guideline Push Gate: `--push` without a matching `--confirm` fetches and builds everything, pushes nothing, and exits 42 with each target's status (`new`, `changed`, `unchanged`, `skipped`), the diff for every target it would write, and the re-run line. The token digests URL, branch, old tip and new blob of every target to write. - With a matching token, each target gets exactly one commit on the fetched tip changing only `GUIDELINES.md`, authored as the instance checkout's `user.name`/`user.email`, pushed without force; one line per target: `unchanged`, `written <old> -> <new>`, `skipped (<reason>)`, `rejected (<reason>)` or `failed (<reason>)`. - Nothing to write (every target unchanged or skipped) ends with exit 0 and no gate. A second run after a successful one finds every target unchanged. - The target repository's own `AGENTS.md` points at the file (Claude Code: `@GUIDELINES.md`); that belongs to the repository, not to this command. **SEE ALSO** - `wikitool search` - the same predicates, to preview the selection - `wikitool raw capture` - how a repository becomes a target - `instructions/gates.md` - the gate procedure #### `export run` Write the derived, read-only reading copy of `kb/` into the folders the `export` section configures - the selected pages as Markdown, or converted by a program of this checkout. **SYNOPSIS** - `wikitool export run [<target> ...]` - Every configured target, or the named ones. Needs an `export` section in `.wikitool-local.json`; the user writes it by hand. **PROPERTIES** - effect: write - idempotent: yes - atomic: Per file within a target (a temporary file in the target, then a move over its place), the manifest last; targets are independent, so a failed target does not undo another - budget: counted - network: no **EXAMPLES** - `tools/wikitool export run` - `tools/wikitool export run work-notes` **EXIT STATUS** - 0 success - 1 No `export` section, an unknown target name, or a broken section (a predicate that does not parse, a path inside the instance or in the way of another target, an `intake` folder or a folder the backup writes into: `<backup.target>/git` or this machine's folder) - 1 `kb/` or `types/` has uncommitted changes, the instance is not a git checkout, or a page under `kb/` has unreadable frontmatter - 1 A target failed: a foreign folder, a folder in backup data, a file changed in the target, an empty selection, an unknown field, two files that differ only in case (also a page and a generated index file), a converter that did not exit 0 **ON FAILURE** - No `export` section, an unknown target name, or a broken section (a predicate that does not parse, a path inside the instance or in the way of another target, an `intake` folder or a folder the backup writes into: `<backup.target>/git` or this machine's folder) -> Show the message to the user; the section is theirs to write by hand, never yours - `kb/` or `types/` has uncommitted changes, the instance is not a git checkout, or a page under `kb/` has unreadable frontmatter -> Fix the named pages, or publish the changes first, then retry once - A target failed: a foreign folder, a folder in backup data, a file changed in the target, an empty selection, an unknown field, two files that differ only in case (also a page and a generated index file), a converter that did not exit 0 -> Show the target's reason to the user; the other targets were still served. Never delete or edit files in a target folder to make a run pass **NEVER** - Never edit a file in a target folder to change an export - the next run refuses or overwrites it; change the page, or the target's `include`, instead. - Never put an export folder into `incoming/` or `raw/`. **NOTES** - An export is derived: regenerated from `kb/` on every run, never edited, never read back into the wiki. Each target names its pages with `include`: either the string `"all"` (every page `search` knows, also one directly under `kb/`; never a generated or meta file) or an allowlist of `search --field` predicates (a list of selections, AND within one, OR across them), a handler (`markdown`, or `command` for the operator's converter) and the folder (`path`, outside the instance). A missing or empty `include` is an error, never "all": nothing is exported that the target does not name. - With `index: true` a target also gets `index.md` in its root and an `INDEX.md` per collection (and per area the catalog gives a file of its own), built from the exported pages alone: collections and areas with their counts, then tables of page, type, summary and last modified, with relative Markdown links whatever `links` says. No date of the day and no hint for an agent; the marker is `kind=index` with the newest commit of the pages the file lists, so a commit to one collection rewrites that collection's `INDEX.md` and `index.md` only. The files are ordinary files of the run (manifest, atomic write, removed once no longer generated); the `command` handler receives them with the pages. A page `<collection>/index.md` beside the generated `INDEX.md` stops the target. - Every Markdown file starts with a frontmatter block whose `wikitool_export` line says `kind=pages instance=<origin URL without userinfo, or local> commit=<newest commit touching this page>`, followed by `title`, `type`, `summary`, `modified` where the page has them (`frontmatter: summary`, the default; `minimal` keeps only the marker, `full` the whole frontmatter), then the page text. A file keeps its path below `kb/`. A wikilink to an exported page becomes a relative Markdown link (`links: relative`, the default) or plain text (`links: text`); a link to a page that is not exported is always plain text. Footnote markers stay; code is never rewritten. LF endings, no timestamp: the same pages give the same bytes, and a commit that touches one page rewrites that page's file alone - plus any file whose links change because a page entered or left the selection. The manifest's `commit` is the newest commit touching any selected page; all of them come from one `git log`, however many pages are selected. - Handler `command`: the Markdown is rendered into a staging directory, the program of `export.commands` runs with no shell, stdin closed and a timeout of 300 s, and receives the Markdown directory and an empty output directory as its last two arguments. On exit 0 the regular files in the output directory (recursively) become the target's content; any other end leaves the target unchanged and prints the end of its stderr. The staging directory is always removed. - The folder is only ever touched by what it holds in its manifest, `.wikitool-export.json`: the files an earlier run wrote, each with its sha256. A run removes only manifest files that are no longer exported (and folders it created, once empty), rewrites a manifest file that is missing, leaves every other file byte for byte as it was, and writes nothing - not even a modification time - for a file that did not change. A run that changes nothing writes nothing at all. - Refuses a target, before writing anything into it: a non-empty folder with no manifest (it is somebody's, not an export), a folder that holds or lies in backup data (a `.chemenu-backup.json` - any machine's, also one this checkout does not know - or a bare git repository), a manifest file that was changed in the folder since the last run (naming it), a foreign file where an exported one belongs, an empty selection (it would delete the earlier export), an unknown field in a predicate. A missing folder is created. - Refuses everything, before any target: uncommitted changes in `kb/` or `types/` (untracked files included), no git checkout, a page under `kb/` with unreadable frontmatter, an unknown target name, a broken `export` section. - Output per target: `new` (with the page titles), `changed`, `removed`, the count `unchanged`, or `failed` with the reason. A page that newly enters a selection is reported as new, not gated. Exit 0 when every target ran; exit 1 when any failed - the others were still served. - A target with `after_publish: true` is also refreshed by every `publish` that committed, `--no-push` included; a failure there is a warning, the commit stands, and this command is the repeat. - `raw accept` refuses a file whose sha256 a target's manifest lists, and one that carries the marker: an export never goes back into `raw/`. **SEE ALSO** - `wikitool search` - the same predicates, to preview a selection - `wikitool backup run` - the same kind of folder, for this machine's own files - `docs/how-to/export-to-a-synced-folder.md` ### Workshop runs and session budget #### `work new` Scaffold `work/<runkey>/` for one workshop run. **SYNOPSIS** - `wikitool work new (--input <raw path> | --key <run key>) [--again] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes - one directory with two files - budget: counted - network: no **EXAMPLES** - `tools/wikitool work new --input raw/2026/09/handbuch` - `tools/wikitool work new --key migrate-7.0.0` - `tools/wikitool work new --input raw/2026/09/handbuch --again` **EXIT STATUS** - 0 success - 1 Neither or both of `--input`/`--key` given, or a `--key` that is empty or starts with `ingest-` - 1 `--input` is outside `raw/`, does not exist, or is `raw/` itself (an empty run key) - 1 The workshop already exists **ON FAILURE** - Neither or both of `--input`/`--key` given, or a `--key` that is empty or starts with `ingest-` -> Fix the argument and retry once - `--input` is outside `raw/`, does not exist, or is `raw/` itself (an empty run key) -> Point `--input` at material under `raw/`, or use `--key` for a run with no raw input - The workshop already exists -> A collision is not transient: resume the existing run instead, or pass `--again` if the tree itself changed **NEVER** - Never create a numbered variant of a run key by hand. **NOTES** - Creates `work/<runkey>/` with the required `README.md` and `plan.md`. - `--input` derives the run key from the path below `raw/` (an ingest); `--key` names it outright for a run with no raw input - a migration or a sweep across `kb/` - and may not start with `ingest-`, which stays reserved for derived keys. Exactly one of the two. - Refuses a collision instead of suffixing it. - `--again` opens a dated second pass (`<runkey>-<date>`) over a tree that has itself changed. - `--dry-run` reports the run key and files without writing. **SEE ALSO** - `work/CONTRACT.md` - run keys, required files, how a run closes - `wikitool work close` - deletes the run when it is done - `instructions/ingest-large-tree.md` - the ingest that opens a run #### `work close` Delete a finished workshop. **SYNOPSIS** - `wikitool work close --run-key <name> [--yes] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - a recursive delete - budget: counted - network: no **EXAMPLES** - `tools/wikitool work close --run-key ingest-2026-09-handbuch --dry-run` - `tools/wikitool work close --run-key ingest-2026-09-handbuch --yes` **EXIT STATUS** - 0 success - 1 Unknown run key - 1 `--yes` was not passed - the output lists what would be lost **ON FAILURE** - Unknown run key -> Check `ls work/` for the open runs, then retry once - `--yes` was not passed - the output lists what would be lost -> Check the listed files are no longer needed, confirm the conclusions are in `kb/`, then re-run with `--yes` **NEVER** - Never pass `--yes` before the run's conclusions are in `kb/`. **NOTES** - Deletes `work/<run-key>/` recursively. - Without `--yes` it lists what would be lost and refuses: nothing in a workshop is recoverable from the rest of the repo, so the durable conclusions must already be in `kb/`. - `--dry-run` lists the files without deleting. - Not idempotent: once deleted, the run key is unknown. **SEE ALSO** - `work/CONTRACT.md` - how a run closes - `wikitool work new` - opens a run #### `budget status` Show the current session's `wikitool` call count and recent command history. **SYNOPSIS** - `wikitool budget status` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool budget status` **EXIT STATUS** - 0 success **NOTES** - Shows the current session's `wikitool` call count and its recent command history. - Never counted against the budget; never fails; safe to retry freely. **SEE ALSO** - `instructions/session-setup.md` - scoping the budget to a task - `instructions/gates.md` - what to do when the Iteration Budget Gate refuses #### `budget reset` Clear the current session's (or every session's) iteration budget state. **SYNOPSIS** - `wikitool budget reset --yes [--all]` **PROPERTIES** - effect: write - idempotent: no - atomic: Read/rewrite of one JSON file (or its deletion, with `--all`) - budget: counted - network: no **EXAMPLES** - `tools/wikitool budget reset --yes # only after the user approved it` **EXIT STATUS** - 0 success - 1 `--yes` not passed **ON FAILURE** - `--yes` not passed -> Get the user's approval, then re-run with `--yes` **NEVER** - Never run it on your own initiative to get past a budget or loop-breaker refusal. **NOTES** - Clears the current session's iteration budget state; `--all` deletes every session's. - Requires `--yes`: clearing the counter is itself a way around the Iteration Budget Gate, so it needs the same explicit human approval. - Counted against the budget like any other call. **SEE ALSO** - `wikitool budget status` - the current count - `instructions/gates.md` - why `budget reset` is not the escape hatch ### Types, instructions and docs #### `types list` List every type-spec under `types/`. **SYNOPSIS** - `wikitool types list [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool types list` - `tools/wikitool types list --json` **EXIT STATUS** - 0 success **NEVER** - Never pick a page's directory by hand - `types describe` and `new` compute it. **NOTES** - Lists every type-spec under `types/`: name, schema path, subtype field, and description - which page types exist, without reading `types/*.md` directly. - Never fails; read-only and safe to retry freely. **SEE ALSO** - `wikitool types describe <name>` - one type's full contract #### `types describe` Print one type's full contract. **SYNOPSIS** - `wikitool types describe <name> [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool types describe source` - `tools/wikitool types describe project --json` **EXIT STATUS** - 0 success - 1 Unknown type name **ON FAILURE** - Unknown type name -> Fix the name (see `types list`) and retry **NOTES** - Prints one type's full contract: required and optional frontmatter fields with enums, its subtype field (if any), and its authoring body. - Where the type-spec declares `guidance:`, the stack-owned `types/<name>.guidance.md` is composed in, so a `root: kb` type's contract reads as one answer even though it may live in two files. `--json` reports it separately as `guidance`/`guidance_path`, absent for a type with none. - The generated table-of-contents region a long type-spec (or guidance file) carries is stripped from this output. - Read-only. **SEE ALSO** - `wikitool types list` - every type - `wikitool new <type>` - scaffolds a page of the type #### `instructions sync` Publish every `instructions/<name>/SKILL.md` into the harness skill directories. **SYNOPSIS** - `wikitool instructions sync [--force]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - one directory copy per skill per target (`.agents/skills/`, `.claude/skills/`); each copy is idempotent, so a re-run converges even after a partial failure - budget: counted - network: no **EXAMPLES** - `tools/wikitool instructions sync` **EXIT STATUS** - 0 success - 1 No skills found under `instructions/` - 1 A target directory is not a published skill (no `SKILL.md`) and `--force` was not passed **ON FAILURE** - No skills found under `instructions/` -> Fix the named cause and retry - A target directory is not a published skill (no `SKILL.md`) and `--force` was not passed -> Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not **NEVER** - Never hand-edit a published copy under `.agents/skills/` or `.claude/skills/` - edit the source and re-run this. **NOTES** - Publishes every `instructions/<name>/SKILL.md` into `.agents/skills/` and `.claude/skills/` as **copies**, and deletes published skills whose source is gone. - Each published `SKILL.md` is its source plus one generated line directly under the frontmatter: the `language:` of `kb/CONVENTIONS.md` and a pointer to `AGENTS.md` § Conversation language, so the agent has the value in context when the task begins. The frontmatter and every other file are copied unchanged. With no language declared, the `SKILL.md` is copied unchanged too, and the success message says so. - Both targets are gitignored, so a fresh clone runs this once. - Re-running repairs a drifted copy: the source always wins. - `--force` is required only to replace a target directory that is not a published skill at all (no `SKILL.md` in it). - Each copy is idempotent, so a re-run converges even after a partial failure. **SEE ALSO** - `instructions/bootstrap.md` - the fresh-clone procedure that runs this - `wikitool instructions verify` - checks the copies match #### `instructions verify` Check the instruction layer. **SYNOPSIS** - `wikitool instructions verify` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool instructions verify` **EXIT STATUS** - 0 success - 1 Nothing found under `instructions/` at all, or a malformed instruction or `SKILL.md` - 1 A `SKILL.md` carries a relative markdown link - 1 A published copy drifted from its source - including a language line that belongs to an earlier `language:` (the message names it as such) - 1 An instruction nothing references, or a `manual: true` one that IS linked from AGENTS.md, CLAUDE.md or a skill and so risks running implicitly - 1 Something under `instructions/dev/` is referenced from outside it and outside a `dist:strip` block **ON FAILURE** - Nothing found under `instructions/` at all, or a malformed instruction or `SKILL.md` -> Fix the flagged file, then re-run - A `SKILL.md` carries a relative markdown link -> Rewrite it as a repo-root-relative plain path, then re-run - A published copy drifted from its source - including a language line that belongs to an earlier `language:` (the message names it as such) -> Re-run `instructions sync` - the source under `instructions/` always wins - An instruction nothing references, or a `manual: true` one that IS linked from AGENTS.md, CLAUDE.md or a skill and so risks running implicitly -> Link it from where it is used, or drop the link to a manual one, then re-run - Something under `instructions/dev/` is referenced from outside it and outside a `dist:strip` block -> Remove the reference or wrap it in a `dist:strip` block, then re-run **NEVER** - Never fix drift by hand-editing the published copy. **NOTES** - Flat instructions validate against `types/instruction.schema.yaml`, and each `SKILL.md` carries the frontmatter its harness reads. - No `SKILL.md` carries a relative markdown link: `sync` copies it to a different depth than the source, so a `SKILL.md` references a target as a repo-root-relative plain path instead (`instructions/CONTRACT.md` § "A skill's outbound reference is a plain path, not a link"). - Every published copy is what `sync` would write from its source: byte-identical, except for the generated language line in `SKILL.md` that the current `language:` of `kb/CONVENTIONS.md` calls for. Missing *every* copy is reported as "run sync", not as drift - that is a clean checkout. - No instruction is left that nothing references; one marked `manual: true` must instead not be linked from AGENTS.md, CLAUDE.md or a skill. - Nothing under `instructions/dev/` is referenced from outside it; a `<!-- dist:strip-start/end -->` block is exempt (`instructions/CONTRACT.md`). - Read-only. **SEE ALSO** - `wikitool instructions sync` - publishes the copies - `instructions/CONTRACT.md` - the rules this checks #### `instructions list` List the flat instructions with their descriptions. **SYNOPSIS** - `wikitool instructions list [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool instructions list` - `tools/wikitool instructions list --json` **EXIT STATUS** - 0 success **NOTES** - Lists the flat instructions with their descriptions - how the instruction layer is discovered; `search` covers `kb/` only. - Never fails: an empty `instructions/` prints "No instructions found." Read-only and safe to retry freely. **SEE ALSO** - `wikitool search` - the same question for `kb/` #### `docs verify` Check the docs that mirror the code. **SYNOPSIS** - `wikitool docs verify` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool docs verify` **EXIT STATUS** - 0 success - 1 A command, contract, or type-form mismatch - 1 `VERSION` and the newest `CHANGES.md` entry disagree, or a boundary-crossing entry lacks its breaking line, its migration, or the migration document's path - 1 The newest `CHANGES.md` entry is over 32 000 bytes, or one of its `###` sections has more than one paragraph or a paragraph over 1 000 bytes - 1 The `<!-- wikitool:commands -->` region of `tools/CONTRACT.md` is stale - 1 A type-spec's own frontmatter fails its schema - 1 A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter - 1 `wikitool.json` holds a secret or private value as plain text, or a value its tier puts elsewhere - 1 A shipped `.md`/`.template` cites an issue number - 1 A region of the human reference under `docs/reference/` is stale - 1 A page of the human reference is missing or lacks its marker pair, or a page under `docs/reference/commands/` belongs to no group - 1 A reference file's table-of-contents region is missing or stale - 1 A file sits directly in `docs/`, or in a directory there that is not one of its five areas - 1 `AGENTS.md` § Gates and `cli_contract.GATES` disagree, or a register gate that ends in exit 42 is declared by no command record - 1 A reference file's relative markdown link does not resolve to an existing file - 1 An `INSTALL.md` prerequisites region is stale - 1 An `INSTALL.md` prerequisites region is missing, or names a platform no tool has - 1 A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other **ON FAILURE** - A command, contract, or type-form mismatch -> Fix the documentation it names, then re-run - `VERSION` and the newest `CHANGES.md` entry disagree, or a boundary-crossing entry lacks its breaking line, its migration, or the migration document's path -> Run `version bump`, which writes all of them, or fix whichever is wrong - The newest `CHANGES.md` entry is over 32 000 bytes, or one of its `###` sections has more than one paragraph or a paragraph over 1 000 bytes -> Shorten or merge the named sections - a smaller change keeps only its bump title - then re-run - The `<!-- wikitool:commands -->` region of `tools/CONTRACT.md` is stale -> Run `docs contract --apply`, then re-run - A type-spec's own frontmatter fails its schema -> Fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new - A subtype template `types/<type>.<value>.md` has no type-spec with a `subtype_field:` beside it, names a value outside that field's enum, or carries frontmatter -> Rename the file to the type and value it was meant for, delete it, or remove its frontmatter block - `wikitool.json` holds a secret or private value as plain text, or a value its tier puts elsewhere -> Show the user the message: a plain value there may already be in the history. Move it as the message says - a secret into 1Password, the secrets file or a variable, set by the user, with only the reference left behind - then re-run - A shipped `.md`/`.template` cites an issue number -> Say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block - A region of the human reference under `docs/reference/` is stale -> Run `docs reference --apply`, then re-run - A page of the human reference is missing or lacks its marker pair, or a page under `docs/reference/commands/` belongs to no group -> Create the page with a title, a one-sentence head and the marker pair the message names (or add the pair), then run `docs reference --apply`; `git mv` or `git rm` a page left over from a renamed or removed group - A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run - A file sits directly in `docs/`, or in a directory there that is not one of its five areas -> Move it (`git mv`) into the area its kind of documentation belongs to, and fix the links that pointed at the old path - `AGENTS.md` § Gates and `cli_contract.GATES` disagree, or a register gate that ends in exit 42 is declared by no command record -> Make § Gates list exactly the register's display names, or register the gate in `cli_contract.GATES`; declare an exit-42 gate on the command that raises it - A reference file's relative markdown link does not resolve to an existing file -> Fix the `../` count or the target's name - An `INSTALL.md` prerequisites region is stale -> Run `docs prerequisites --apply`, then re-run - An `INSTALL.md` prerequisites region is missing, or names a platform no tool has -> Add the marker pair where that list belongs (or remove the orphaned region and its introducing prose), then run `docs prerequisites --apply` - A setup question is marked in one of `instructions/setup-instance.md` and `INSTALL.md` but not the other -> Describe the question for the human in `INSTALL.md` with the same marker, or remove the bullet for a question no longer asked **NEVER** - Never hand-write a table-of-contents region, the commands region or a reference region - regenerate it. **NOTES** - Checks the docs that mirror the code. The name is about documentation parity, not the `docs/` directory - it reads that only for its layout and its generated reference pages, and never requires one. - Commands: every command has a `cli_contract` record and is listed in `cli_contract.GROUPS`, in both directions; every command's non-hidden flags appear in its record's SYNOPSIS and vice versa; `tools/CONTRACT.md`'s generated `<!-- wikitool:commands -->` region matches what `docs contract` would write; no command's rendered `--help`/`-h` text cites an issue number. - Collections: every directory under `kb/` has a `COLLECTION.md` and no directory outside it does; every collection declares `profile:` and a `required_by_stack:` that agrees with the stack's own list. - Types: every type the stack lists (currently `source` and `project`) has a type-spec of that name whose schema requires the field the stack list names (`raw_files:`/`state:`); every file under `types/` declaring `type: types/type-spec.md` validates against `types/type-spec.schema.yaml`; every subtype template `types/<type>.<value>.md` (any `<value>` but `guidance`) sits beside a type-spec declaring `subtype_field:`, names a value that field's schema enum allows, and carries no frontmatter; no pre-migration `type: entity` blocks are left in the contracts. - `kb/CONVENTIONS.md`, if it exists at all, names all three tool-owned section headings; every stage contract is present. - The `.gitignore` canaries clear in both directions: nothing ignored under `raw/`/`kb/`, `incoming/` ignored, everything ignored under `reports/` and the published skill directories, `.wikitool-local.json` ignored and `wikitool.json` tracked. - A `wikitool.json` at the root holds every value in the place its tier allows - no secret and no private field as plain text, no `checkout` field at all except as a reference - and a well-formed `schema` and `instance_id`. The local file is not read. - No `.md`/`.template` file `dist export` would ship cites an issue number. A `<!-- dist:strip-start/end -->` region is exempt: the check reads the export plan's text, from which it is already gone. - Every reference file `docs toc` covers carries the current table-of-contents region for its own headings - missing and stale are one check. - Every relative markdown link in one of those reference files resolves to an existing file. A target's `#anchor` suffix is stripped first, and code fences and inline code spans are masked before scanning, so link syntax shown as an example is not mistaken for a real reference. - Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, `reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another directory. - `AGENTS.md` § Gates names exactly the gates in `cli_contract.GATES` (the bold entry names, `- **<Name>.**`), and every gate in that register that ends in exit 42 is declared by at least one command record. A record naming a gate outside the register fails when `wikitool` starts, not here. - If `docs/` exists: every generated region of the human reference under `docs/reference/` matches what `docs reference` would write, every `cli_contract.GROUPS` entry has its page under `docs/reference/commands/`, and no page there belongs to no group. That includes the four configuration regions in `docs/reference/configuration.md`. The skill catalogue and the page types are checked only in the origin repo (no `.wikitool-release.json`): they describe the stack's shipped defaults, which an instance's own type-specs and instructions may differ from. - `INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a question the agent asks is never one the human guide leaves out, nor the reverse. - `VERSION` parses and the newest versioned `CHANGES.md` entry names it. An entry that crosses the compatibility boundary from the last release carries `**Breaking Change:**`, has a migration document or `**Migration:** none required`, and its `**Migration:**` line names every required migration document targeting it. - The newest versioned `CHANGES.md` entry is at most 32 000 bytes (UTF-8) as `version notes` prints it, and each of its `###` sections is exactly one paragraph of at most 1 000 bytes, heading line excluded. Older entries and a changelog with no versioned entry are not checked. - Read-only. **SEE ALSO** - `wikitool docs toc` - regenerates tables of contents - `wikitool docs contract` - regenerates the commands region - `wikitool docs reference` - regenerates the human reference - `wikitool docs prerequisites` - regenerates `INSTALL.md`'s prerequisites lists - `wikitool instructions verify` - the same kind of check for `instructions/` #### `docs toc` Create, refresh or remove the generated table-of-contents region. **SYNOPSIS** - `wikitool docs toc [--apply]` **PROPERTIES** - effect: write - idempotent: yes - atomic: `--apply` rewrites each named file in place, one at a time and idempotently, so a re-run after an interruption converges rather than doubling a region; the dry-run form is read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool docs toc` - `tools/wikitool docs toc --apply` **EXIT STATUS** - 0 success - 0 Never fails on content: a file with no `##` heading, or one at or under the threshold, is simply left without a region **NEVER** - Never hand-write or hand-edit a table-of-contents region. **NOTES** - Creates, refreshes or removes the generated table-of-contents region on every reference file over 100 lines: `AGENTS.md`, every stage contract, `kb/CONVENTIONS.md`, every `kb/*/COLLECTION.md`, every flat `instructions/**.md` file, every `types/*.md` type-spec, and every `docs/` page - each together with the `<name>.template` it ships as, where one exists. - The scope is computed from those categories rather than listed, so a file added later is in scope without a code change. - Out of scope: every `SKILL.md`, the human docs at the root and beside the code (`README.md`, `CHANGES.md`, `INSTALL.md`, `tools/README.md`), and every subtype template `types/<type>.<value>.md` - `new` copies it into a page, so it never carries a region whatever its length. - Dry-run by default (prints which files would change); `--apply` writes. Idempotent: a re-run after an interruption converges rather than doubling a region. - If `docs verify` still reports a stale region after `--apply`, the file's `##` headings changed in between; run it again. **SEE ALSO** - `wikitool docs verify` - checks every region is current #### `docs prerequisites` Regenerate `INSTALL.md`'s prerequisites lists from `tools/prerequisites.txt`. **SYNOPSIS** - `wikitool docs prerequisites [--apply]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - every region is rewritten in one file write - budget: counted - network: no **EXAMPLES** - `tools/wikitool docs prerequisites` - `tools/wikitool docs prerequisites --apply` **EXIT STATUS** - 0 success - 1 `INSTALL.md` is missing, or lacks a region the manifest calls for **ON FAILURE** - `INSTALL.md` is missing, or lacks a region the manifest calls for -> Not transient - add the marker pair the message names where that list belongs (restore the file if it is gone), then retry **NEVER** - Never hand-edit a prerequisites region - change `tools/prerequisites.txt` and re-run this. **NOTES** - Rewrites each `<!-- wikitool:prerequisites -->` region in `INSTALL.md` (tools every platform needs) and `<!-- wikitool:prerequisites-<platform> -->` region (tools only that platform needs) from the manifest: one list item per tool, its label and minimum version. The manifest's reason field stays out - it explains a tool to whoever reads the preflight's own output, while the region only says what to install. - Never places a region: where a list belongs in the human guide is that guide's own decision. A region the manifest calls for but the file lacks is an error naming the marker pair to add. - Dry-run by default (says whether the file would change); `--apply` writes. - `docs verify` checks the result stays current. **SEE ALSO** - `wikitool docs verify` - checks every region is current #### `docs contract` Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region. **SYNOPSIS** - `wikitool docs contract [--apply]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - the whole region is rewritten in one file write - budget: counted - network: no **EXAMPLES** - `tools/wikitool docs contract` - `tools/wikitool docs contract --apply` **EXIT STATUS** - 0 success - 1 `tools/CONTRACT.md` is missing **ON FAILURE** - `tools/CONTRACT.md` is missing -> Not transient - restore the file, which carries hand-written prose around the region this command does not generate, then retry **NEVER** - Never hand-edit the region - change the record in code and re-run this. **NOTES** - Rebuilds the region from every `cli_contract` record: the index (one line per command, `GROUPS` order) followed by each `###` group's commands as `#### <path>` man-page-shaped sections. - Dry-run by default (says whether the file would change); `--apply` writes. - `docs verify` checks the result stays current. **SEE ALSO** - `tools/README.md` § Adding a command - how a command gets its record - `wikitool docs reference` - the same records, rendered for a person under `docs/reference/` - `wikitool docs verify` - checks the region is current #### `docs reference` Regenerate the human reference under `docs/reference/`. **SYNOPSIS** - `wikitool docs reference [--apply]` **PROPERTIES** - effect: write - idempotent: yes - atomic: `--apply` rewrites each page in place, one at a time and idempotently, so a re-run after an interruption converges; the dry-run form is read-only - budget: counted - network: no **EXAMPLES** - `tools/wikitool docs reference` - `tools/wikitool docs reference --apply` **EXIT STATUS** - 0 success - 1 A reference page is missing, or lacks its region's marker pair **ON FAILURE** - A reference page is missing, or lacks its region's marker pair -> Not transient - create the page the message names with a title, a one-sentence head and the marker pair, or add the marker pair below its head, then retry **NEVER** - Never hand-edit a reference region - change the record in code and re-run this. **NOTES** - Rewrites the generated region of every page of the human command reference from the `cli_contract` records: one `docs/reference/commands/<group>.md` per `cli_contract.GROUPS` entry (`<!-- wikitool:reference-commands -->`), the overview in `docs/reference/commands/README.md` (`<!-- wikitool:reference-commands-index -->`), and the gates and exit-42 causes in `docs/reference/exit-codes.md` (`<!-- wikitool:reference-exit-codes -->`). - Also, in the origin repo only, the skill catalogue `docs/reference/skills.md` (`<!-- wikitool:reference-skills -->`: one row per shipped `instructions/<name>/SKILL.md`, its description split at the sentence starting "Use when") and the page types `docs/reference/page-types.md` (`<!-- wikitool:reference-page-types -->`: one section per `root: kb` type-spec, from what `types describe` reads). Both describe the stack's shipped defaults. In an instance - a tree with `.wikitool-release.json` - they are left alone and the output says so: an instance's adopted type-specs and own instructions may differ, and rewriting a shipped page there would block the next `dist upgrade`. - Also the shapes of the four hand-written configuration files in `docs/reference/configuration.md`, one region each (`<!-- wikitool:reference-config-telemetry -->`, `-remotes`, `-tasks`, `-upload`) inside the hand-written section on that file, rendered from the records in `chemenu.config_shapes` the loaders read the files with. Those records ship with `tools/`, so these regions are written and checked in an instance as well. Several regions in one page are refreshed one after the other into the same text. - Renders each command's summary, synopsis, options, examples, notes, properties in words, exit codes with their causes and see-also - but not the ON FAILURE reactions or NEVER, which address an agent; `tools/CONTRACT.md` keeps those. - Refreshes the table of contents of each page it writes in the same step. - Never creates a page or places a region: a page's title and one-sentence head are written by hand. A group's page is named after the group - lower case, every run of other characters one hyphen. - Dry-run by default (lists the pages that would change); `--apply` writes. - `docs verify` checks the result stays current. **SEE ALSO** - `wikitool docs contract` - the same records, rendered for an agent into `tools/CONTRACT.md` - `wikitool docs verify` - checks every reference region is current - `tools/README.md` § Adding a command - how a command gets its record ### Telemetry #### `eval sessions` List the sessions that have a trace under `reports/telemetry/`. **SYNOPSIS** - `wikitool eval sessions [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool eval sessions` - `tools/wikitool eval sessions --json` **EXIT STATUS** - 0 success **NOTES** - Lists the sessions that have a trace under `reports/telemetry/`, most recent first. - Never fails; an empty list is a valid answer. - Read-only and exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool eval score` - scores one of them - `docs/explanation/telemetry-and-evaluation.md` - how telemetry and evaluation work #### `eval score` Score one traced session. **SYNOPSIS** - `wikitool eval score [--session <id>] [--json] [--markdown out.md] [--save] [--fail-on-error]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only, apart from the files `--save`/`--markdown` write - budget: exempt - network: no **EXAMPLES** - `tools/wikitool eval score` - `tools/wikitool eval score --session wiki-1727330000 --save` **EXIT STATUS** - 0 success - 1 No trace exists for the named session - 1 Only with `--fail-on-error`: the tree has hard errors or an invariant was violated **ON FAILURE** - No trace exists for the named session -> Run `eval sessions` to see which ids exist; check with `doctor` whether telemetry is on - Only with `--fail-on-error`: the tree has hard errors or an invariant was violated -> Act on the scorecard; re-run only to re-measure **NOTES** - Scores one traced session: structural state from `lint`'s own checks (L1) plus trajectory rules over the trace (L2) - was a refused call repeated unchanged, was a gate flag passed without that gate having refused anything, did a publish of `kb/` pages go unlogged. - Defaults to the current session; `--session <id>` picks another. - `--save` writes `reports/evals/<date>/<session>.{json,md}`; `--markdown` writes the report to the named file. - A session records nothing when telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in (`wikitool doctor` says which) - so an absent trace is not necessarily a fault. - Read-only over `kb/`, safe to retry, and exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool eval sessions` - which session ids exist - `docs/explanation/telemetry-and-evaluation.md` - the scoring levels ### Distribution and versioning #### `dist export` Write a contentless, distributable copy of this repo's machinery. **SYNOPSIS** - `wikitool dist export <target> [--dry-run] [--source-repo U] [--source-commit SHA] [--release-url U] [--update-url U]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - nothing is written until every file is planned - budget: counted - network: no **EXAMPLES** - `tools/wikitool dist export ../my-wiki --dry-run` - `tools/wikitool dist export ../my-wiki` **EXIT STATUS** - 0 success - 1 `<target>` exists and is not empty, or is not a directory - 1 The tree has no readable `VERSION` - 1 A licence file is missing from the source tree - 1 The plan would carry this instance's own data (a filled personalization, conventions or page type-spec file) **ON FAILURE** - `<target>` exists and is not empty, or is not a directory -> Point `<target>` at an empty (or new) directory and retry - The tree has no readable `VERSION` -> Fix `VERSION`, then retry - A licence file is missing from the source tree -> Restore it, then retry - The plan would carry this instance's own data (a filled personalization, conventions or page type-spec file) -> Report it: it is an allowlist bug in `dist export`, not something to work around by deleting files from the target **NEVER** - Never merge an export into a non-empty directory by hand. **NOTES** - Writes a contentless, distributable copy of this repo's machinery into an empty or new `<target>` directory. - Ships `AGENTS.md`/`README.md`/`INSTALL.md` with any `dist:strip-start`...`dist:strip-end` marker region removed, `instructions/` (minus `instructions/dev/`), `types/` (the `root: kb` page type-specs, their schemas and their subtype templates `types/<type>.<value>.md` re-keyed as `.template`, the stack's own verbatim), `docs/` verbatim (minus `docs/dev/`), `tools/` (no venv/caches, and not the dev-only test suite `tools/chemenu/tests/` with its `pytest.ini`/`.coveragerc`), the `.github/hooks/`+`.vibe/` session-tracing config plus `.claude/settings.json`, `kb/CONTRACT.md` (no pages, no areas) and `VERSION`. - Ships `raw/` and `incoming/` as flat roots, each with a `.gitkeep` and no subdirectories. `incoming/.gitkeep` is trackable and survives becoming a git repository, so a plain clone gets the directory without any bootstrap step. - Ships templates, never the filled files: `USER.md.template`/`SOUL.md.template`, `kb/CONVENTIONS.md.template`, and each collection's contract re-keyed as `kb/<name>/COLLECTION.md.template`. The filled `USER.md`/`SOUL.md`/`kb/CONVENTIONS.md`/`kb/<name>/COLLECTION.md`/`types/<page-type>.md`/`types/<page-type>.<value>.md` bind their instance; `find_leaks` refuses a plan carrying one. - Writes a generated `.wikitool-release.json` stamp: version, export date, origin, and a sha256 per exported file - the base a later upgrade compares against. - The four origin options only fill stamp fields: `export` never calls git and cannot discover them. - A build and test tool: every release is an export packed as a tarball, and an instance is installed from such a release, never from an export directly. - One-way: no command reconstructs a distributed instance into a dev instance - work on the stack in a clone of the origin repo. - `--dry-run` lists every file it would write, and writes nothing. **SEE ALSO** - `instructions/setup-instance.md` - installs a release, which is this export as a tarball - `wikitool dist upgrade` - applies a later export to an existing instance - `wikitool version show` - reads the stamp this writes #### `dist adopt` Take shipped templates as this instance's own: copy each to its unsuffixed name. **SYNOPSIS** - `wikitool dist adopt [<template>...] [--dry-run]` **PROPERTIES** - effect: write - idempotent: yes - atomic: No - files are copied one by one; a re-run completes an interrupted one - budget: counted - network: no **EXAMPLES** - `tools/wikitool dist adopt` - `tools/wikitool dist adopt types/project.md.template types/project.schema.yaml.template` - `tools/wikitool dist adopt --dry-run` **EXIT STATUS** - 0 success - 1 A named path does not exist, or is not a collection contract or page type-spec template **ON FAILURE** - A named path does not exist, or is not a collection contract or page type-spec template -> Not transient - name a template from the set in NOTES, or call it without a path **NEVER** - Never delete a target to make `dist adopt` replace it - a filled file is the instance's own work. **NOTES** - Copies `<name>.template` to `<name>` byte for byte; the template stays where it is, as the base the next `dist upgrade` compares against. - Without a path: every `kb/<collection>/COLLECTION.md.template` and every `types/*.template` (the `root: kb` page type-specs, their schemas and their subtype templates). - With paths: exactly those templates, each of which has to be one of the set above. - Never overwrites: a target that already exists is reported as kept and left untouched. - Not for `kb/CONVENTIONS.md.template` or the personalization templates - those carry a sentinel and are filled in, not copied. - `--dry-run` lists what it would copy and keep, and writes nothing. **SEE ALSO** - `instructions/setup-instance.md` - adopts every template on a fresh instance - `instructions/upgrade-instance.md` - adopts a template a release added - `wikitool dist export` - re-keys these files as `.template` in the first place #### `dist upgrade` Apply a stack update `dist export` produced - the write half of `version check`. **SYNOPSIS** - `wikitool dist upgrade (<source> | --latest [--expect <version>]) [--dry-run] [--keep-local] [--take-release <path>]... [--prune] [--pre]` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes for every refusal - nothing is written. Once writing starts it is a plain sequential file copy with no partial-state cleanup: an interruption mid-copy (killed process, disk full) can leave the tree part-old, part-new - budget: counted - network: yes **EXAMPLES** - `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --dry-run` - `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz` - `tools/wikitool dist upgrade ../chemenu-7.1.0.tar.gz --take-release tools/README.md` - `tools/wikitool dist upgrade --latest --expect 8.0.0 --dry-run` **EXIT STATUS** - 0 success - 0 The source's version equals the installed one - a no-op success - 1 Both `<source>` and `--latest`, or neither; or `--expect` without `--latest` - 1 Local `VERSION` missing, or no local `.wikitool-release.json` with a `files` block - 1 `.wikitool-kb.json` is missing - 1 A migration is already outstanding against the *installed* machinery - 1 The working tree is dirty - 1 `<source>` does not exist, fails its `.sha256`, or does not unpack to exactly one top-level directory - 1 `--latest`: the release feed cannot be reached, or answers with something that is not a release - 1 `--latest --expect`: the feed's latest release is another version than the one expected; nothing was downloaded - 1 `--latest`: the release publishes no archive or no `.sha256` under the expected name; nothing was downloaded - 1 `--latest`: an asset download fails, or the archive fails its `.sha256` - 1 `--latest`: the downloaded archive's `VERSION` is not the version the feed announced - 1 The source carries no `VERSION`, `.wikitool-release.json` or `files` block - 1 The source's version is older than the installed one, or a pre-release without `--pre` - 1 A `--take-release` path this run does not classify as locally changed - the one refusal a `--dry-run` also raises - 1 Locally changed files that neither `--keep-local` nor a `--take-release` answers for; nothing was written **ON FAILURE** - Both `<source>` and `--latest`, or neither; or `--expect` without `--latest` -> Not transient - name exactly one source, and pass `--expect` only with `--latest` - Local `VERSION` missing, or no local `.wikitool-release.json` with a `files` block -> Not transient - fix the named precondition and retry. A clone of the origin repo is a development checkout and takes no `dist upgrade` at all - `.wikitool-kb.json` is missing -> Run `wikitool migrate baseline <version>`, then retry - A migration is already outstanding against the *installed* machinery -> Finish it first - `wikitool migrate status` names it - then retry - The working tree is dirty -> Commit or stash first, then retry - `<source>` does not exist, fails its `.sha256`, or does not unpack to exactly one top-level directory -> Fix the path or re-download the release archive, then retry - `--latest`: the release feed cannot be reached, or answers with something that is not a release -> Transient - retry once; then report the URL from the message, or take the release page's archive by hand and pass it as `<source>` - `--latest --expect`: the feed's latest release is another version than the one expected; nothing was downloaded -> Not transient - read `wikitool version notes` for the version the message names, then either expect that one or stop - `--latest`: the release publishes no archive or no `.sha256` under the expected name; nothing was downloaded -> Not transient - the message lists the assets present and the release page; report it there rather than upgrading without the checksum - `--latest`: an asset download fails, or the archive fails its `.sha256` -> Retry once; if it fails again, report the exact message - do not fall back to an unchecked archive - `--latest`: the downloaded archive's `VERSION` is not the version the feed announced -> Not transient - an inconsistent release; report it against the release page - The source carries no `VERSION`, `.wikitool-release.json` or `files` block -> Point `<source>` at a distribution export, then retry - The source's version is older than the installed one, or a pre-release without `--pre` -> Not transient - choose another source, or pass `--pre` for a pre-release - A `--take-release` path this run does not classify as locally changed - the one refusal a `--dry-run` also raises -> Correct it against the locally-changed list the refusal prints, then retry - Locally changed files that neither `--keep-local` nor a `--take-release` answers for; nothing was written -> Choose one of the three answers the refusal names, re-run lines filled in: `--take-release <path>` to write the release's version over it (ends the divergence), `--keep-local` to leave them untouched (reported again on every later run until they stop diverging), or reconcile by hand and retry **NEVER** - Never treat any of the three answers to locally changed files as the default. **NOTES** - Exactly one of `<source>` and `--latest` names the release. `<source>` is an already-fetched export directory or `.tar.gz` release archive and touches no network: the archive is verified against a sibling `.sha256` if one is present (a missing one is a WARN, not a block) and must unpack to exactly one top-level directory - the shape `.gitea/workflows/release.yml` packs. - `--latest` asks the release feed (`update_url` of the local stamp, overridable with `$WIKITOOL_UPDATE_URL`; `$WIKITOOL_UPDATE_TOKEN` is sent along) which release is latest, checks that version - `--expect`, a downgrade, a pre-release without `--pre`, already installed - before any download, then downloads the archive and its `.sha256` into a scratch directory removed on every exit. The checksum is mandatory here (a release without one, or an archive that fails it, is an error, not a WARN) and the archive's own `VERSION` must equal the feed's version. The asset URLs are the feed's own `browser_download_url` values, and the token reaches an asset download only if it is on the feed's origin. The checksum protects against transfer errors, not against a feed that is itself compromised - authenticity is the trust in the feed's host. - `--expect <version>` (only with `--latest`) pins the release the feed may announce: a different latest version is refused before anything is downloaded. Pass the version `wikitool version notes` was read for, in the dry run and in the real run alike. - The write set is exactly the *new* `.wikitool-release.json`'s `files` block, minus what an export re-seeds from a blank template every time (`kb/log.md`, `raw/.gitkeep`) or seeds once and the instance owns from then on (`.wikitool-kb.json`, `CHANGES.md`), plus the stamp itself, always rewritten. - Every candidate path is classified against the *local* `.wikitool-release.json`'s recorded digest: unchanged is overwritten silently, absent from the old stamp is created, and locally modified or locally deleted is **never** silently overwritten. - Locally changed files abort the run with the full list; the abort text names the three answers with the command line filled in, and none of them is the default. - `--keep-local` proceeds and leaves every locally changed file untouched. The new stamp is still written whole, recording the release's digest for files that were not written, so a skipped file keeps diverging and is reported again on every later run. - `--take-release <path>` (repeatable) writes the release's version over the named path, discarding the local change, re-creates it if it was locally deleted, and deletes it if the release no longer ships it. The path then matches the stamp and stops being reported. - The two are decided per path and compose on one call: without `--keep-local`, a locally changed path that no `--take-release` names still aborts the run. - A path in the old stamp but not the new one is no longer part of the release. Still unchanged since installation, it is deleted, as silently as an unchanged file is overwritten; changed since, it is locally changed like any other and needs one of the same answers - `--take-release` deletes it, `--keep-local` keeps it as the instance's own file, which no later upgrade reports again. Either way it is decided in this run: the new stamp no longer names the path. What the instance owns (`kb/log.md`, `CHANGES.md`, `.wikitool-kb.json`, ...) is never a candidate, and neither is any file the old stamp does not name. - A directory left holding nothing - at most a `__pycache__/` of `*.pyc` - once those files are deleted is removed with them, up to but never including the instance root. `--prune` is accepted and ignored; it used to switch the deletion on. - Reports the migration chain the new machinery would owe, but never runs any of it - there is no `migrate run`. - Reports, but does not block on, a crossed compatibility boundary. - The local preconditions - `VERSION`, the release stamp, `.wikitool-kb.json`, no outstanding migration, a clean working tree - are checked before the source is read. Not being a git repository at all is a WARN, not a refusal. - Never touches git - no commit, no push. - `--dry-run` classifies and reports without writing; a pre-release (`-beta.N`) source needs `--pre`. With `--latest` it still downloads and verifies the archive - that is the only way to classify - and removes it again. - An interrupted write is not resumed automatically: compare the tree against the printed classification and finish or revert by hand. - The closing report names `instructions/upgrade-instance.md`, which carries the order for everything after the swap and resumes at `instructions sync`. **SEE ALSO** - `wikitool version check` - finds out whether an update exists - `wikitool version notes` - the notes of the release `--expect` should name - `instructions/upgrade-instance.md` - the order after the swap - `docs/how-to/update-an-instance.md` - which release, whether to take it, where the tarball comes from - `wikitool migrate status` - the migrations the report names #### `version show` Print this instance's stack version and where it came from. **SYNOPSIS** - `wikitool version show [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool version show` - `tools/wikitool version show --json` **EXIT STATUS** - 0 success - 1 `VERSION` is missing or unparseable **ON FAILURE** - `VERSION` is missing or unparseable -> Fix `VERSION` and retry **NOTES** - Prints `VERSION` and where this tree came from: "development tree" without a release stamp, otherwise the export date, source commit and repository from `.wikitool-release.json`, plus the release page when the stamp records one. - `--json` prints the version, its compatibility key, the stamp and the update URL. - Bare `wikitool version` is an alias for this. - Read-only and offline; exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool version check` - asks whether a newer stack exists - `wikitool version notes` - prints a version's release notes #### `version check` Ask the origin's release feed whether a newer stack exists. **SYNOPSIS** - `wikitool version check [--url U] [--timeout S] [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only, no local writes - budget: exempt - network: yes **EXAMPLES** - `tools/wikitool version check` - `tools/wikitool version check --json` **EXIT STATUS** - 0 success - 1 `VERSION` is missing or unparseable - 1 The feed could not be reached, answered non-JSON, or carried no `tag_name` **ON FAILURE** - `VERSION` is missing or unparseable -> Fix `VERSION` and retry - The feed could not be reached, answered non-JSON, or carried no `tag_name` -> A network failure is transient - retry once, then report it. HTTP 401/403 names `$WIKITOOL_UPDATE_TOKEN`; 404 means no release exists yet or the URL points at the wrong repo **NEVER** - Never report "up to date" for a check that failed - an unreachable feed is not that answer. **NOTES** - Asks the release feed for its latest release and compares it with `VERSION`: `state` is `current`, `update`, `migration` (the step crosses a compatibility boundary) or `ahead`. - The only command whose whole job is the network call - `version notes` reaches the same feed too, but only as a fallback on a distributed instance, and `dist upgrade --latest` asks it which release to download. - Never reached implicitly from another command (`dist upgrade` asks only when passed `--latest`), needs no key, and times out after `--timeout` seconds (default 10). - The feed is `--url`, else `$WIKITOOL_UPDATE_URL`, else the release stamp's, else the built-in origin. `$WIKITOOL_UPDATE_TOKEN` is only needed if that feed is not readable anonymously. - For `update` or `migration` it prints that applying the release is a separate, manual step (`docs/how-to/update-an-instance.md`). - Read-only; exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool dist upgrade` - applies the update this reports - `wikitool version notes` - prints the release notes - `docs/how-to/update-an-instance.md` - what the operator decides before an update #### `version notes` Print one version's release notes. **SYNOPSIS** - `wikitool version notes [--version X.Y.Z] [--offline] [--url U] [--timeout S]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: yes **EXAMPLES** - `tools/wikitool version notes` - `tools/wikitool version notes --version 7.0.0` - `tools/wikitool version notes --offline` **EXIT STATUS** - 0 success - 1 `--version` is unparseable, `VERSION` is unreadable when `--version` is omitted, or `CHANGES.md` is missing - 1 No entry for the requested version in a tree with no release stamp (a dev checkout) - 1 No entry for the requested version, and `--offline` was passed - 1 No entry for the requested version, and the feed could not be reached or returned a release with an empty `body` **ON FAILURE** - `--version` is unparseable, `VERSION` is unreadable when `--version` is omitted, or `CHANGES.md` is missing -> Fix the named argument or file, then retry - No entry for the requested version in a tree with no release stamp (a dev checkout) -> Write the entry, or run `version bump` - No entry for the requested version, and `--offline` was passed -> Read the release page the error names - No entry for the requested version, and the feed could not be reached or returned a release with an empty `body` -> A feed failure is transient - retry once, then read the release page the error names **NOTES** - Prints the `CHANGES.md` entry for `--version` (default: this tree's `VERSION`). - With no such entry, in a tree with a release stamp (a `dist export` tree) and without `--offline`, it prints the release feed's latest release notes instead. A tree without a stamp (a dev checkout) never asks the feed. - Only the feed's latest release can be asked for. When that is a different version than requested, stderr names it and the notes are printed anyway - the expected case before an upgrade, where `VERSION` still names the release being left. - stdout carries nothing but the notes; the line naming the feed being asked and the one naming the release that answered go to stderr. `release.yml` redirects stdout into the file it posts as the release body. - `--offline` never asks the feed and fails with the stamp's `release_url` instead. - Every failure names the stamp's `release_url` where it has one, so a run that cannot read the notes is still told where they are. - Read-only, safe to retry, and exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool version check` - asks the same feed whether a newer stack exists - `wikitool version bump` - writes the entry this prints #### `version bump` Raise or continue the one running candidate between two releases. **SYNOPSIS** - `wikitool version bump --major|--minor|--patch --title "<...>" [--impact high|medium|low] [--breaking "<what breaks>"] [--no-migration "<reason>"] [--migration-required] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - `VERSION` then `CHANGES.md` - budget: counted - network: no **EXAMPLES** - `tools/wikitool version bump --patch --title "Fix the lint report path" --impact low` - `tools/wikitool version bump --minor --title "New command: wikitool review" --dry-run` - `tools/wikitool version bump --major --title "Rename --gate-file to --remotes-file" --breaking "--gate-file is gone; scripts must pass --remotes-file" --no-migration "no content changes"` **EXIT STATUS** - 0 success - 1 Not exactly one of `--major`/`--minor`/`--patch`, an empty `--title`, or an unknown `--impact` - 1 `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions - 1 An escalation to a boundary crossing without `--breaking`, or with neither a migration document targeting the new base nor `--no-migration` - 1 `--breaking` or `--no-migration` on a bump that crosses nothing - 1 `--no-migration` while a required migration document targets the new base - 1 `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to replace, or without a required migration document already targeting the new base **ON FAILURE** - Not exactly one of `--major`/`--minor`/`--patch`, an empty `--title`, or an unknown `--impact` -> Nothing was written - fix the argument and retry once - `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions -> Nothing was written - fix whichever is wrong, then retry - An escalation to a boundary crossing without `--breaking`, or with neither a migration document targeting the new base nor `--no-migration` -> Nothing was written - add what the error asks for, or, if nothing actually breaks, choose a smaller part - `--breaking` or `--no-migration` on a bump that crosses nothing -> Nothing was written - drop the flag and retry - `--no-migration` while a required migration document targets the new base -> Nothing was written - drop `--no-migration`, or remove the document if it is wrong, then retry - `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to replace, or without a required migration document already targeting the new base -> Nothing was written - write the migration document or fix the combination, then retry **NEVER** - Never re-run after an uncertain outcome without first reading `VERSION` and the top of `CHANGES.md` - a second run escalates or continues the candidate again. - Never hand-edit `VERSION` or the machine-written parts of the entry (heading, bump list, breaking and migration lines); `--migration-required` is the only way to take a `none required` line back. **NOTES** - `VERSION` gets a `-beta.N` suffix: one running candidate between two releases, never a fresh number per bump. - `--major`/`--minor`/`--patch` is **max-wins escalation** against the last release (patch < minor < major): a `--patch` on a MINOR candidate only advances `N`, and escalation never steps back down. - The first bump of a candidate opens its `CHANGES.md` entry - heading, date, author, and a machine-managed `<!-- wikitool:bumps -->` list of every `--title` collected so far, graded by `--impact` (default `medium`). Every later bump of the same candidate updates that entry in place: one entry per candidate, not one per bump. - The bump list renders grouped under `**High/Medium/Low impact**` headings, empty groups omitted - except while every bump is `medium`, where it stays one flat list. `version regrade` corrects a grade after the fact. - Writes the heading, the bump list and the breaking/migration lines; the entry's prose - a summary and one-paragraph `###` sections, one per topic - is left to the author. - Whenever a required migration document (`migrates_to:` equal to the candidate's base) exists, every bump writes or refreshes `**Migration:** required - <paths>`, the documents' paths sorted and comma-separated, in place of any `none required` line. The two forms never stand side by side. - Compatibility follows the **leftmost non-zero component** of the candidate's base, which for this stack (at `1.0.0` and up) means MAJOR: PATCH is a fix, MINOR a compatible capability, MAJOR a version that is **not a drop-in replacement** - any hand-work on update, or a downgrade that no longer works. Whether content must be migrated is a second, independent question. - The bump that first escalates a candidate past the boundary requires `--breaking "<what stops working>"` and, on top of it, a migration document targeting the candidate's base or `--no-migration "<reason>"`. Both are anchored just above the bump list, persist over later bumps of the same candidate without being repeated, and are refused on a bump that crosses nothing at all. - On a later crossing of the same candidate, a further `--breaking` **joins** the reasons already recorded (flat on the marker line while there is one, bullets under a bare marker from the second on; repeating a reason verbatim is a no-op), while a further `--no-migration` **replaces** the single migration line. - `--migration-required` replaces the running candidate's `--no-migration` line with the `required` line; it needs a required migration document already targeting the new base. Nothing retracts a recorded `--breaking` reason. - Enforces that a crossing documents itself, never that the part was chosen correctly. - Not idempotent: every successful run escalates or continues the candidate again. - `--dry-run` reports the step without writing. **SEE ALSO** - `wikitool version regrade` - corrects an `--impact` grade - `wikitool version release` - fixes the candidate into a release - `instructions/migrate-corpus.md` - how a migration document is written #### `version regrade` List the running candidate's bump titles with their impact grade, or change one or more of them. **SYNOPSIS** - `wikitool version regrade [INDICES...] [--impact high|medium|low]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - `CHANGES.md` only, and only when indices are given - budget: exempt_without_args - network: no **EXAMPLES** - `tools/wikitool version regrade` - `tools/wikitool version regrade 3 7 --impact high` **EXIT STATUS** - 0 success - 1 `VERSION` or `CHANGES.md` is missing, `VERSION` and the changelog's newest entry name different versions, or the topmost entry has no bump list - 1 An index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact` **ON FAILURE** - `VERSION` or `CHANGES.md` is missing, `VERSION` and the changelog's newest entry name different versions, or the topmost entry has no bump list -> Nothing was written - fix whichever is wrong, then retry - An index outside the rendered list's range, indices given without `--impact`, or an unknown `--impact` -> Nothing was written - list again with the bare command, then retry once with corrected arguments **NEVER** - Never re-run the same indices after a write without listing again first - the positions may have moved. **NOTES** - Without arguments: lists the running candidate's bump titles with their grade, numbered by 1-based position in the rendered list (High before Medium before Low, chronological within a grade). Read-only and exempt from the Iteration Budget Gate. - With indices and `--impact`: sets the grade of every named position in one call, all resolved against a single read of the current list - `version regrade 3 7 --impact high` grades what is at 3 and 7 now, not 7 after 3 has moved. Writes `CHANGES.md` and is counted by the Iteration Budget Gate. - The correction path for an `--impact` grade judged at bump time. - Touches only the topmost entry's bump list - never `VERSION`, never any other part of `CHANGES.md`. - A write is not idempotent against a changed list: after a first success, the same indices may name different bumps. **SEE ALSO** - `wikitool version bump` - sets the grade in the first place - `wikitool version release` - refuses without a summary above this list #### `version release` Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHANGES.md` entry. **SYNOPSIS** - `wikitool version release [--title "<...>"] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: No - `VERSION` then `CHANGES.md` - budget: counted - network: no **EXAMPLES** - `tools/wikitool version release --dry-run` - `tools/wikitool version release --title "Command records rewritten for agents"` **EXIT STATUS** - 0 success - 1 `VERSION` is already a release - there is no running candidate to fix - 1 `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions - 1 Two or more bumps and no summary paragraph above the `###` sections - 1 The entry is over 32 000 bytes, or a `###` section has more than one paragraph or a paragraph over 1 000 bytes - the error names each with its size **ON FAILURE** - `VERSION` is already a release - there is no running candidate to fix -> After an uncertain run this means it already ran; otherwise there is nothing to release - `VERSION` or `CHANGES.md` is missing, or `VERSION` and the changelog's newest entry name different versions -> Fix whichever is wrong, then retry - Two or more bumps and no summary paragraph above the `###` sections -> Write a short summary paragraph right below the bump list, then retry - The entry is over 32 000 bytes, or a `###` section has more than one paragraph or a paragraph over 1 000 bytes - the error names each with its size -> Nothing was written - shorten or merge the named sections (a smaller change keeps only its bump title), then retry **NEVER** - Never retry after an uncertain outcome without reading `VERSION` first - a release-shaped `VERSION` means it already ran. **NOTES** - Strips `VERSION`'s `-beta.N` suffix - the candidate's base becomes the release - and closes the candidate's `CHANGES.md` entry. - Without `--title` the heading keeps whichever bump last set it; `--title` replaces it - the normal case for a candidate that collected several bumps, whose entry wants a summarising heading rather than the most recent one. - Leaves the entry's machine-managed bump list untouched, as the record of what happened. - From two bumps on, requires a summary paragraph (at least 200 non-whitespace characters) between the bump list and the first `###` section heading. A candidate with exactly one bump is exempt, since there its own paragraph already is the summary. `--dry-run` runs this check too. - Refuses an entry that would exceed its budget once released: at most 32 000 bytes (UTF-8) as `version notes` prints it, and every `###` section exactly one paragraph of at most 1 000 bytes, heading line excluded. The same check as `docs verify`, run on the released heading; `--dry-run` runs it too. - Commits nothing and pushes nothing. The following `publish` moves `VERSION` onto `main`, which `release.yml` reacts to. - Not idempotent: a second run fails once the suffix is gone. **SEE ALSO** - `wikitool version bump` - opens and continues the candidate - `wikitool version regrade` - shows the bump list the summary sits under - `wikitool publish` - moves the released `VERSION` onto `main` ### Content migrations #### `migrate list` List every migration document under `instructions/migrations/`. **SYNOPSIS** - `wikitool migrate list [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool migrate list` **EXIT STATUS** - 0 success **NOTES** - Lists every migration document under `instructions/migrations/`, oldest target first, with its kind and obligation. - Never fails. Read-only and **exempt from the Iteration Budget Gate**. **SEE ALSO** - `wikitool migrate status` - which of them this instance still owes - `instructions/migrate-corpus.md` - how a migration is run #### `migrate status` Show the migrations this instance still owes, in the order they must run. **SYNOPSIS** - `wikitool migrate status [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool migrate status` - `tools/wikitool migrate status --json` **EXIT STATUS** - 0 success - 1 `.wikitool-kb.json` is missing - the content version is undeclared - 1 `VERSION` is unreadable **ON FAILURE** - `.wikitool-kb.json` is missing - the content version is undeclared -> Run `migrate baseline <version>` once, then retry - `VERSION` is unreadable -> Fix `VERSION`, then retry **NOTES** - Shows every **required** migration whose `migrates_to` lies in `(kb_version, VERSION]`, in the order it must run. - `offered` migrations are listed separately above the chain: they never block, never count as owed, and are bounded by the applied ledger rather than by `kb_version` - taking one does not move the version. - With a release stamp present, also reports which shipped files this instance has since edited (from the per-file sha256 in `.wikitool-release.json`) - which says whether an offer may be copied over or has to be reconciled by hand. Without a stamp that question is reported as unanswerable rather than answered. - Exits 1 only when the content version is undeclared (`.wikitool-kb.json` missing) or `VERSION` is unreadable; it never guesses the content's shape. - Read-only, safe to retry freely, and exempt from the Iteration Budget Gate. **SEE ALSO** - `wikitool migrate done` - records one as applied - `instructions/migrate-corpus.md` - how a migration is run - `instructions/upgrade-instance.md` - where an upgrade checks this #### `migrate verify` Compare `kb/` against a git revision on the invariants a content migration must not change. **SYNOPSIS** - `wikitool migrate verify --from <rev> [--path P ...] [--expect-body-change] [--json] [--fail-on-error]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: no **EXAMPLES** - `tools/wikitool migrate verify --from HEAD` - `tools/wikitool migrate verify --from HEAD --path kb/concepts --expect-body-change` **EXIT STATUS** - 0 success - 1 Only with `--fail-on-error`: an invariant changed - 1 `--from` is not a revision in this repository **ON FAILURE** - Only with `--fail-on-error`: an invariant changed -> Act on the findings - exit 1 here means "act on the findings", not "the tool is broken". A finding names a page and what changed on it; it is never fixed by re-running - `--from` is not a revision in this repository -> Fix the revision and retry **NEVER** - Never re-run to make a finding go away - fix the page it names. **NOTES** - Compares `kb/` against the revision `--from` on the invariants a content migration must not change: wikilink and citation **counts** (not sets), footnote definitions, H1, structural frontmatter, and the **count of generated-region marker pairs**. - Pages are matched by **title**, not path, so a page `move` (or `move --reconcile`) relocated compares as itself - reported separately as `moved` - rather than as a removed-and-added pair. - Reports added and removed pages without failing on them. - `--expect-body-change` additionally flags a page whose body did not change at all. - Not migration-specific: worth running after any bulk rewrite. - Exits 0 whatever it finds unless `--fail-on-error` is passed. - Read-only and exempt from the Iteration Budget Gate. **SEE ALSO** - `instructions/migrate-corpus.md` - where a migration runs this - `wikitool lint` - the single-revision checks #### `migrate done` Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json`. **SYNOPSIS** - `wikitool migrate done <version> [--pages N] [--dry-run]` **PROPERTIES** - effect: write - idempotent: no - atomic: Yes - single file write - budget: counted - network: no **EXAMPLES** - `tools/wikitool migrate done 7.0.0 --pages 42 --dry-run` - `tools/wikitool migrate done 7.0.0 --pages 42` **EXIT STATUS** - 0 success - 1 Unknown version, no `.wikitool-kb.json`, or nothing outstanding - 1 A *required* version that is not the next link in the chain **ON FAILURE** - Unknown version, no `.wikitool-kb.json`, or nothing outstanding -> Check `migrate status`, fix the argument, then retry once - A *required* version that is not the next link in the chain -> Run `migrate status` and apply the migrations in the order it prints **NEVER** - Never force the order of required migrations. - Never hand-edit `.wikitool-kb.json` to advance the version. **NOTES** - Records one migration as applied, advancing `kb_version` in `.wikitool-kb.json` to its target. - **Refuses any required version that is not the next link in the chain.** - An `offered` migration is recorded in the applied ledger *without* moving `kb_version` and with no ordering rule applied. Re-recording one already in the ledger is a no-op, not an error - idempotent and safe to repeat. - Not idempotent for a required migration: it advances the chain. - `--dry-run` reports without writing. **SEE ALSO** - `wikitool migrate status` - the order to apply them in - `instructions/migrate-corpus.md` - the migration procedure #### `migrate baseline` Declare `kb_version` once, for an instance predating `.wikitool-kb.json`. **SYNOPSIS** - `wikitool migrate baseline <version> [--force]` **PROPERTIES** - effect: write - idempotent: yes - atomic: Yes - single file write - budget: counted - network: no **EXAMPLES** - `tools/wikitool migrate baseline 6.2.0` **EXIT STATUS** - 0 success - 1 Unparseable version - 1 A declaration already exists and `--force` was not passed **ON FAILURE** - Unparseable version -> Fix the version and retry - A declaration already exists and `--force` was not passed -> It is almost always `migrate done` that was wanted **NEVER** - Never use `--force` to advance the version past a migration - that is `migrate done`. **NOTES** - Declares `kb_version` once, for an instance predating `.wikitool-kb.json`. - Refuses to overwrite an existing declaration without `--force`. Advancing the version after a migration is `migrate done`, which checks the chain; this command does not. - Safe to re-run with the same version. **SEE ALSO** - `wikitool migrate done` - advances the version after a migration - `wikitool migrate status` - what is owed from the declared version ### Configuration #### `config set` Set one value of the tiered settings, in the file its tier names. **SYNOPSIS** - `wikitool config set <section>.<field> <value>` - `wikitool config set <section>.<field> --ref <name|op://vault/item/field>` **PROPERTIES** - effect: write - idempotent: yes - atomic: Each file is replaced in one rename; both are checked before either is written - budget: counted - network: no **EXAMPLES** - `tools/wikitool config set tasks.stalled_waiting_days 14` - `tools/wikitool config set caldav.app_password --ref caldav_app_password` - `tools/wikitool config set caldav.username --ref op://Chemenu/caldav/username` **EXIT STATUS** - 0 success - 1 The key names no registered section or field, or is not `<section>.<field>` - 1 A plain value given for a field of tier `secret` - 1 `--ref` given for a field that takes a plain value, or a malformed reference - 1 The value is not of the field's type, or the files would not pass their check afterwards - 42 The field names a program to run or affects a gate **ON FAILURE** - The key names no registered section or field, or is not `<section>.<field>` -> Use a key from the list the message prints; `docs/reference/configuration.md` describes the sections - A plain value given for a field of tier `secret` -> Do not retry with the value. Ask the user to put it into `WIKITOOL_SECRET_<NAME>`, the secrets file or 1Password, then set the field with `--ref` - `--ref` given for a field that takes a plain value, or a malformed reference -> Give the value itself, or fix the reference to `<name>` (lower case, digits, `_`) or `op://<vault>/<item>/<field>` - The value is not of the field's type, or the files would not pass their check afterwards -> Fix the value or the problem the message names in the file it names, then re-run once - The field names a program to run or affects a gate -> Show the user the output verbatim and stop. Only the user sets this field, by editing `.wikitool-local.json` themselves **NEVER** - Never pass a secret's value on the command line - not to this command and not to any other. - Never read the secrets file or run `op read` yourself to see a value. **NOTES** - Writes a plain value of a field of tier `instance` into `wikitool.json`, and one of tier `checkout` - a private field's included - into `.wikitool-local.json`. - With `--ref`, writes a reference instead of a value into `wikitool.json`: `<name>` resolves through `WIKITOOL_SECRET_<NAME>`, then the secrets file in the user's configuration directory; `op://<vault>/<item>/<field>` resolves through the 1Password CLI. Only a secret or private field takes one. - Removes the same key from the other file, so a value never has two places, and says so. - Creates `wikitool.json` with a fresh `instance_id` the first time it writes anything; the secrets file is found by that id. - Checks both files as they would stand after the write against every registered section, and writes nothing when that fails. - Prints the key and the file it wrote, never the value. Resolves no reference. **SEE ALSO** - `wikitool doctor` - whether both files pass and every reference resolves - `docs/reference/configuration.md` - the files, the tiers and the references - `docs/how-to/use-1password-for-settings.md` - setting 1Password up as the source #### `config migrate` Move the settings out of the per-feature files of earlier versions into the tiered settings. **SYNOPSIS** - `wikitool config migrate` **PROPERTIES** - effect: write - idempotent: yes - atomic: Everything is computed and checked in memory first; the secrets file, `wikitool.json` and `.wikitool-local.json` are each replaced in one rename, read back and checked, and only then are the old files removed. Any failure before that puts every target back to the bytes it had and leaves the old files as they were - budget: counted - network: no **EXAMPLES** - `tools/wikitool config migrate` **EXIT STATUS** - 0 success - 1 An old file does not pass the check the new section applies, a target key holds a different value, or the files would not pass their check afterwards **ON FAILURE** - An old file does not pass the check the new section applies, a target key holds a different value, or the files would not pass their check afterwards -> Nothing was changed. Fix the problem the message names, then re-run once **NEVER** - Never read the secrets file or an old file yourself to see a value. - Never delete an old file by hand to make a leftover message go away - the move is what carries its settings over. **NOTES** - Moves the four per-feature settings files of earlier versions - telemetry, remotes, upload and the task tracker, each named in the migration document - into the sections `telemetry`, `remotes`, `upload`, and `tasks`, `superproductivity` and `caldav`: each value into the file its field's tier names, a secret's value into the secrets file with only a reference in `wikitool.json`. - Every setting keeps its effective value, defaults included: the old content is read with the same checks the new sections use, and a field the old file did not set is not written. - Run once per checkout, since the old files and `.wikitool-local.json` belong to one checkout. `.wikitool-tools.json` stays as it is. - Prints each key and the file it went to, plus anything the old file held that was not read before and is not carried over - never a value. - A second run, or a run with no old file, prints that there is nothing to move and changes no file. - A key the target files already hold with a different value stops the move before anything is written: decide which one stands, remove the other, run again. **SEE ALSO** - `wikitool doctor` - reports every leftover old file as a `FAIL` - `docs/reference/configuration.md` - the sections the files became - `instructions/migrations/10.0.0-settings-tiers.md` - the migration this command belongs to ### Instance health #### `doctor` Check that this instance is correctly configured. **SYNOPSIS** - `wikitool doctor [--json]` **PROPERTIES** - effect: read - idempotent: yes - atomic: Read-only - budget: exempt - network: yes **EXAMPLES** - `tools/wikitool doctor` - `tools/wikitool doctor --json` **EXIT STATUS** - 0 success - 1 At least one check reported `FAIL` (a `WARN`, e.g. no remote or no `WIKITOOL_SESSION_ID`, does not exit 1) **ON FAILURE** - At least one check reported `FAIL` (a `WARN`, e.g. no remote or no `WIKITOOL_SESSION_ID`, does not exit 1) -> Each finding names its own fix command; re-run after applying it **NOTES** - Checks dependencies (Python, ripgrep), author resolution, stack version, git identity/branch/remote, published skills, the kb/raw/reports/work/sites/instructions structure, and generated files. - Import filters (`import-filter-<name>`): one line for each `raw derive` format that reads through a package (PDF, Outlook `.msg`) - a `WARN` naming the package when it is missing, since that costs only its own format; fixed by running `tools/preflight.sh` again. - Tool paths (`tool-paths`): `.wikitool-tools.json` written by a preflight that finished, every tool `tools/prerequisites.txt` names for this platform recorded, and every recorded path still there - a `FAIL` otherwise, fixed by running `tools/preflight.sh` again (PowerShell 7: `tools/preflight.ps1`). - Install folder (`install-dir`): on Windows with long paths off, a `FAIL` when the folder holding `tools/` is longer than `tools/prerequisites.txt` allows (95 characters) - the limit the preflight enforces before it sets anything up. - PowerShell (`execution-policy`, `script-marks`; Windows only, `OK` elsewhere): a `FAIL` when the effective execution policy is `Restricted` or `AllSigned` - the line then says whether a group policy sets it, which only whoever administers the computer can change - and a `FAIL` when a script under `tools/` carries a Mark of the Web from the internet zone, which a browser download unpacked in Explorer leaves behind and `Invoke-WebRequest` plus `tar` do not. `tools/preflight.ps1` checks the same two things before it stops. - Personalization: `USER.md`/`SOUL.md` present **and** filled - a file still carrying the template's sentinel is a `FAIL`. - KB conventions: `kb/CONVENTIONS.md` present, unsentinelled, and naming all three tool-owned section headings - a `FAIL` on any of the three. - Environment note: `ENVIRONMENT.md` is optional, so absent is `OK`; a still-templated one is a `WARN`. - Legacy settings (`legacy-settings`): one `FAIL` for each per-feature settings file of an earlier version (telemetry, remotes, upload, task tracker) that is still there. Nothing reads them any more, so the settings they hold are not in effect; the fix is `tools/wikitool config migrate`. - Synced folder (`sync-folder`): a `WARN` when the git directory, links and junctions resolved, lies under a folder named by `OneDrive`, `OneDriveCommercial` or `OneDriveConsumer` (compared without regard to case on Windows) or under `~/Library/CloudStorage/` (macOS); `OK` otherwise, never a `FAIL`. - Backup (`backup`): the `backup` section absent is `OK`, malformed is a `FAIL`. Configured, a `WARN` when `<target>/git` is not a bare repository, `origin` does not push there, or - with `machine_data` true - no machine-data backup exists yet; otherwise `OK` with the age of the last machine-data backup. - Page export (`export`): the `export` section absent is `OK`, malformed is a `FAIL`; otherwise `OK` listing each target with its handler, folder and whether it runs after `publish`. - Intake channels (`upload-intake`): the channels of the `intake` section with their handler, and how many submissions wait in `quarantine/`. Absent is `OK` and means no channel - no `submit` tool, nothing to collect; malformed is a `FAIL`, and so is an `mcp-upload/` of an earlier version that still holds submissions (the message names the command that moves it). A `folder` channel whose `path` does not exist is a `WARN`. - Task tracker: the `tasks` section present, absent or malformed - absent is `OK` (no tracker configured), malformed is a `FAIL`. When `WIKITOOL_TASKS_CONFIG` is set, the profile it names is read instead of the `tasks`, `superproductivity` and `caldav` sections and the finding names it; a set variable that names no file is a `FAIL`, never `OK`. - For a configured `superproductivity` provider, the configured `access` path's own state: `access: "api"` reports whether its local REST API answers `GET /health` with a ready renderer right now, `access: "snapshot"` whether a backup file is ready. The other access path is never attempted, and neither state is ever a `FAIL`. - For a configured `caldav` provider, whether the server is reachable and Basic auth succeeds - never a `FAIL`; only a broken config block is. - Settings (`settings-instance`, `settings-checkout`, `settings-secrets`): one line per tier. `wikitool.json` and `.wikitool-local.json` each present or absent - absent is `OK` - and a `FAIL` naming file, key and the right tier when a value sits in the wrong file, a secret or a committed private field holds a plain value, or a value fails its field; a section this version does not read is a `WARN`. For the references: how many resolve, never a value - an unresolvable one is a `FAIL`, one 1Password will not hand out until a human unlocks it a `WARN`. - Session id source: `OK` for `WIKITOOL_SESSION_ID` or a registered harness variable, `WARN` only for the bare parent-pid fallback. - Telemetry: on or off and why - installation-form default, the `telemetry` section of `.wikitool-local.json`, or `WIKI_TRACE` - and the current session's count and byte total against both caps; never a `FAIL`. A `WARN` when that section is not read as written - unreadable JSON, not an object, `enabled` not a JSON boolean, a cap not a positive integer, or an unknown key - naming the field; telemetry itself drops or reinterprets such a value without a word, so this is where it shows. - Site handlers (`site-handlers`): every file in `sites/` is read against the handler schema and the rules the schema cannot state; `FAIL` for a file that cannot be used, naming the file and the reason, and `OK` listing the instance's handlers, each one that replaces a stack handler (the same host and path) named as such. - Browser component (`browser`): whether the optional Playwright component and its browser are installed in the venv - `OK` either way, never a `FAIL`; a `WARN` only when it is missing and the instance needs it: a host `fetch.routes` sends through the session (`raw login`), or a site handler that declares `render`. The user installs it with `tools/preflight.sh --with browser`. - Exits 1 only on a `FAIL`; a missing remote, session id or `VERSION` is a `WARN`, not a fault. - Read-only and exempt from the Iteration Budget Gate. **SEE ALSO** - `instructions/setup-instance.md` - the setup steps most findings point back to - `instructions/preflight.md` - what `tool-paths` and `install-dir` point back to - `docs/reference/configuration.md` - the per-checkout configuration files and the tiered settings - `docs/explanation/telemetry-and-evaluation.md` - telemetry state and caps - `docs/how-to/back-up-to-a-synced-folder.md` - what `sync-folder` and `backup` point to - `instructions/session-setup.md` - setting `WIKITOOL_SESSION_ID` <!-- /wikitool:commands --> ## Design notes - All commands operate on the real repo, so they can be run from any working directory. The root is resolved by precedence - an explicit argument, then `$CHEMENU_ROOT`, then a walk up from the package's own location - and the walk-up is the default, so `tools/wikitool` with no variable set behaves exactly as it always has. Nothing under the root is bound at import time: `KB_DIR`, `RAW_DIR` and the rest follow whatever `ROOT` currently is, which is what makes the half-repointed state (a moved `ROOT` with a stale `KB_DIR`) unconstructible rather than merely discouraged. - **The library boundary.** `chemenu.api.Corpus` is the in-process entry point: it takes a corpus root, returns the same structures the `--json` forms print, and raises `ChemenuError` where the CLI prints `ERROR` and exits 1. It is read-only *structurally* - nothing under `chemenu.commands` is imported from it, so `new`, `publish` and the rest are not reachable, rather than filtered. The cores it calls (`search/service.py`, `lint_core.py`, `types_core.py`) import no `typer` and no `rich`; the modules under `commands/` are the terminal adapters over them. A second consumer is therefore a second adapter, not a second implementation. - **The MCP server** (`chemenu/mcp/`) is that second adapter: `search`, `types`, `describe_type`, `lint` and `status` over `chemenu.api`, on `stdio` or `streamable-http`. Its dependency is optional and lives in `requirements-mcp.txt`, so a CLI-only instance does not install it. Five of its tools are structurally read-only - nothing under `commands/` is importable from the server, so `new`/`touch`/`xref`/`cite`/ `publish`/`migrate` are unreachable rather than filtered - and every response carries the commit it was computed from. The one exception is `submit` (opt-in via the `upload` section of `.wikitool-local.json` - absent means the tool is not registered at all): the server may write, but by a **positive list** rather than an absence - `chemenu.upload._write_atomic_within` resolves every target and refuses anything outside `mcp-upload/`, the one directory the process may touch. The *reviewer* commands that move a submission out of that quarantine (`upload accept`/`upload reject`) keep the original absence property: they live under `commands/`, not reachable from the server, same as every other write command. Running the server, and keeping its checkout current, is [instructions/mcp-read-server.md](../instructions/mcp-read-server.md); reviewing a submission is [instructions/ingest-queue.md](../instructions/ingest-queue.md). Authentication and rate limiting are middleware in front of the process, not code here; the Iteration Budget Gate is deliberately not applied to any of the six tools, because it bounds an agent session rather than a user. - `new`/`xref`/`log append` only produce structurally-correct frontmatter and body skeletons/edits - the prose (Description, Summary, judgment calls about relationships) is still written by the LLM afterwards. - `lint` only reports what's mechanically verifiable. Contradictions, staleness judgment, and "what's worth writing next" remain the LLM's job; `lint` produces a markdown skeleton with a "Semantic Review" section for that. - **Iteration Budget Gate / Loop-Breaker** (see the root `AGENTS.md` "Gates" section): every invocation is recorded and checked in `main()` (`cli.py`) before Typer dispatches to any subcommand, so it applies uniformly without each command needing its own opt-in. State lives in the gitignored `tools/.wikitool_session/budget.json`, keyed by `chemenu.session`'s fallback chain (`WIKITOOL_SESSION_ID`, else a registered harness session variable, else the caller's parent process id), so a new terminal/session starts with a clean budget - and a bucket whose recorded origin no longer matches the current one starts a fresh count rather than inheriting a stranger's. Default ceiling: 60 calls/session, or 3 identical calls in a row (whichever trips first). A call that left through `_util.fail()` - a rejected argument, or a read-only check reporting findings - is refunded: it declined instead of acting, and the contract's own answer to a rejected argument is "retry once", which would otherwise cost two slots for one operation. The call stays in the loop-breaker's history. `budget status` is exempt so the situation stays reportable after the gate trips; `budget reset` is not, and additionally requires `--yes`. Bypass only with `--override-budget`, and only after explicit human approval - never on the agent's own initiative. ## Tests The suite and its configuration (`chemenu/tests/`, `pytest.ini`, `.coveragerc`) exist in the origin repository only - `dist export` leaves them out, because they test that repository's own type-specs and conventions rather than an instance's. There: ```bash cd tools .venv/bin/pip install pytest pytest-cov # one time; pytest-cov is optional .venv/bin/python -m pytest -q # add --cov for a coverage report ``` Neither is in `requirements.txt`, and `pytest-cov` is CI-only by intent: `requirements.txt` is what an instance needs at runtime, and an instance does not measure this suite. ## Maintenance schedule Run by the LLM through the skills, on this cadence: | Task | Frequency | Command | |------|-----------|---------| | Log append | Every operation | `log append --op <type> --title "..."` | | Index rebuild | Every page change | `index rebuild` | | Provenance index rebuild | After any source/citation change | `sources rebuild-index` | | Publish | After any change worth persisting | `publish --message "<op>: <desc>"` | | Full lint | Every 10 sources (checked via `log status`), or on request | `lint`, then carry the semantic findings into `kb/log.md` via `log append --op lint` - the report itself is gitignored | | Raw coverage check | Every 10 sources | `sources coverage` | | Docs/instruction verification | After changing the CLI, a contract, or an instruction | `docs verify`, `instructions verify` | | Budget check | Any time a session feels long | `budget status` | | Retention review | Every 90 days | Manual | ## Future considerations (not implemented) - A pre-commit hook running `wikitool lint --fail-on-error` before every `wikitool publish`. CI already runs it on every push (`.gitea/workflows/ci.yml`), which catches it after the fact rather than before.