Files
chemenu/tools/CONTRACT.md
T
torben be78ad20af
CI / verify (push) Successful in 1m12s
Release / release (push) Successful in 36s
tools: command records, Provenance group - examples, exit lines per cause (#142)
Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/chemenu/commands/provenance_cmd.py
2026-09-26 08:51:53 +02:00

141 KiB

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 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, 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 render from that one source:

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 #### <path> records, in the same order wikitool -h prints. Reading the file end to end is for changing the CLI itself.

Contents

Setup (one time)

Full bootstrap for a fresh clone - including publishing the skills, which are not committed - is instructions/bootstrap.md. The environment alone:

cd tools
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt

Usage

Run from the repo root:

tools/wikitool <command> -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.

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    Create one open item in the configured task tracker - never a kb/ page.
task list             read  idempotent     budget:counted              exit:0,1    List a project's open items - id, title, and whether each carries the WAITING status.
task close            write idempotent     budget:counted              exit:0,1    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 <rel> 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    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 accept            write non-idempotent budget:counted              exit:0,1    Promote one or more files from `incoming/` into `raw/`.
upload list           read  idempotent     budget:counted              exit:0      List every MCP submission currently waiting in the quarantine (`mcp-upload/`).
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 `<remote>/<branch>` and bring the local branch up to date with it.
publish               write non-idempotent budget:counted              exit:0,1,42 Reconcile with `<remote>/<branch>`, then stage all changes, commit, and push.
work new              write non-idempotent budget:counted              exit:0,1    Scaffold `work/<runkey>/` 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/<name>/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,1    Create, refresh or remove the generated table-of-contents region.
docs contract         write idempotent     budget:counted              exit:0      Regenerate `tools/CONTRACT.md`'s `<!-- wikitool:commands -->` region.
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 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`.
upstream merge        write non-idempotent budget:counted              exit:0,1    Take a stack update into a private instance's branch, machinery only.
upstream verify       read  idempotent     budget:exempt               exit:0,1    Compare two revisions: did anything under a content stage change except through a stack-owned path?
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 <type-name> --name "<Name>" [--type <path>] [--set field=value ...] - Any type; its type-spec decides fields, directory, title prefix and template.
  • wikitool new entity --name "<Name>" --set entity_type=<t> [--set tags=a,b] [--set related=X,Y] [--set sources="Source - Z"] [--set provenance=sourced|general|mixed] - Writes kb/entities/<subdir>/<Name>.md
  • wikitool new concept --name "<Name>" --set concept_type=<t> ... - Writes kb/concepts/<Name>.md
  • wikitool new source --name "<Name>" --set raw_files=raw/notes/x.md,raw/notes/y.md [--set source_url=<URL>] [--set entities=A,B] [--set concepts=C,D] - Writes kb/sources/Source - <Name>.md (prefix added automatically) 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 "<Name>" --set responsibility=<bereich> [--resume] - Writes kb/gtd/<bereich>/<Name>.md and, with a task tracker configured, a same-named tracker project

PROPERTIES

  • effect: write
  • idempotent: no
  • atomic: new <type>: 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
  • gates: human-intervention-required (new project only)

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

  • The type-spec drives everything: fields, directory (base_dir/layout), title prefix, and template. types list/types describe show what a type requires.
  • 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 .wikitool-tasks.json 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 <type> - 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/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 "<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 .wikitool-tasks.json - no tracker configured
  • 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")

ON FAILURE

  • No .wikitool-tasks.json - no tracker configured -> Not transient - configure a tracker first
  • 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

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.
  • No .wikitool-tasks.json 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.
  • Never exits 42. 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/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 .wikitool-tasks.json - no tracker configured

ON FAILURE

  • No .wikitool-tasks.json - no tracker configured -> Not transient - configure a tracker first, then retry once

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 .wikitool-tasks.json - no tracker configured
  • 1 An --id matching no tracker item right now
  • 1 A read-only access path (Super Productivity's access: "snapshot")

ON FAILURE

  • No .wikitool-tasks.json - no tracker configured -> Not transient - configure a tracker first
  • 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

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.
  • No .wikitool-tasks.json 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.
  • Never exits 42.

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
  • 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 -> Choose another title and retry once
  • 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.

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 exists (a pre-existing duplicate-stem collision) - 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 exists (a pre-existing duplicate-stem collision) - refused rather than silently skipped -> Resolve the collision, 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>.
  • --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

xref add

Declare that A B.

SYNOPSIS

  • wikitool xref add --a "<A>" --b "<B>" --rel <label> [--dry-run]

PROPERTIES

  • effect: write
  • idempotent: yes
  • atomic: No - writes A then B, but both edits are idempotent, and both refusals happen before either write
  • 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

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

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:.
  • 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 cannot be read

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 cannot be read -> 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, 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.
  • 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: quote-limit overages (>2 blockquoted lines/page).
  • 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 log status - whether a full lint is due

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 .wikitool-tasks.json, or a malformed one - a clear "no tracker configured" message
  • 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

ON FAILURE

  • No .wikitool-tasks.json, or a malformed one - a clear "no tracker configured" message -> Not fixed by retrying unchanged - configure or repair .wikitool-tasks.json first
  • 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

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 .wikitool-tasks.json, never from the schema.
  • 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 accept

Promote one or more files 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 raw/<YYYY>/<MM>/, computed from the accept date rather than chosen by hand (raw/CONTRACT.md "Getting a file in"): a subdirectory under incoming/ is tolerated and ignored, not inspected - raw/ no longer addresses by type. 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 here (see types describe source; unknown is refused, backfill-only) - the one moment both are knowable. --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 - a capture field is fixed once); if that 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 it has no other owner (provenance.duplicate_raw_file_owners). The set of names occupied anywhere under raw/ - file stems and bundle directory names alike, old type directories and date shards together - must stay unique: a promote whose target name already belongs to something this call does not itself own is refused, naming both --replaces and renaming-in-incoming/ without recommending either
  • wikitool raw accept <file> --replaces <raw-path> [--fidelity <v>] [--authority <v>] [--dry-run] - The one sanctioned way past that uniqueness rule, and the one sanctioned way to correct an already-set capture field: overwrites <raw-path> in place with the single incoming file (same filename required; there is no type directory left to match), leaving every page's raw_files: untouched and writing no kb/ page - the previous edition survives only in git log --follow <raw-path>. --fidelity/--authority are optional here, and passing one overwrites the owning page's already-set value - the one path fill-once does not block, because a corrected capture is a new edition of the source, not an edit of the page describing it. Refuses if the target has more than one owning source page; if it has none, replaces anyway and says so. Cannot be combined with --page or with more than one incoming file - a replacement is one file for one file. Prints the source page (if any) and its citing pages, so their update lands in the same commit as the replacement

PROPERTIES

  • effect: write
  • idempotent: no
  • atomic: raw accept: No - one filesystem move per file, then (with --page) one page write. raw accept --replaces: No - one unlink() + one rename(), plus (if --fidelity/--authority was given) one page write
  • budget: counted
  • network: no

EXIT STATUS

  • 0 success
  • 1 raw accept: A file does not exist, is not under incoming/, or is nested more than one level below it, two files in one call share a filename, a target path already exists, --fidelity/--authority is missing (unless --replaces) or names unknown or a value outside the schema's enum, the target name is already occupied anywhere under raw/ by something the call does not own, --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, --page also given, the incoming file does not exist or is not under incoming/ (or is nested more than one level below it), its filename differs from the target's, the target does not lie under raw/ or does not exist, --fidelity/--authority names unknown or a value outside the schema's enum, or the target has more than one owning source page

ON FAILURE

  • raw accept: A file does not exist, is not under incoming/, or is nested more than one level below it, two files in one call share a filename, a target path already exists, --fidelity/--authority is missing (unless --replaces) or names unknown or a value outside the schema's enum, the target name is already occupied anywhere under raw/ by something the call does not own, --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. Safe to retry as-is once the cause is fixed: a file already at its computed destination is what "already exists" reports, not a partial prior run to resume. A stem-occupied refusal is not fixed by retrying at all - it names --replaces and renaming in incoming/ as the two routes and neither is the tool's to pick. Never choose the destination by hand instead - that is the decision this command exists to take away
  • raw accept --replaces: More than one incoming file, --page also given, the incoming file does not exist or is not under incoming/ (or is nested more than one level below it), its filename differs from the target's, the target does not lie under raw/ or does not exist, --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. Every check runs before the filesystem is touched, so a refusal leaves both files exactly as they were

NOTES

See raw/CONTRACT.md "Getting a file in: incoming/".

upload list

List every MCP submission currently waiting in the quarantine (mcp-upload/).

SYNOPSIS

  • wikitool upload list [--json]

PROPERTIES

  • effect: read
  • idempotent: yes
  • atomic: Read-only
  • budget: counted
  • network: no

EXIT STATUS

  • 0 success

NOTES

Oldest id first - id, filename, size, submitter. Only ever non-empty when .wikitool-upload.json opts a checkout into the MCP server's submit tool (see the MCP read server design note). Never fails - a submission directory with a corrupt manifest is silently skipped. Safe to retry freely.

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

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

Filename, size, sha256, submitter, submitter source (the header name, not a claim the header was honest), submission time. What a reviewer reads before accept

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

EXIT STATUS

  • 0 success
  • 1 Unknown or malformed submission id, the submission's file is missing from mcp-upload/<id>/, or incoming/<filename> already exists. Exit 42, not 1, when --confirm is absent or does not match the manifest's current token - the Upload Review Gate, not a validation error
  • 42 needs clearance - upload-review (see AGENTS.md § Gates)

ON FAILURE

  • Unknown or malformed submission id, the submission's file is missing from mcp-upload/<id>/, or incoming/<filename> already exists. Exit 42, not 1, when --confirm is absent or does not match the manifest's current token - the Upload Review Gate, not a validation error -> For exit 42: show the user the full manifest and the exact --confirm <token> re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md invariant 6). For the three exit-1 cases: fix the named argument and retry once; an occupied incoming/<filename> is not fixed by retrying unchanged - rename or clear it first

NOTES

Promote a submission's file from mcp-upload/<id>/ into incoming/, delete the quarantine directory, and append an accepted event to mcp-upload/ledger.jsonl. Without a matching --confirm, exits 42 and prints the manifest in full plus the exact re-run line - the same shape as the Mass-Update Gate's clearance, one submission at a time. The token digests id/filename/size/sha256/submitter, so an edited or superseded manifest invalidates it. Refuses (without the gate - these are ordinary validation errors) when incoming/<filename> already exists

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

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. Not idempotent against a second call with the same id: the first call already deleted the submission, so a retry reports "unknown id" - that is confirmation, not a failure

NOTES

An append-only rejected event naming the reason and the sha256 of what was declined. No gate - rejecting needs no clearance, only accepting does

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
  • budget: counted
  • network: no
  • 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, or the remote cannot be reached - reported and skipped, not a failure
  • 1 The automatic rebase hit a real conflict (git failed); it is aborted cleanly
  • 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

ON FAILURE

  • The automatic rebase hit a real conflict (git failed); it is aborted cleanly -> Do not retry and do not force - resolve the conflict manually, then re-run
  • 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

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.
  • 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; either one moving makes it stale.
  • Makes no commit, no push, and no forced operation of any kind.
  • 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, but every gate runs before staging
  • budget: counted
  • network: no
  • 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
  • 1 git failed - git add, git commit, git push, or the reconcile's automatic rebase
  • 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 --yes/-y was passed - the flag does not exist and fails with an explicit error
  • 1 .wikitool-remotes.json is unreadable or has no usable allowed_push_urls list
  • 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: .wikitool-remotes.json exists and the push URL of --remote is not listed in it, or --remote resolves to no push URL

ON FAILURE

  • git failed - git add, git commit, git push, or the reconcile's automatic rebase -> 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
  • --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
  • .wikitool-remotes.json is unreadable or has no usable allowed_push_urls list -> Show the error to the user and stop - a malformed file is not permission, and fixing or deleting it is theirs to do
  • 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: .wikitool-remotes.json exists 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 that file

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 .wikitool-remotes.json 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.
  • 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.
  • 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 remote that cannot be reached is not read as lacking the branch: on a clean tree publish reports "Nothing to commit" and attempts no push.
  • 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 gate is evaluated before anything is staged, so a refused publish leaves the working tree untouched.
  • 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). The refusal line accounts for both, by reason.
  • The --confirm token covers each counted path, its contents and the publish target: a different file list or edited contents need a new clearance.
  • Publish-Remote Gate: when the checkout carries .wikitool-remotes.json 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 file means unrestricted; a malformed one is an error, not permission.
  • --path (repeatable) scopes the whole operation - gate count, staging and commit - to that subtree.
  • 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

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

EXIT STATUS

  • 0 success
  • 1 Neither or both of --input/--key given, --input outside raw/, a --key that is empty or starts with ingest-, or the workshop already exists

ON FAILURE

  • Neither or both of --input/--key given, --input outside raw/, a --key that is empty or starts with ingest-, or the workshop already exists -> A collision is not transient: resume the existing run instead, or pass --again if the tree itself changed. Never create a numbered variant by hand

NOTES

Refuses a collision instead of suffixing it, and writes the required README.md + 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. --again opens a dated second pass over a tree that has itself changed. See work/CONTRACT.md

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

EXIT STATUS

  • 0 success
  • 1 Unknown run key, or --yes was not passed

ON FAILURE

  • Unknown run key, or --yes was not passed -> For "not confirmed": check the listed files are no longer needed, confirm the conclusions are in kb/, then re-run with --yes

NOTES

Lists what would be lost and requires --yes, because nothing in it is recoverable from the rest of the repo - the durable conclusions must already be in kb/

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

EXIT STATUS

  • 0 success

NOTES

Recent command history is never counted against the budget. Never fails. Safe to retry freely.

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

EXIT STATUS

  • 0 success
  • 1 --yes not passed

ON FAILURE

  • --yes not passed -> Get the user's approval, then re-run with --yes

NOTES

Requires --yes: clearing the counter is itself a way around the gate, so it needs the same explicit human approval

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

EXIT STATUS

  • 0 success

NOTES

Name, schema path, subtype field, and description - discover what page types exist without reading types/*.md directly. Never fails. Safe to retry freely.

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

EXIT STATUS

  • 0 success
  • 1 Unknown type name

ON FAILURE

  • Unknown type name -> Fix the name and retry

NOTES

Required/optional frontmatter fields with enums, its subtype field (if any), and its authoring body - composed with the stack-owned types/<name>.guidance.md where the type-spec declares guidance: (--json reports it separately as guidance/guidance_path, absent for a type with none), so a root: kb type's contract reads as one answer even though it may live in two files. A type-spec (or its guidance file) over the docs toc threshold carries a generated table-of-contents region; it is stripped from this output rather than echoed, since the whole body is being handed over and a navigation aid into it would be noise

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

EXIT STATUS

  • 0 success
  • 1 No skills found under instructions/, or a target directory is not a published skill (no SKILL.md) and --force was not passed

ON FAILURE

  • No skills found under instructions/, or 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; otherwise fix the named cause and retry

NOTES

Publish every instructions/<name>/SKILL.md into .agents/skills/ and .claude/skills/ as copies, and delete published skills whose source is gone. Both targets are gitignored, so a fresh clone runs this once - see instructions/bootstrap.md. Re-running is also how a drifted copy is repaired: 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)

instructions verify

Check the instruction layer.

SYNOPSIS

  • wikitool instructions verify

PROPERTIES

  • effect: read
  • idempotent: yes
  • atomic: Read-only
  • budget: counted
  • network: no

EXIT STATUS

  • 0 success
  • 1 Nothing found under instructions/ at all, a malformed instruction or SKILL.md, a SKILL.md carrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, for manual: true, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under instructions/dev/ referenced from outside it and outside a dist:strip block

ON FAILURE

  • Nothing found under instructions/ at all, a malformed instruction or SKILL.md, a SKILL.md carrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, for manual: true, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under instructions/dev/ referenced from outside it and outside a dist:strip block -> Fix the flagged file, then re-run. For a relative link in a SKILL.md, rewrite it as a repo-root-relative plain path instead. For drift, re-run sync instead of hand-editing the published copy - the source under instructions/ always wins

NOTES

Flat instructions validate against types/instruction.schema.yaml, 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 - see instructions/CONTRACT.md § "A skill's outbound reference is a plain path, not a link"), every published copy is byte-identical to its source, no instruction is left that nothing references, and nothing under instructions/dev/ is referenced from outside it (a <!-- dist:strip-start/end --> block is exempt - see instructions/CONTRACT.md). Missing every copy is reported as "run sync", not as drift - that is a clean checkout

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

EXIT STATUS

  • 0 success

NOTES

This is how the layer is discovered; search deliberately covers kb/ only. Never fails - an empty instructions/ prints "No instructions found." Safe to retry freely.

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

EXIT STATUS

  • 0 success
  • 1 A command, contract, or type-form mismatch was found, a type-spec's own frontmatter fails its schema, a shipped .md/.template cites an issue number, a reference file's table-of-contents region is missing or stale, or a reference file's relative markdown link does not resolve to an existing file

ON FAILURE

  • A command, contract, or type-form mismatch was found, a type-spec's own frontmatter fails its schema, a shipped .md/.template cites an issue number, a reference file's table-of-contents region is missing or stale, or a reference file's relative markdown link does not resolve to an existing file -> Fix the documentation it names, then re-run. For a type-spec's own frontmatter: fix the field, or add a matching line to types/type-spec.schema.yaml if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a <!-- dist:strip-start/end --> block. For a table of contents: run docs toc --apply - never hand-write the region. For a dead link: fix the ../ count or the target's name

NOTES

Check the docs that mirror the code: every command has a cli_contract record and is listed in cli_contract.GROUPS (both directions, so a command dropped from one is not hidden by the other), 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 cli_contract.render_commands_region() would write, no command's rendered --help/-h text cites an issue number, every directory under kb/ has a COLLECTION.md and no directory outside it does, every collection declaring profile: and a required_by_stack: that agrees with the stack's own list, every type the stack lists (currently source and project) having a type-spec of that name whose schema requires the field the stack list also names (raw_files:/state:), kb/CONVENTIONS.md naming all three tool-owned section headings if it exists at all, every stage contract present, every file under types/ declaring type: types/type-spec.md validating against types/type-spec.schema.yaml, no pre-migration type: entity blocks left in the contracts, the .gitignore canaries clear in both directions (nothing ignored under raw//kb/, incoming/ ignored, everything ignored under reports/ and the published skill directories), and no .md/.template file dist export would ship citing an issue number - the tracker exists only in the origin repo, so such a number in a distributed instance is a reference its reader can neither resolve nor recognise as unresolvable (a <!-- dist:strip-start/end --> region is exempt: it is already gone from the text the check reads, which is the export plan's, not the working tree's), every reference file docs toc covers carrying the current table-of-contents region for its own headings - missing and stale are one check, because the generator is idempotent - and every relative markdown link in one of those same reference files resolving to a file that actually exists (a target's #anchor suffix is stripped first; code fences and inline code spans are masked before scanning, so a passage showing link syntax as an example is not mistaken for a real reference). The name is about documentation parity, not about the docs/ directory - it neither reads nor requires one, the same way kb/ predates the collection it now checks

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

EXIT STATUS

  • 0 success
  • 1 Never fails on content: a file with no ## heading, or one at or under the threshold, is simply left without a region

ON FAILURE

  • Never fails on content: a file with no ## heading, or one at or under the threshold, is simply left without a region -> Nothing to fix - re-run with --apply to write what the dry run listed. If docs verify still reports a stale region afterwards, the file's ## headings changed in between; run it again

NOTES

On every reference file over 100 lines, in the scope Anthropic's skill-authoring guidance names for a file previewed rather than read in full: 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. Computed from those categories rather than listed, so a file added later is in scope without a code change. A template is in scope because it is the same document one step earlier in its life: an instance adopts it by copying it back, so a region missing there is a region missing in the adopted file, which is how kb/CONVENTIONS.md.template came to grow past the threshold with no region and left every instance adopting it failing docs verify at the end of its own setup. SKILL.md is the one exception, and the same guidance is why: it places a skill body on the loading level that is read whole when the skill triggers, and aims its own TOC advice at the bundled reference files a skill points at. Human docs (README.md, CHANGES.md, EVALS.md, INSTALL.md, tools/README.md) are out of scope because AGENTS.md § File naming says no agent loads them as instruction. Dry-run by default (prints which files would change); --apply writes. docs verify checks the result stays current the same way it checks every other generated-from-code copy

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

EXIT STATUS

  • 0 success

NOTES

Rebuilds the region from cli_contract.all_records(): the index (one line per command, GROUPS order) followed by each ###-group's commands as #### <path> man-page-shaped sections. Dry-run by default, like docs toc; --apply writes. docs verify's check_commands_region checks the result stays current the same way it checks every other generated-from-code copy.

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

EXIT STATUS

  • 0 success

NOTES

Most recent first. Read-only and exempt from the Iteration Budget Gate. Never fails; an empty list is a valid answer.

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

EXIT STATUS

  • 0 success
  • 1 No trace exists for the named session

ON FAILURE

  • No trace exists for the named session -> Run eval sessions to see which ids exist. 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. Safe to retry

NOTES

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. --save writes reports/evals/<date>/<session>.{json,md}. Read-only over kb/ and exempt from the budget; see EVALS.md

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/EVALS.md with any dist:strip-start...dist:strip-end marker region removed, instructions/ (minus instructions/dev/), types/ (the root: kb page type-specs and their schemas re-keyed as .template, the stack's own verbatim), docs/ verbatim, tools/ (no venv/caches), 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 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.
  • One-way: no command reconstructs a distributed instance into a dev instance - work on the stack in the origin repo, or in a new dev instance exported from it.
  • --dry-run lists every file it would write, and writes nothing.

SEE ALSO

  • instructions/setup-instance.md - what comes after the export
  • wikitool dist upgrade - applies a later export to an existing instance
  • wikitool version show - reads the stamp this writes

dist upgrade

Apply a stack update dist export produced - the write half of version check.

SYNOPSIS

  • wikitool dist upgrade <source> [--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: no

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

EXIT STATUS

  • 0 success
  • 0 The source's version equals the installed one - a no-op success
  • 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 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

  • Local VERSION missing, or no local .wikitool-release.json with a files block -> Not transient - fix the named precondition and retry. A checkout with shared git history takes stack updates with wikitool upstream merge instead
  • .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
  • 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

  • Never downloads anything: <source> is an already-fetched export directory or .tar.gz release archive. An 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.
  • 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, and re-creates it if it was locally deleted. 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 reported as no longer part of the release and left alone, unless --prune is passed, which removes it only if it is still unchanged since installation.
  • 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.
  • 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
  • instructions/upgrade-instance.md - the order after the swap
  • INSTALL.md § "Version und Updates" - which release, whether to take it, where the tarball comes from
  • wikitool upstream merge - the update path for a checkout with shared git history
  • 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.
  • One of the two commands in wikitool that make a network call, and the only one whose whole job it is - version notes is the other, and only on a distributed instance.
  • Never reached implicitly from another command, 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 (INSTALL.md § "Eine Instanz aktualisieren").
  • 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
  • INSTALL.md § "Version und Updates" - 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 --migration-required combined with --no-migration, on a bump with no running candidate, with no --no-migration line to retract, or without a 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
  • --migration-required combined with --no-migration, on a bump with no running candidate, with no --no-migration line to retract, or without a 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 the migration 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 is left to the author.
  • 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 retracts the running candidate's --no-migration line; it needs a 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 changesets

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 changesets -> Write a short summary paragraph right below the bump list, 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 ### <bump title> changeset heading. A candidate with exactly one bump is exempt, since there its own changeset already is the summary. --dry-run runs this check 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

EXIT STATUS

  • 0 success

NOTES

Oldest target first, with its kind and obligation. Read-only and exempt from the Iteration Budget Gate. Never fails.

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

EXIT STATUS

  • 0 success
  • 1 .wikitool-kb.json is missing (content version undeclared), or VERSION is unreadable

ON FAILURE

  • .wikitool-kb.json is missing (content version undeclared), or VERSION is unreadable -> For a missing declaration: run migrate baseline <version> once, then retry. Safe to retry freely otherwise

NOTES

Every required document whose migrates_to lies in (kb_version, VERSION]. offered documents are listed separately above the chain and never block, never count as owed, and are bounded by the applied ledger rather than by kb_version - taking one deliberately does not move the version, so the version cannot say whether it was taken. When a release stamp is present, also reports which shipped files this instance has since edited (from the per-file sha256 in .wikitool-release.json), which is what 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 .wikitool-kb.json is missing - the content's shape is a question the tool refuses to answer by guessing. Read-only and exempt from the budget gate

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

EXIT STATUS

  • 0 success
  • 1 Only with --fail-on-error: an invariant changed. Also exits 1 if --from is not a revision in this repository

ON FAILURE

  • Only with --fail-on-error: an invariant changed. Also exits 1 if --from is not a revision in this repository -> Exit 1 from --fail-on-error means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it

NOTES

Wikilink and citation counts (not sets), footnote definitions, H1, structural frontmatter, and the count of generated-region marker pairs - a page that went from one links region to two has the same set of region names and a different count, and a lost marker turns a generated region into prose the next write appends a second one beside. Pages are matched by title, not path, so a page wikitool move (or move --reconcile) relocated compares as itself - reported separately as moved - rather than as a removed-and-added pair. Reports added/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, and the one question lint cannot answer, since it reads a single revision and so cannot see that something went missing. Read-only and exempt from the budget gate

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

EXIT STATUS

  • 0 success
  • 1 Unknown version, no .wikitool-kb.json, nothing outstanding, or a required version that is not the next link in the chain

ON FAILURE

  • Unknown version, no .wikitool-kb.json, nothing outstanding, or a required version that is not the next link in the chain -> Not idempotent for a required migration: it advances the chain. For "not the next link", run migrate status and apply them in the order it prints - never force the order. Recording an offered migration is idempotent and safe to repeat

NOTES

Refuses any version that is not the next link in the chain - skipping one leaves the corpus in a shape no version describes, and an interrupted multi-step upgrade has to be resumable rather than guessable. An offered migration is recorded in the applied ledger without moving kb_version and with no ordering rule applied: it is not a link in the chain, so there is nothing to skip, and requiring the chain first would make an unrelated file upgrade wait on it. Re-recording one already in the ledger is a no-op, not an error

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

EXIT STATUS

  • 0 success
  • 1 Unparseable version, or a declaration already exists and --force was not passed

ON FAILURE

  • Unparseable version, or a declaration already exists and --force was not passed -> Safe to re-run with the same version. If a declaration exists, it is almost always migrate done that was wanted

NOTES

Refuses to overwrite an existing declaration without --force: advancing after a migration is done, which checks the chain, and this command must not become the quiet way around it

Private instances

upstream merge

Take a stack update into a private instance's branch, machinery only.

SYNOPSIS

  • wikitool upstream merge [--remote upstream] [--branch main] [--no-fetch]

PROPERTIES

  • effect: write
  • idempotent: no
  • atomic: No - can leave an open, uncommitted merge behind on refusal after fetching
  • budget: counted
  • network: no

EXIT STATUS

  • 0 success
  • 1 Dirty working tree, a merge already in progress, the remote does not resolve, git refused to open the merge at all (unrelated histories), or a real conflict remains in tools//types//instructions/ after the content stages and stack-owned paths were restored

ON FAILURE

  • Dirty working tree, a merge already in progress, the remote does not resolve, git refused to open the merge at all (unrelated histories), or a real conflict remains in tools//types//instructions/ after the content stages and stack-owned paths were restored -> Not idempotent, and not safe to retry unchanged. For a dirty tree or an in-progress merge: fix the named precondition and retry once. For a real conflict: do not retry, do not force - resolve the named paths by hand (take the upstream side, or re-file the local change as an issue against the public repo per instructions/private-instance.md) and either git commit --no-edit yourself or git merge --abort. If the postcheck after commit finds a leak, the merge commit already exists and is not rolled back automatically - inspect it by hand; this is a bug report, not a retry

NOTES

The code procedure behind instructions/private-instance.md § "Taking a stack update". Refuses on a dirty working tree, a merge already in progress, or a remote that does not resolve; WARNs (does not block) when .wikitool-remotes.json is absent, pointing at the setup step that arms it. Fetches <remote>/<branch> (unless --no-fetch) and reports "already up to date" if nothing new exists. Otherwise opens git merge --no-commit --no-ff <remote>/<branch> - and stops, untouched, if git refused to open a merge at all (unrelated histories), since without a MERGE_HEAD every stack path would read as "the upstream deleted it". Then forces every content stage (kb/, raw/, work/, reports/) back to the local side by removing only the paths tracked in either tree and checking HEAD's back out - never the stage directory wholesale, because reports/ is gitignored apart from its contract and holds local, non-recomputable data (telemetry traces eval score reads, saved eval and lint reports) that no merge has business deleting. Then restores from the upstream side exactly the paths chemenu.ownership.is_stack_owned recognises as machinery (<stage>/CONTRACT.md, and anything ending .template under a content stage) - including a deletion, if the upstream removed one. A real conflict left in tools/, types/ or instructions/ after that leaves the merge open, uncommitted, and exits 1 rather than guessing. Commits with git commit --no-edit, then re-checks the resulting range with the same logic as upstream verify; a finding there is a loud, uncommitted-nothing-rolled-back error, because the merge commit already exists and needs a human's eyes, not an automatic repair. Never pushes. Not idempotent - see the tool error contract below

upstream verify

Compare two revisions: did anything under a content stage change except through a stack-owned path?

SYNOPSIS

  • wikitool upstream verify --since <rev> [--until HEAD]

PROPERTIES

  • effect: read
  • idempotent: yes
  • atomic: Read-only
  • budget: exempt
  • network: no

EXIT STATUS

  • 0 success
  • 1 A leak was found (content changed under a content stage through a path that is not stack-owned), or --since/--until is not a revision in this repository

ON FAILURE

  • A leak was found (content changed under a content stage through a path that is not stack-owned), or --since/--until is not a revision in this repository -> A finding is not fixed by re-running - it names the paths that leaked. Fix the revision argument and retry for the second case

NOTES

Shares its check with upstream merge's own postcheck, so a hand-resolved merge conflict, or a dist upgrade, can be verified the same way. Exit 1 with the offending paths if anything leaked; otherwise reports which stack-owned paths legitimately moved. Read-only and exempt from the Iteration Budget Gate, like migrate verify

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

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

Dependencies (Python, ripgrep), author resolution, stack version, git identity/branch/remote, published skills, kb/raw/reports/work/instructions structure, personalization (USER.md/SOUL.md present and filled - a file still carrying the template's sentinel is a FAIL, since a renamed template is not a filled one), the KB conventions (kb/CONVENTIONS.md present, unsentinelled, and naming all three tool-owned section headings - a FAIL on any of the three, because xref/cite write out of it), the environment note (ENVIRONMENT.md - optional, so absent is OK; a still-templated one is a WARN), generated files, whether the MCP submit tool is armed (.wikitool-upload.json present/absent/malformed, its limits, and how many submissions are waiting in mcp-upload/ - absent is OK and means the write path does not exist at all, malformed is the one FAIL here, since a broken opt-in must not silently disable the limits it exists to enforce), the task-tracker provider (.wikitool-tasks.json present/absent/malformed - absent is OK and means no tracker is configured, malformed is FAIL for the same reason the upload opt-in is; for a configured superproductivity provider, also its configured access path's own state - access: "api" reports whether its local REST API answers GET /health right now, access: "snapshot" reports whether a backup file is ready; the other access path is never attempted and is not a finding - and neither ever FAILs, an app that is simply not running is not a fault; for a configured caldav provider, whether the server is reachable and Basic auth succeeds - also never a FAIL, only a broken config block is), the session id source (OK for WIKITOOL_SESSION_ID or a registered harness variable, WARN only for the bare parent-pid fallback - see chemenu.session), and telemetry state (on/off, why - installation-form default, .wikitool-telemetry.json, or WIKI_TRACE - and the current session count/byte total against both caps; never FAIL, see EVALS.md). Read-only, exit 1 only on a FAIL (a missing remote, session id, or VERSION is a WARN, not a fault). Exempt from the Iteration Budget Gate

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 .wikitool-upload.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; reviewing a submission is 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

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 - see README.md and EVALS.md.

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.