Files changed: - AGENTS.md - CHANGES.md - VERSION - instructions/dev/doc-pull-through.md - instructions/dev/stack-close/SKILL.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/cli.py - tools/chemenu/cli_contract.py - tools/chemenu/commands/cite_cmd.py - tools/chemenu/commands/dist_cmd.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/eval_cmd.py - tools/chemenu/commands/git_publish.py - tools/chemenu/commands/index_build.py - tools/chemenu/commands/instructions_cmd.py - tools/chemenu/commands/links_cmd.py - tools/chemenu/commands/lint.py - tools/chemenu/commands/log_append.py - tools/chemenu/commands/migrate_cmd.py - tools/chemenu/commands/new_page.py - tools/chemenu/commands/page_ops.py - tools/chemenu/commands/provenance_cmd.py - tools/chemenu/commands/raw_cmd.py - tools/chemenu/commands/review_cmd.py - tools/chemenu/commands/run_budget.py - tools/chemenu/commands/search.py - tools/chemenu/commands/task_cmd.py - tools/chemenu/commands/touch.py - tools/chemenu/commands/types_cmd.py - tools/chemenu/commands/upload_cmd.py - tools/chemenu/commands/upstream_cmd.py - tools/chemenu/commands/version_cmd.py - tools/chemenu/commands/work_cmd.py - tools/chemenu/commands/xref.py - tools/chemenu/tests/test_cli_contract.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_run_budget.py
114 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)
- Usage
- Commands
- Design notes
- Tests
- Maintenance schedule
- Future considerations (not implemented)
Setup (one time)
Full bootstrap for a fresh clone - including publishing the skills, which are not
committed - is instructions/bootstrap.md. 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 ...]- Scaffold a page of any type. The type-spec drives fields, directory (base_dir/layout), title prefix, and template - a schemadefault:is materialized only for a field the schema also lists inrequired:(an optional field's default is a reader-side assumption, not a scaffold-time value) ---setis repeatable, and comma-separated values fill array fields. An element that itself contains a comma is written\,, or passed as its own repeated--setfor that field - repeating an array field appends. Seetypes list/types describe.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]- Scaffoldkb/entities/<subdir>/<Name>.mdwikitool new concept --name "<Name>" --set concept_type=<t> ...- Scaffoldkb/concepts/<Name>.mdwikitool 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]- Scaffoldkb/sources/Source - <Name>.md(prefix added automatically) with araw_files:list (rejects paths that don't exist)wikitool new comparison --name "X vs Y" --set entities=X,Y- Scaffoldkb/comparisons/X vs Y.mdwikitool new project --name "<Name>" --set responsibility=<bereich> [--resume]- Scaffoldkb/gtd/<bereich>/<Name>.mdand, if.wikitool-tasks.jsonconfigures a task tracker, a same-named tracker project - one name, one identity. Tracker before page: the tracker side is settled first, so a failure past that point leaves a tracker project with no page - a statereview's check 3 already reports - never a page with no tracker project. No tracker configured is a legitimate, explicitly announced state (page only). A name already taken (case-insensitively) inkb/or the tracker is refused outright, naming where it was found, and creates nothing. A provider whose configured access path has no write path (Super Productivity'saccess: "snapshot"- the tracker is read-only from there by construction) refuses entirely, exit 1, naming theaccess: "api"instance to use instead - neither the tracker project nor the page is created, and--resumebehaves the same. A provider that could write but has no project-creation endpoint of its own (Super Productivity'saccess: "api"-GET /projectsexists,POST /projectsdoes not) raiseschemenu.errors.HumanInterventionRequired; the command shows its instructions and exits 42 (needs_clearance(), same posture as the four named gates, without being a fifth one - see that class's docstring), creating nothing.--resumeis how a later run tells the command a human has done what that message asked: it re-verifies via the read path (find_project) before continuing to page creation, rather than trusting the claim, and repeats the same 42 if the tracker still doesn't have it.--resumeon any other type is refused
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 statereview'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 projectonly)
EXIT STATUS
- 0 success
- 1 new : Duplicate page title, unknown type, invalid
--setvalue, or araw_filespath that doesn't exist - 1 new project: Everything
new <type>covers, plus: the name is already taken in the tracker (case-insensitively - forcaldavthis is checked against every list in the account, not only the ones counted as projects),--resumewas passed for a type other thanproject, or the configured provider's access path has no write path at all (Super Productivity'saccess: "snapshot") - 42 needs clearance - human-intervention-required (
new projectonly) (see AGENTS.md § Gates)
ON FAILURE
- new : Not transient; fix the argument and retry once. Never hand-craft the page instead
- new project: A collision, a bad
--set, or a read-only access path is not transient, same asnew <type>- the last of those points at theaccess: "api"instance instead and refuses on every--resumeretry too, since nothing about the config changes by asking again. Exit 42 (NEEDS USER CLEARANCE, not exit 1) is its own separate outcome from the ordinary exit-1 cases above, and issuperproductivity-only: that provider can write but cannot create the project itself and a human must, per the printed instructions; re-run with--resumeonce that is done - it re-verifies via the read path rather than trusting the claim, and exits 42 again unchanged if the tracker still does not have it.caldavnever produces this outcome -MKCALENDARis a real collection-creation verb, so a valid, non-colliding name always creates the list itself
NOTES
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.
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
EXIT STATUS
- 0 success
- 1 No
.wikitool-tasks.json, neither or both of--project/--inboxgiven, a--follow-up-atwithout--waitingor notYYYY-MM-DD, a--projectname matching no tracker project,--waitingagainst a provider with no way to represent it right now (Super Productivity: thewaitingtag does not exist), or a read-only access path (Super Productivity'saccess: "snapshot")
ON FAILURE
- Not transient; fix the argument, create the missing tracker project or tag first, or point at an
access: "api"instance, then retry once. Never exit 42 - unlikenew project, every provider offering a write path at all has a real item-creation call, so there is no human-clearance step to wait on here
NOTES
The second creation command alongside new project, and the last one their split needed - see docs/knowledge-and-commitment.md. Exactly one of --project (an existing tracker project, matched case-insensitively - never created and never searched or guessed) or --inbox (the tracker's own inbox, a deliberate exit with a cost: an item filed there never appears in review, since every one of its checks is reached through a project name and the inbox has none) is required; an omitted --project refuses rather than silently falling into the inbox. --waiting sets the WAITING status the review's own waiting-overdue check reads; --follow-up-at is refused without --waiting, since it is never a due date on its own. --notes carries a freetext backref (e.g. to the kb/ source page this item came from), stored verbatim, never parsed - the same posture a WAITING item's own title already has for the person named in it. No .wikitool-tasks.json fails immediately with the same "no tracker configured" message as review. A provider whose configured access path has no write path (Super Productivity's access: "snapshot") refuses entirely, exit 1, naming the access: "api" instance to use instead - same posture as new project. Unlike new project, never exits 42: every provider offering a write path at all has a real item-creation call (Super Productivity's POST /tasks, where POST /projects does not exist) - a named --project that does not match any tracker project, or --waiting against a provider that cannot represent it right now (Super Productivity: the waiting tag does not exist yet, and tags cannot be created via its API), are ordinary exit-1 refusals instead, creating nothing
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
EXIT STATUS
- 0 success
- 1 No
.wikitool-tasks.json
ON FAILURE
- Not transient; configure a tracker first, then retry once. A
--projectmatching no tracker project is not an error here - see its Commands row
NOTES
Read-only; the id source task close and the review's own waiting_overdue/someday_stale findings need, without first running wikitool review. Works on either access mode a provider offers, unlike the write commands below. No .wikitool-tasks.json fails with the same "no tracker configured" message as review/task new; a --project matching no tracker project prints "No open items", since TaskReader.open_items does not distinguish "empty" from "unknown" (chemenu.tasks.protocol.TaskReader.open_items's own docstring)
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
EXIT STATUS
- 0 success
- 1 No
.wikitool-tasks.json, an--idmatching no tracker item right now, or a read-only access path (Super Productivity'saccess: "snapshot")
ON FAILURE
- Not transient; fix the id (re-run
task listorreviewto get a current one) or point at anaccess: "api"instance, then retry once. Never exit 42, same reasoning astask new
NOTES
<item-id> is the provider's own id, from task list or a review finding, never a title - the tracker-side identity is opaque, unlike the project name that is kb/'s and the tracker's only shared coupling. The only closing write this stack makes: no "move a reminder", no "remove an item". No .wikitool-tasks.json fails with the same "no tracker configured" message as task new. A provider whose configured access path has no write path (Super Productivity's access: "snapshot") refuses entirely, exit 1, naming the access: "api" instance to use instead - same posture as task new. Never exits 42, same reasoning as task new: every provider offering a write path has a real per-item write call
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
EXIT STATUS
- 0 success
- 1 Page not found; an invalid value for a field it writes; a field owned by another command (
type:, a page-ref array) or absent from the type's schema;--add/--removeon a non-array field; araw_files:path that doesn't exist
ON FAILURE
- Fix the argument and retry once. Safe to re-run as-is:
--setand--addare idempotent, and--removeof an already-absent element succeeds while reporting it
NOTES
Update a page's own frontmatter: bump modified: and optionally rewrite any field its 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 - same rules as new --set. Refused with the command that owns them instead: type: (page-lifecycle), and the page-ref arrays related:/sources:/entities:/concepts: (xref). Everything else the schema declares is settable, and an unknown field lists what the page actually has. Schema-validates the fields it writes, and raw_files: entries must exist on disk. A source declares date: instead of modified:, and that is the publication date of the raw material - it is never bumped to today, and changes only when --date names a value explicitly.
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
EXIT STATUS
- 0 success
- 1 Neither
--fromnor--tois a page, target title already taken, or--fromequals--to
ON FAILURE
- Safe to retry once as-is; each page's rewrite is idempotent. Use
--dry-runfirst to see the blast radius. Never fix up references by hand instead
NOTES
Rename a page and repoint 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
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
EXIT STATUS
- 0 success
- 1 Page not found, or other pages still reference it and
--yeswas not passed
ON FAILURE
- For "still referenced": show the user the inbound list, get approval, then re-run with
--yes. Prose references it reports afterwards are an editorial fix, not a retry
NOTES
Refuses without --yes while other pages still reference it. Strips ref-array entries and bare - [[Title]] / - **label:** [[Title]] bullets; leaves prose and inline citations in place and reports 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
EXIT STATUS
- 0 success
- 1 Neither or both of
--page/--reconcilegiven, the named page not found, it has notype:to compute a placement from, or the destination already exists
ON FAILURE
- Safe to retry once as-is; a page already at its computed location is reported and left alone, and
--reconcileonly re-moves what is still misplaced. Use--dry-runfirst to see the blast radius. Never choose a directory by hand instead
NOTES
Move a page to the directory its type-spec computes for its current frontmatter (base_dir + layout - the same rule new places a page by, via TypeResolver.compute_target_dir), never 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 (lint's Misplaced Pages finding is the advisory that this fixes, and its Nested Pages finding the hard one - see lint). 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 along with it, so a page that was nested below its area leaves no leftover directory behind. A destination already occupied (a pre-existing duplicate-stem collision) is refused rather than silently skipped
Links and citations
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
EXIT STATUS
- 0 success
- 1 Page A or B not found, or a page's type declares no
related:field
ON FAILURE
- Safe to retry once as-is; re-running never duplicates a link. Never create the missing page just to force the link through, and never hand-write a reference field the type does not declare
NOTES
Declare 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, and re-running with a different label relabels rather than appending, since one page asserts one thing about another. Refuses before writing when the type does not declare related: (a source page declares entities:/concepts: - the refusal names them and points at link-source), and when <label> is not authorised by the source collection's outbound: block for the target's collection; that refusal lists the authorised set and points at instructions/link-taxonomy.md
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
EXIT STATUS
- 0 success
- 1 Page A not found (B is allowed not to exist)
ON FAILURE
- Safe to retry freely; removing an absent link is a no-op
NOTES
Clears the reference in both directions - it is the cleanup command for a deleted or hand-renamed page rather than the strict inverse of a one-directional add. Clears <B> from every page-ref frontmatter field <A>'s type declares (related:, sources:, entities:, concepts:) plus the matching bullets. It also sweeps a field the type does not declare but some other type does, and drops that key outright once empty - a leftover written before the check above existed has to stay repairable, or the page is a dead end. --b need not still exist as a page, so this is how a reference left by a hand-deleted or hand-renamed page gets cleared without hand-editing frontmatter. Idempotent.
xref link-source
Batch-link a source page to every entity/concept it mentions.
SYNOPSIS
wikitool xref link-source --source "Source - X" --entities A,B,C [--dry-run]
PROPERTIES
- effect: write
- idempotent: yes
- atomic: No - one write per entity plus one for the source page, idempotent per page
- budget: counted
- network: no
EXIT STATUS
- 0 success
- 1 Source page not found, an entity in
--entitiesdoesn't exist, or the source page itself could not be written after its targets were
ON FAILURE
- Use
--dry-runfirst; safe to retry.sources trace --page "<Title>"shows who was already linked
NOTES
Each target gets sources:, and the source page records each target in its own entities:/concepts:. No body bullet is written on either side - sources: is the record, and the See Also bullet this used to add was the reciprocal half of a model that no longer exists. Which of the two is chosen follows the target's collection (kb/entities/ -> entities:), so a new collection needs no code change here. A target whose collection matches no reference field the source type declares is linked one-way and named in the output. Idempotent in both directions
links show
Show the edges out of and into a page.
SYNOPSIS
wikitool links show --page "<Title>" [--json]
PROPERTIES
- effect: read
- idempotent: yes
- atomic: Read-only
- budget: exempt
- network: no
EXIT STATUS
- 0 success
- 1 Page not found
ON FAILURE
- Check the exact title with
search; a wikilink target is not always the page's stem
NOTES
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 (computed across the corpus). The inbound half is derived rather than stored - that is what makes it complete, and it is the answer authored directional edges would otherwise have nowhere to come from. Read-only, exempt from the Iteration Budget Gate
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
EXIT STATUS
- 0 success
NOTES
Read-only preview - does not check the id is actually free on any given page. Never fails. Safe to retry freely. Exempt from the Iteration Budget Gate
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
EXIT STATUS
- 0 success
- 1 Page or source not found
ON FAILURE
- Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
NOTES
Upsert a [^cite-id]: [[Source - X]] definition in the page's generated footnotes region, creating it between <!-- wikitool:footnotes --> markers if absent (reusing the id if the page already cites this exact source/file pair) and add Source - X to frontmatter sources:. Prints the [^cite-id] marker - pasting it into the prose is still a manual, editorial step
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
EXIT STATUS
- 0 success
- 1 Neither or both of
--page/--allgiven, or page not found
ON FAILURE
- Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run
cite add) and re-run
NOTES
Prune definitions nothing references any more, re-render the region in first-reference order, and report any [^id] reference left with no definition. A page still carrying the pre-4.0.0 undelimited block is converted to a marked region in the same pass - the marker carries the region's identity now, so re-rendering it under this instance's heading is a repair rather than a rename
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
EXIT STATUS
- 0 success
- 1 Rare I/O error only
ON FAILURE
- Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges
NOTES
kb/index.md becomes a map (statistics, one row per collection and per area, links to the shards) and the page tables are written to a generated INDEX.md in each collection. An area past 50 rows gets its own shard. Stale shards from removed collections/areas are deleted in the same pass
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
EXIT STATUS
- 0 success
- 1 Invalid
--opor unreadable--body-file
ON FAILURE
- Not idempotent. If the previous run's outcome is uncertain, check the tail of
kb/log.mdbefore retrying
NOTES
Append a formatted entry to kb/log.md.
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
EXIT STATUS
- 0 success
NOTES
The deterministic trigger behind the Maintenance Schedule's "every 10 sources" full-lint cadence. Never fails (reports 0 if kb/log.md is missing or empty). Safe to retry freely.
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
EXIT STATUS
- 0 success
- 1 Only with
--fail-on-error: hard findings exist
ON FAILURE
- Safe to retry freely, but re-run it to re-measure, never to re-read: the printed path holds the full report. Exit 1 means "act on the findings", not "the tool is broken"
NOTES
Structural + provenance checks: broken wikilinks, dangling frontmatter references, orphan pages, index drift, schema gaps, duplicate titles, title mismatches, pages nested more than one directory below their collection (hard - the generated catalog folds these into their area silently rather than merely reading it), uncovered raw files, broken raw_files: refs, raw files claimed by more than one source page, unmarked provenance, citation/frontmatter drift, unbalanced generated-region markers, edges whose label is missing or not authorised by the source collection's outbound: (both hard once kb_version has reached the release that introduced labelled edges - advisory below it, so a corpus mid-migration is not refused by the check measuring it), see-also edges whose reverse direction already carries a specific label (advisory only - redundant rather than wrong, and never migration-gated, since no version turns the redundancy into an error), a collection past the catalog's per-area shard threshold that has no areas to shard (advisory only - sharding is automatic but per area, so a collection nobody gave areas keeps one table however large it grows; reported with the split its subtype field would produce, and only when that split puts every resulting area at or under the threshold, so a lopsided or small collection stays silent), source pages sitting in the unclassified catalog slot (advisory only - unclassified is the visible fallback for a genuinely unclear source, not a defect), quote-limit overages (>2 blockquoted lines/page, advisory only). 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
search
Find pages in kb/ by text and/or frontmatter.
SYNOPSIS
wikitool search ["<text>"] [--field <predicate> ...] [--kind/--subtype/--collection/--tag <v>] [--regex] [--limit N] [--sort [-]<field>] [--backend <name>] [--matches] [--json]
PROPERTIES
- effect: read
- idempotent: yes
- atomic: Read-only
- budget: exempt
- network: no
EXIT STATUS
- 0 success
- 1
rgis not installed or did not finish within 30 s, a malformed--fieldpredicate, an unknown field name, or an unknown--backend
ON FAILURE
- Fix the argument and retry. A timeout is a pathological pattern or an unresponsive corpus directory, not a slow answer - narrow the query or drop
--regexrather than retrying it unchanged. An unknown field name is reported with the list of fields that do exist - it is never answered with an empty result, because that would read as "no such pages"
NOTES
Find pages in kb/ without reading the index. 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, so a hit can be judged without opening the page and then opened without looking it up: title and path are never truncated (the title is the identifier touch/xref/cite take), and 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 anything kb_scan.iter_kb_pages excludes (the kb-root meta files, every COLLECTION.md, every generated INDEX.md), which is why 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; the same default and the same fields are what api.search and the MCP search tool carry, from one constant. 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. --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
review
The GTD weekly review.
SYNOPSIS
wikitool review [--json]
PROPERTIES
- effect: read
- idempotent: yes
- atomic: Read-only
- budget: exempt
- network: yes
EXIT STATUS
- 0 success
- 1 Either no
.wikitool-tasks.json(or a malformed one) - not yours to fix by retrying unchanged, configure or repair it first - or the provider was reachable at config-parse time but a read call failed mid-run, in which case the full report (findings plus which checks ran) is printed first and exit 1 follows, never a silent partial success
ON FAILURE
- The two exit-1 causes above need different responses: a config problem needs editing
.wikitool-tasks.json; an unreachable provider (e.g. the tracker app not running) needs starting it, then a plain retry - the command re-reads everything fresh each time, so nothing here is ever stale to re-fetch
NOTES
Joins the configured task-tracker provider (chemenu.tasks) against kb/gtd/ project pages over the case-normalized project name, at read time, storing nothing - not even a reports/ file. Five checks: stalled (a tracker project with zero open items whose kb/ page is state: active - dormant/completed/abandoned never fire, since those states mean the initiative not having a next action is expected rather than a problem), 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 the unpaged-project join, 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 at all, a tracker project with no determinable creation date - is its own finding (waiting_no_follow_up/project_age_unknown) rather than a silent skip of waiting-overdue/unpaged-project for that item or project. 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). No .wikitool-tasks.json fails immediately with a clear "no tracker configured" message; a provider that cannot be reached mid-run degrades only the checks that needed the failing call, and the report is never rendered as if it were complete - see its error-contract row. Read-only, and exempt from the Iteration Budget Gate
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
EXIT STATUS
- 0 success
NOTES
List raw files with no source page, broken raw_files: references, and legacy directory/URL-only source pages. Never fails. Safe to retry freely.
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
EXIT STATUS
- 0 success
- 1 Neither or both of
--raw/--pagegiven,--rawnames a file no source page covers (reported as a plain finding plus exit 1, not the usualERROR-prefixed rejection), or--pagenames an unknown page
ON FAILURE
- Fix the argument and retry
NOTES
Trace provenance in either direction: raw file -> source page(s) -> citing pages, or page -> its sources -> their raw files
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
EXIT STATUS
- 0 success
- 1 Rare I/O error only
ON FAILURE
- Safe to retry freely
NOTES
Regenerate the kb/provenance.md reverse index (raw file -> source page -> citing pages)
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 fromincoming/intoraw/<YYYY>/<MM>/, computed from the accept date rather than chosen by hand (raw/CONTRACT.md"Getting a file in"): a subdirectory underincoming/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 underraw/<YYYY>/<MM>/<stem>/, named after the first file's stem.--fidelity/--authorityare required here (seetypes describe source;unknownis refused, backfill-only) - the one moment both are knowable.--page "<Title>"additionally extends that existing source page'sraw_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 underraw/- 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--replacesand renaming-in-incoming/without recommending eitherwikitool 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'sraw_files:untouched and writing nokb/page - the previous edition survives only ingit log --follow <raw-path>.--fidelity/--authorityare 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--pageor 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 - oneunlink()+ onerename(), plus (if--fidelity/--authoritywas 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/--authorityis missing (unless--replaces) or namesunknownor a value outside the schema's enum, the target name is already occupied anywhere underraw/by something the call does not own,--pagenames an unknown page or one with noraw_files:yet, an existingraw_files:entry is missing on disk, a file to be moved has more than one owning page, or--pagewould overwrite an already-setfidelity/authoritywith a different value - 1 raw accept --replaces: More than one incoming file,
--pagealso given, the incoming file does not exist or is not underincoming/(or is nested more than one level below it), its filename differs from the target's, the target does not lie underraw/or does not exist,--fidelity/--authoritynamesunknownor a value outside the schema's enum, or the target has more than one owning source page
ON FAILURE
- raw accept: 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
--replacesand renaming inincoming/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: 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
- 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>/, orincoming/<filename>already exists. Exit 42, not 1, when--confirmis 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
- 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 occupiedincoming/<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
- 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
EXIT STATUS
- 0 success
- 1 The automatic rebase hit a real conflict (git failed)
- 42 needs clearance - rebase-review (see AGENTS.md § Gates)
ON FAILURE
- For a conflict: do not retry, do not force - resolve manually and re-run. Exit 42, not 1, when the rebase-review gate needs clearance: show the user the command's full output verbatim (upstream commits, the overlapping files, their diff) and stop; re-running with
--confirm-rebase <token>clears it, and a wrong, invented, or superseded token exits 42 again with the current state. No remote configured, or one that cannot be reached, is not a failure - reported and skipped
NOTES
Fetch <remote>/<branch> and bring the local branch up to date with it: fast-forward when the remote is simply ahead, rebase local commit(s) on top when both sides moved but touch disjoint files (a content conflict is then impossible by construction), and exit 42 for review when they touch the same file (the rebase-review gate - see publish below). Never commits, never pushes, never force-anything - no remote configured, or one that cannot be reached, is reported and skipped, not a failure. Meant to run once at the start of a writing session (instructions/session-setup.md) so the rest of it works against a current tree instead of discovering the drift at the final publish
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 both gates run before staging
- budget: counted
- network: no
- gates: mass-update, publish-remote, rebase-review
EXIT STATUS
- 0 success
- 1 git failed, the push target is not the checked-out branch (including a real detached HEAD - but not the unborn branch of a fresh
git init, which is a normal first publish), or--yes/-ywas passed. Exit 42, not 1, when the Mass-Update Gate, the rebase-review gate (raised by the same reconcilesyncperforms), or the Publish-Remote Gate refuses - 42 needs clearance - mass-update, publish-remote, rebase-review (see AGENTS.md § Gates)
ON FAILURE
- For git failures: do not retry, do not force - report and ask the user (the reconcile step already retried the push once on its own, if a rebase resolved the rejection). For exit 42: show the user the command's full output verbatim and stop; it names the evidence and the
--confirm <token>or--confirm-rebase <token>line to re-run, and re-running without it exits 42 again. The Publish-Remote Gate is the exception with no such line: it names the push URL that would have been written to and the ones this checkout allows, and only the user resolves it
NOTES
Reconcile with <remote>/<branch> exactly like sync (skipped for --no-push), then stage all changes, commit, and push. Refuses before staging anything when the push target is not the checked-out branch, so a git push <branch> cannot quietly publish a ref other than the commit just made; the unborn branch of a fresh git init -b main counts as checked out, which is what lets the first publish of a new instance work (instructions/setup-instance.md step 14), while a genuine detached HEAD is still refused. If the reconcile step found a still-unpushed local commit and there is nothing new to stage, that commit is pushed anyway - a previous publish whose push failed no longer strands it, and neither does a branch the remote has never seen (a newly created, empty remote repository). A remote that cannot be reached at all is deliberately not read that way: it keeps reporting "Nothing to commit" on a clean tree rather than attempting a push, so an offline or local-only instance is unaffected. If the push is rejected despite the pre-check (a genuine race - something landed on the remote in between), one more reconcile-and-retry is attempted before giving up; never more than one. Mass-Update Gate: when >= --threshold (default 10) counted files would be committed, exits 42 (EXIT_NEEDS_CLEARANCE) instead of publishing - a third outcome distinct from success (0) and a validation error (1) - and prints a review report: a scale line (file count, total lines added/removed, status breakdown), only-what-applies attention notes (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, generated files split out as needing no review. The token digests each counted path and its contents plus the publish target, so a clearance carries neither to a different file list nor to edited contents; a wrong, invented or superseded token exits 42 again with the current state. Two kinds of path are committed but never counted and never shown for approval: anything under work/, and the files wikitool generates itself (kb/index.md, kb/log.md, kb/provenance.md, every INDEX.md) - each is recomputable from the tree, so approving it decides nothing, and a routine ingest rebuilds five or six of them. The refusal line accounts for both, by reason. The gate is evaluated before anything is staged, so a refused publish leaves the working tree untouched. Publish-Remote Gate: when this checkout carries a .wikitool-remotes.json and the resolved push URL of --remote is not listed in it, exits 42 before the reconcile step even fetches - the URL is read from git remote get-url --push, so a repointed remote does not pass on its name. Unlike the other two gates it has no token and no flag: the way past it is the user adding the URL to that file, and an agent editing it to get past a refusal is opening a gate on its own initiative. Absent file means unrestricted; a malformed one is an error, not permission. See instructions/gates.md. --yes/-y are gone and now fail with an explicit error. --path (repeatable) scopes the whole operation - gate count, staging, and commit - to a subtree. Stack-machinery note: after a successful commit/push whose changed files include tools/, types/, instructions/, AGENTS.md, or a path ending CONTRACT.md - roughly the scope a stack version bump covers, deliberately a shade broader than CI's version gate, which matches only a CONTRACT.md one segment deep - prints one reminder line that 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. Not a gate: no exit code change, nothing to clear, silent for an ordinary content publish
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/--keygiven,--inputoutsideraw/, a--keythat is empty or starts withingest-, or the workshop already exists
ON FAILURE
- A collision is not transient: resume the existing run instead, or pass
--againif 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
--yeswas not passed
ON FAILURE
- 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
--yesnot passed
ON FAILURE
- 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
- 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 (noSKILL.md) and--forcewas not passed
ON FAILURE
- Check whether the flagged target holds anything worth keeping, then re-run with
--forceif 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 orSKILL.md, aSKILL.mdcarrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, formanual: true, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something underinstructions/dev/referenced from outside it and outside adist:stripblock
ON FAILURE
- 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-runsyncinstead of hand-editing the published copy - the source underinstructions/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/.templatecites 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
- 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.yamlif 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: rundocs 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:
--applyrewrites 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
- Nothing to fix - re-run with
--applyto write what the dry run listed. Ifdocs verifystill 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/--markdownwrite - budget: exempt
- network: no
EXIT STATUS
- 0 success
- 1 No trace exists for the named session
ON FAILURE
- Run
eval sessionsto see which ids exist. A session records nothing when telemetry is off -WIKI_TRACE=0, or a distributed instance with no opt-in (wikitool doctorsays 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
EXIT STATUS
- 0 success
- 1 Target exists and is not empty, is not a directory, or the tree has no readable
VERSION
ON FAILURE
- Point
<target>at an empty (or new) directory and retry. Never merge into a non-empty one by hand
NOTES
Write a contentless, distributable copy of this repo's machinery into an empty <target> directory: AGENTS.md/README.md/EVALS.md with any <!-- dist:strip-start -->...<!-- dist:strip-end --> 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), the two flat anchors raw/.gitkeep and incoming/.gitkeep (both roots are flat now that a file's location under raw/ is a date shard rather than a hand-picked type, so a fresh export no longer creates any type subdirectories under either root; incoming/.gitkeep is trackable and survives becoming a git repository, so a plain clone gets the directory without any bootstrap step re-creating it), VERSION, USER.md.template/SOUL.md.template plus kb/CONVENTIONS.md.template and each collection's contract re-keyed as kb/<name>/COLLECTION.md.template (the templates ship; the filled USER.md/SOUL.md/kb/CONVENTIONS.md/kb/<name>/COLLECTION.md/types/<page-type>.md never do - all of them bind their instance and none are the stack's to decide, and find_leaks refuses a plan carrying one), and a generated .wikitool-release.json stamp (version, export date, origin, and a sha256 per exported file - the base a later upgrade would compare against). The four origin options only fill stamp fields: export never calls git and cannot discover them. Refuses a non-empty target, and a tree with no VERSION. See instructions/setup-instance.md. One-way: there is no command that reconstructs a distributed instance into a dev instance - work on the stack in the origin repo (or a new dev instance exported from it) instead
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 the refusal cases above - 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
EXIT STATUS
- 0 success
- 1 Missing local
VERSION/.wikitool-release.json(files)/.wikitool-kb.json, a migration already outstanding against the installed machinery, a dirty working tree, a source with noVERSION/stamp/filesblock, a source version that is older than, equal to, or (without--pre) a pre-release relative to the installed one, a--take-releasepath that is not classified as locally changed (the one refusal a--dry-runalso raises), or one or more locally changed files that neither--keep-localnor a--take-releaseanswers for
ON FAILURE
- For every refusal above: fix the named precondition and retry - none of them are transient. For a rejected
--take-releasepath: correct it against the locally-changed list the refusal prints. For locally changed files, the refusal names all three answers with the re-run line filled in ---take-release <path>to write the release's version over it (which ends the divergence),--keep-localto leave them untouched (repeatable, and it reports the same files again on every subsequent run until they stop diverging), or reconcile by hand and retry. An interrupted write is not resumed automatically; compare the tree against the printed classification and finish or revert by hand
NOTES
Never downloads anything: <source> is an already-fetched export directory or .tar.gz release archive (verified against a sibling .sha256 if one is present; WARNs, does not block, if it is absent), which 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 - chemenu.ownership.is_export_stub) or seeds once and the instance owns from then on (.wikitool-kb.json, CHANGES.md - chemenu.ownership.is_upgrade_preserved), plus the stamp itself, always rewritten. Every candidate path is classified against the local .wikitool-release.json's recorded digest for it: unchanged is overwritten silently, absent from the old stamp is created, and locally modified or locally deleted is never silently overwritten - the run aborts with the full list, and its text names the three answers with the command line already filled in, so that no reader takes any of them for the default. --keep-local proceeds and leaves every one of them untouched; --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 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 --take-release path that this run does not report as locally changed is refused, in a --dry-run as well as a writing run - it is a mistake in the argument rather than a state of the tree, and a path that silently did nothing would report a successful upgrade while keeping the change it was asked to discard. After a --keep-local run the new stamp is still written whole, so it records the release's digest for files that were deliberately not written: the stamp is the baseline for the next comparison, not a literal inventory of what is on disk. That is what keeps a skipped file diverging - and therefore reported - on every later run, rather than quietly reading as current once it has been skipped once. A path taken with --take-release is the opposite case and the reason the flag exists: it was written, so it matches the digest the stamp records and stops being reported at all. 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 (chemenu.kb_state.chain over the new tree's instructions/migrations/, read via a directory argument to load_migrations) but never runs any of it - there is no migrate run. Refuses before touching the source at all when: VERSION or .wikitool-release.json (with a files block) is missing locally, .wikitool-kb.json is missing, a migration is already outstanding against the installed machinery, or the working tree is dirty (not being a git repository at all is a WARN, not a refusal). Refuses after reading the source when: it carries no VERSION/.wikitool-release.json/files block, its version is older than or equal to the installed one (equal is a no-op success), it is a pre-release (-beta.N) without --pre, or --take-release names a path this run does not classify as locally changed. Reports, but does not block on, a crossed compatibility boundary. Never touches git - no commit, no push (invariant 5). The closing report carries no step list of its own: everything after the swap is one order, written in instructions/upgrade-instance.md, which the report names and which resumes at instructions sync. What a human decides before the swap - which release, whether to take it, where the tarball comes from - is INSTALL.md § "Version und Updates"
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
EXIT STATUS
- 0 success
- 1
VERSIONis missing or unparseable
ON FAILURE
- Fix
VERSIONand retry
NOTES
Development tree, or a distribution with its export date and origin. Bare wikitool version is an alias for this. Read-only, offline, and exempt from the Iteration Budget Gate
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
EXIT STATUS
- 0 success
- 1 The feed could not be reached, answered non-JSON, or carried no
tag_name. Never answers "up to date" for a question it could not ask
ON FAILURE
- 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
NOTES
Ask the origin's release feed whether a newer stack exists, and whether the step crosses a compatibility boundary (state: current|update|migration|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, times out, and reports an unreachable feed as an error rather than as "up to date". The feed is $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. Read-only and exempt from the budget gate
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
EXIT STATUS
- 0 success
- 1 An unparseable
--version, an unreadableVERSIONwhen--versionis omitted, or a missingCHANGES.md. No entry for the requested version is an error only where the feed cannot answer either: in a tree with no release stamp (a dev checkout - write the entry, orversion bump), with--offline, or when the feed could not be reached or returned a release with an emptybody. Every one of those failures names the stamp'srelease_urlwhere it has one, so a run that cannot read the notes is still told where they are
ON FAILURE
- Fix the named argument or file, then retry. A feed failure is transient - retry once, then read the release page the error names. Safe to retry
NOTES
Default: this tree's VERSION. The CHANGES.md entry where there is one, and where there is not, the feed's latest release notes. The fallback exists because an instance's CHANGES.md is a stub dist upgrade never overwrites (chemenu.ownership.is_upgrade_preserved), so the local file can never carry the entry - not today and not after any future release, which made the command permanently unanswerable exactly where the release notes are most needed. It is reached only with a release stamp present, i.e. only from a dist export tree: a dev checkout keeps the plain error, which is what keeps the origin repo and CI offline. 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. Only the feed's latest release can be asked for (update_url is the one URL a stamp records, and composing a by-tag URL out of it would be guessing at an API shape), so a returned version other than the one asked for is named on stderr and printed anyway - the expected shape before an upgrade, where VERSION still names the release being left. --offline refuses the call and fails with the stamp's release_url instead. Read-only and exempt from the budget gate
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 -
VERSIONthenCHANGES.md - budget: counted
- network: no
EXIT STATUS
- 0 success
- 1 More or fewer than one of
--major/--minor/--patch, an empty--title, an unknown--impact, a missingVERSION/CHANGES.md,VERSIONand the changelog's newest entry naming different versions, an escalation to a boundary crossing without--breakingor with neither a migration document nor--no-migration,--breaking/--no-migrationon a bump that crosses nothing, or--migration-requiredcombined with--no-migration, on a bump with no running candidate, with no--no-migrationline to retract, or without a migration document already targeting the new base
ON FAILURE
- Not idempotent: a second run escalates or continues the candidate again. If the outcome is uncertain, read
VERSIONand the top ofCHANGES.mdbefore retrying
NOTES
VERSION gets a -beta.N suffix, never a second 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. Opens the matching CHANGES.md entry on the first bump of a candidate (heading, date, author, and a machine-managed <!-- wikitool:bumps --> list of every --title collected so far, graded by --impact, default medium) and updates that same entry in place on every later bump of the same candidate - one entry per candidate, not one per bump. The list renders grouped under **High/Medium/Low impact** headings (empty groups omitted), except when every bump so far is medium, where it stays the flat, ungrouped list the region always had - version regrade corrects a grade after the fact. Refuses more or fewer than one part, an empty title, an unknown --impact, and a VERSION/newest-changelog-entry mismatch. 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. The two then behave differently on a second crossing, because they answer different questions: a further --breaking joins the ones already recorded (one reason per crossing - rendered flat on the marker line while there is only one, as bullets under a bare marker from the second onward, and repeating a reason verbatim is a no-op), while a further --no-migration replaces the single line that says whether content has to change. A candidate crossing the boundary twice is the normal shape of a long-running one, and each crossing is a separate thing an operator has to act on; whether content migrates stays one yes/no about the candidate as a whole. There is deliberately no retraction path for a single accumulated --breaking reason - --migration-required retracts the migration line, and nothing retracts a breaking one. A later bump of the same candidate that finds out --no-migration was wrong after all retracts that line with --migration-required instead of restating --no-migration - refused without a migration document already targeting the new base, and without an existing --no-migration line to retract. Which part a change earns stays a judgment call: the command enforces that a crossing documents itself, never that the part was chosen correctly
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.mdonly, and only when indices are given - budget: exempt_without_args
- network: no
EXIT STATUS
- 0 success
- 1 A missing
VERSION/CHANGES.md,VERSIONand the changelog's newest entry naming different versions, a topmost entry with no bump list, an index outside the rendered list's range, indices given without--impact, or an unknown--impact
ON FAILURE
- The bare listing never writes anything. A write is not idempotent against a changed list: re-running the same indices after a first success regrades whatever is at those positions now, which may no longer be the same bumps - list again before retrying
NOTES
1-based rendered position (no arguments - the correction path for a --impact judgement made at bump time), or change one or more of them in a single call: version regrade 3 7 --impact high grades both against a single read of today's list, not position 3 first and then position 7 against whatever that produced. Touches only the topmost entry's bump list - never VERSION, never any other part of CHANGES.md. The bare listing is read-only and exempt from the Iteration Budget Gate, like version notes; a call with indices writes CHANGES.md and is counted like version bump. Refuses an index outside the rendered list's range, an unknown --impact, indices given without --impact, a missing VERSION/CHANGES.md, a VERSION/newest-changelog-entry mismatch, or a topmost entry with no bump list at all
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 -
VERSIONthenCHANGES.md - budget: counted
- network: no
EXIT STATUS
- 0 success
- 1 A missing
VERSION/CHANGES.md,VERSIONalready a release (no running candidate),VERSIONand the changelog's newest entry naming different versions, or (from two bumps on) an entry with no summary paragraph above the changesets
ON FAILURE
- Not idempotent: a second run fails outright once the suffix is gone. If the outcome is uncertain, read
VERSIONbefore retrying - a release-shapedVERSIONmeans it already ran
NOTES
Ends the pre-release phase version bump started. Without --title the heading keeps whichever bump last set it; with it, the heading's title is replaced - the normal case for a candidate that collected several bump titles, since the entry wants a summarising heading rather than the most recent one. Leaves the entry's machine-managed bump-title list untouched, as the record of what happened. Refuses when the candidate collected two or more bumps and the entry still carries no 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 and reports the same refusal. Commits nothing and pushes nothing (invariant 5) - the following publish moves VERSION onto main, which release.yml reacts to. Refuses when VERSION is already a release (no running candidate to fix), or when the changelog's newest entry does not match VERSION
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.jsonis missing (content version undeclared), orVERSIONis unreadable
ON FAILURE
- 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--fromis not a revision in this repository
ON FAILURE
- Exit 1 from
--fail-on-errormeans "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
- Not idempotent for a required migration: it advances the chain. For "not the next link", run
migrate statusand apply them in the order it prints - never force the order. Recording anofferedmigration 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
--forcewas not passed
ON FAILURE
- Safe to re-run with the same version. If a declaration exists, it is almost always
migrate donethat 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
- 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 eithergit commit --no-edityourself orgit 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/--untilis not a revision in this repository
ON FAILURE
- 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(aWARN, e.g. no remote or noWIKITOOL_SESSION_ID, does not exit 1)
ON FAILURE
- 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, sotools/wikitoolwith no variable set behaves exactly as it always has. Nothing under the root is bound at import time:KB_DIR,RAW_DIRand the rest follow whateverROOTcurrently is, which is what makes the half-repointed state (a movedROOTwith a staleKB_DIR) unconstructible rather than merely discouraged. - The library boundary.
chemenu.api.Corpusis the in-process entry point: it takes a corpus root, returns the same structures the--jsonforms print, and raisesChemenuErrorwhere the CLI printsERRORand exits 1. It is read-only structurally - nothing underchemenu.commandsis imported from it, sonew,publishand the rest are not reachable, rather than filtered. The cores it calls (search/service.py,lint_core.py,types_core.py) import notyperand norich; the modules undercommands/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,lintandstatusoverchemenu.api, onstdioorstreamable-http. Its dependency is optional and lives inrequirements-mcp.txt, so a CLI-only instance does not install it. Five of its tools are structurally read-only - nothing undercommands/is importable from the server, sonew/touch/xref/cite/publish/migrateare unreachable rather than filtered - and every response carries the commit it was computed from. The one exception issubmit(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_withinresolves every target and refuses anything outsidemcp-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 undercommands/, 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 appendonly produce structurally-correct frontmatter and body skeletons/edits - the prose (Description, Summary, judgment calls about relationships) is still written by the LLM afterwards.lintonly reports what's mechanically verifiable. Contradictions, staleness judgment, and "what's worth writing next" remain the LLM's job;lintproduces 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 inmain()(cli.py) before Typer dispatches to any subcommand, so it applies uniformly without each command needing its own opt-in. State lives in the gitignoredtools/.wikitool_session/budget.json, keyed bychemenu.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 statusis exempt so the situation stays reportable after the gate trips;budget resetis 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-errorbefore everywikitool publish. CI already runs it on every push (.gitea/workflows/ci.yml), which catches it after the fact rather than before.