tools: command records, Git group - NOTES as bullets, one exit line per cause, examples and prohibitions (#142)
CI / verify (push) Successful in 1m15s
Release / release (push) Successful in 37s

Files changed:
- CHANGES.md
- VERSION
- tools/CONTRACT.md
- tools/README.md
- 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
This commit is contained in:
torben committed 2026-09-26 08:25:41 +02:00
1 parent 2a60f3a265
commit fd0f60b2e7
34 files changed
+601 -285

No files matched your search

+122 -60
View File
@@ -174,8 +174,8 @@ Scaffold a new wiki page of any type.
**ON FAILURE**
- new <type>: 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 as `new <type>` - the last of those points at the `access: "api"` instance instead and refuses on every `--resume` retry 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 is `superproductivity`-only: that provider *can* write but cannot create the project itself and a human must, per the printed instructions; re-run with `--resume` once 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. `caldav` never produces this outcome - `MKCALENDAR` is a real collection-creation verb, so a valid, non-colliding name always creates the list itself
- new <type>: Duplicate page title, unknown type, invalid `--set` value, or a `raw_files` path that doesn't exist -> Not transient; fix the argument and retry once. Never hand-craft the page instead
- new project: Everything `new <type>` covers, **plus**: the name is already taken in the tracker (case-insensitively - for `caldav` this is checked against every list in the account, not only the ones counted as projects), `--resume` was passed for a type other than `project`, or the configured provider's access path has no write path at all (Super Productivity's `access: "snapshot"`) -> A collision, a bad `--set`, or a read-only access path is not transient, same as `new <type>` - the last of those points at the `access: "api"` instance instead and refuses on every `--resume` retry 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 is `superproductivity`-only: that provider *can* write but cannot create the project itself and a human must, per the printed instructions; re-run with `--resume` once 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. `caldav` never produces this outcome - `MKCALENDAR` is a real collection-creation verb, so a valid, non-colliding name always creates the list itself
**NOTES**
@@ -204,7 +204,7 @@ Create one open item in the configured task tracker - never a kb/ page.
**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** - unlike `new 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
- No `.wikitool-tasks.json`, neither or both of `--project`/`--inbox` given, a `--follow-up-at` without `--waiting` or not `YYYY-MM-DD`, a `--project` name matching no tracker project, `--waiting` against a provider with no way to represent it right now (Super Productivity: the `waiting` tag does not exist), or a read-only access path (Super Productivity's `access: "snapshot"`) -> 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** - unlike `new 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**
@@ -233,7 +233,7 @@ List a project's open items - id, title, and whether each carries the WAITING st
**ON FAILURE**
- Not transient; configure a tracker first, then retry once. A `--project` matching no tracker project is not an error here - see its Commands row
- No `.wikitool-tasks.json` -> Not transient; configure a tracker first, then retry once. A `--project` matching no tracker project is not an error here - see its Commands row
**NOTES**
@@ -262,7 +262,7 @@ Mark one tracker item done - never delete it.
**ON FAILURE**
- Not transient; fix the id (re-run `task list` or `review` to get a current one) or point at an `access: "api"` instance, then retry once. **Never exit 42**, same reasoning as `task new`
- No `.wikitool-tasks.json`, an `--id` matching no tracker item right now, or a read-only access path (Super Productivity's `access: "snapshot"`) -> Not transient; fix the id (re-run `task list` or `review` to get a current one) or point at an `access: "api"` instance, then retry once. **Never exit 42**, same reasoning as `task new`
**NOTES**
@@ -291,7 +291,7 @@ Bump a page's `modified:` date and optionally rewrite its other frontmatter fiel
**ON FAILURE**
- Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it
- 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`/`--remove` on a non-array field; a `raw_files:` path that doesn't exist -> Fix the argument and retry once. Safe to re-run as-is: `--set` and `--add` are idempotent, and `--remove` of an already-absent element succeeds while reporting it
**NOTES**
@@ -320,7 +320,7 @@ Rename a page, or repoint references that name a page that never existed.
**ON FAILURE**
- Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` first to see the blast radius. Never fix up references by hand instead
- Neither `--from` nor `--to` is a page, target title already taken, or `--from` equals `--to` -> Safe to retry once as-is; each page's rewrite is idempotent. Use `--dry-run` first to see the blast radius. Never fix up references by hand instead
**NOTES**
@@ -349,7 +349,7 @@ Delete a page and mechanically de-link it from the rest of the wiki.
**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
- Page not found, **or** other pages still reference it and `--yes` was not passed -> 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**
@@ -379,7 +379,7 @@ Move a page (or every misplaced page) to the directory its type-spec computes.
**ON FAILURE**
- Safe to retry once as-is; a page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced. Use `--dry-run` first to see the blast radius. Never choose a directory by hand instead
- Neither or both of `--page`/`--reconcile` given, the named page not found, it has no `type:` to compute a placement from, or the destination already exists -> Safe to retry once as-is; a page already at its computed location is reported and left alone, and `--reconcile` only re-moves what is still misplaced. Use `--dry-run` first to see the blast radius. Never choose a directory by hand instead
**NOTES**
@@ -410,7 +410,7 @@ Declare that A <rel> B.
**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
- Page A or B not found, or a page's type declares no `related:` field -> 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**
@@ -439,7 +439,7 @@ Remove a cross-reference: the inverse of `xref add`.
**ON FAILURE**
- Safe to retry freely; removing an absent link is a no-op
- Page A not found (B is allowed not to exist) -> Safe to retry freely; removing an absent link is a no-op
**NOTES**
@@ -468,7 +468,7 @@ Batch-link a source page to every entity/concept it mentions.
**ON FAILURE**
- Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked
- Source page not found, an entity in `--entities` doesn't exist, or the source page itself could not be written after its targets were -> Use `--dry-run` first; safe to retry. `sources trace --page "<Title>"` shows who was already linked
**NOTES**
@@ -497,7 +497,7 @@ Show the edges out of and into a page.
**ON FAILURE**
- Check the exact title with `search`; a wikilink target is not always the page's stem
- Page not found -> Check the exact title with `search`; a wikilink target is not always the page's stem
**NOTES**
@@ -550,7 +550,7 @@ Upsert a `[^cite-id]: [[Source - X]]` definition in a page's footnotes region.
**ON FAILURE**
- Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
- Page or source not found -> Safe to retry; upserting the same (page, source, file) pair twice reuses the existing id and changes nothing the second time
**NOTES**
@@ -579,7 +579,7 @@ Reconcile each page's footnotes region against its actual `[^id]` references.
**ON FAILURE**
- Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run `cite add`) and re-run
- Neither or both of `--page`/`--all` given, or page not found -> Safe to retry freely. An undefined-reference report is not a failure - fix the reference (or run `cite add`) and re-run
**NOTES**
@@ -610,7 +610,7 @@ Regenerate the catalog from every page's frontmatter.
**ON FAILURE**
- Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges
- Rare I/O error only -> Safe to retry freely - the plan is always recomputed from the pages currently on disk, so a re-run converges
**NOTES**
@@ -639,7 +639,7 @@ Append a formatted entry to `kb/log.md`.
**ON FAILURE**
- **Not idempotent.** If the previous run's outcome is uncertain, check the tail of `kb/log.md` before retrying
- Invalid `--op` or unreadable `--body-file` -> **Not idempotent.** If the previous run's outcome is uncertain, check the tail of `kb/log.md` before retrying
**NOTES**
@@ -694,7 +694,7 @@ Run structural lint checks against kb/.
**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"
- Only with `--fail-on-error`: hard findings exist -> 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**
@@ -723,7 +723,7 @@ Find pages in `kb/` by text and/or frontmatter.
**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 `--regex` rather 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"
- `rg` is not installed or did not finish within 30 s, a malformed `--field` predicate, an unknown field name, or an unknown `--backend` -> 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 `--regex` rather 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**
@@ -752,7 +752,7 @@ The GTD weekly review.
**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
- 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 -> 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**
@@ -807,7 +807,7 @@ Trace provenance in either direction: raw file, or page.
**ON FAILURE**
- Fix the argument and retry
- Neither or both of `--raw`/`--page` given, `--raw` names a file no source page covers (reported as a plain finding plus exit 1, not the usual `ERROR`-prefixed rejection), or `--page` names an unknown page -> Fix the argument and retry
**NOTES**
@@ -836,7 +836,7 @@ Regenerate the `kb/provenance.md` reverse index.
**ON FAILURE**
- Safe to retry freely
- Rare I/O error only -> Safe to retry freely
**NOTES**
@@ -869,8 +869,8 @@ Promote one or more files from `incoming/` into `raw/`.
**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 `--replaces` and renaming in `incoming/` as the two routes and neither is the tool's to pick. Never choose the destination by hand instead - that is the decision this command exists to take away
- raw accept --replaces: 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
- raw accept: A file does not exist, is not under `incoming/`, or is nested more than one level below it, two files in one call share a filename, a target path already exists, `--fidelity`/`--authority` is missing (unless `--replaces`) or names `unknown` or a value outside the schema's enum, the target name is already occupied anywhere under `raw/` by something the call does not own, `--page` names an unknown page or one with no `raw_files:` yet, an existing `raw_files:` entry is missing on disk, a file to be moved has more than one owning page, or `--page` would overwrite an already-set `fidelity`/`authority` with a different value -> Fix the named argument and retry once. Safe to retry as-is once the cause is fixed: a file already at its computed destination is what "already exists" reports, not a partial prior run to resume. A stem-occupied refusal is not fixed by retrying at all - it names `--replaces` and renaming in `incoming/` as the two routes and neither is the tool's to pick. Never choose the destination by hand instead - that is the decision this command exists to take away
- raw accept --replaces: More than one incoming file, `--page` also given, the incoming file does not exist or is not under `incoming/` (or is nested more than one level below it), its filename differs from the target's, the target does not lie under `raw/` or does not exist, `--fidelity`/`--authority` names `unknown` or a value outside the schema's enum, or the target has more than one owning source page -> Fix the named argument and retry once. Every check runs before the filesystem is touched, so a refusal leaves both files exactly as they were
**NOTES**
@@ -923,7 +923,7 @@ Print one submission's manifest in full.
**ON FAILURE**
- Fix the id (see `upload list`) and retry
- Unknown or malformed submission id -> Fix the id (see `upload list`) and retry
**NOTES**
@@ -954,7 +954,7 @@ Filename, size, sha256, submitter, submitter source (the header name, not a clai
**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 occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it first
- Unknown or malformed submission id, the submission's file is missing from `mcp-upload/<id>/`, or `incoming/<filename>` already exists. **Exit 42, not 1**, when `--confirm` is absent or does not match the manifest's current token - the Upload Review Gate, not a validation error -> For exit 42: show the user the full manifest and the exact `--confirm <token>` re-run line printed, and stop - the same rule as every other exit-42 gate (AGENTS.md invariant 6). For the three exit-1 cases: fix the named argument and retry once; an occupied `incoming/<filename>` is not fixed by retrying unchanged - rename or clear it first
**NOTES**
@@ -983,7 +983,7 @@ Delete a submission's material, keeping only its ledger trail.
**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
- Unknown or malformed submission id, or an empty `--reason` -> Fix the argument and retry once. Not idempotent against a second call with the same id: the first call already deleted the submission, so a retry reports "unknown id" - that is confirmation, not a failure
**NOTES**
@@ -1008,19 +1008,41 @@ Fetch `<remote>/<branch>` and bring the local branch up to date with it.
- network: no
- gates: rebase-review
**EXAMPLES**
- `tools/wikitool sync`
- `tools/wikitool sync --confirm-rebase <token> # re-run after exit 42, once the user approved`
**EXIT STATUS**
- 0 success
- 1 The automatic rebase hit a real conflict (git failed)
- 42 needs clearance - rebase-review (see AGENTS.md § Gates)
- 0 No remote configured, or the remote cannot be reached - reported and skipped, not a failure
- 1 The automatic rebase hit a real conflict (git failed); it is aborted cleanly
- 42 Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff
**ON FAILURE**
- 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
- The automatic rebase hit a real conflict (git failed); it is aborted cleanly -> Do not retry and do not force - resolve the conflict manually, then re-run
- Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
**NEVER**
- Never retry a conflict unchanged, and never force past it.
- Never pass a `--confirm-rebase` token the user has not seen and approved.
**NOTES**
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`
- Fetches `<remote>/<branch>`, then: fast-forwards when only the remote moved; rebases the local commits on top when both sides moved but touched disjoint files; exits 42 (rebase-review gate) when both sides touched the same file.
- A refused call performs no rebase attempt and leaves the branch where it was.
- The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides; either one moving makes it stale.
- Makes no commit, no push, and no forced operation of any kind.
- Run it once at the start of a writing session.
**SEE ALSO**
- `wikitool publish` - runs the same reconcile before it commits and pushes
- `instructions/session-setup.md` - where a session runs `sync`
- `instructions/gates.md` - the gate procedure
#### `publish`
@@ -1034,24 +1056,64 @@ Reconcile with `<remote>/<branch>`, then stage all changes, commit, and push.
- effect: write
- idempotent: no
- atomic: No - sequential git operations, but both gates run before staging
- atomic: No - sequential git operations, but every gate runs before staging
- budget: counted
- network: no
- gates: mass-update, publish-remote, rebase-review
**EXAMPLES**
- `tools/wikitool publish --message "ingest: docker-cheatsheet"`
- `tools/wikitool publish --confirm <token> --message "ingest: docker-cheatsheet" # re-run after a Mass-Update exit 42, once the user approved`
- `tools/wikitool publish --confirm-rebase <token> --message "ingest: docker-cheatsheet" # re-run after a rebase-review exit 42, once the user approved`
**EXIT STATUS**
- 0 success
- 1 git failed, 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`/`-y` was passed. **Exit 42, not 1**, when the Mass-Update Gate, the rebase-review gate (raised by the same reconcile `sync` performs), or the Publish-Remote Gate refuses
- 42 needs clearance - mass-update, publish-remote, rebase-review (see AGENTS.md § Gates)
- 1 git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase
- 1 The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case
- 1 `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error
- 1 `.wikitool-remotes.json` is unreadable or has no usable `allowed_push_urls` list
- 42 Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset
- 42 Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff
- 42 Publish-Remote Gate: `.wikitool-remotes.json` exists and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL
**ON FAILURE**
- 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
- git failed - `git add`, `git commit`, `git push`, or the reconcile's automatic rebase -> Do not retry and do not force - report and ask the user. `publish` has already made its one retry of a rejected push itself, where a reconcile resolved the rejection
- The push target (`--branch`) is not the checked-out branch, or HEAD is detached; the unborn branch of a fresh `git init` is not this case -> Check out the branch you mean to publish, or pass `--branch <checked-out branch>`, then retry once
- `--yes`/`-y` was passed - the flag does not exist and fails with an explicit error -> Drop it. The Mass-Update Gate is cleared only with `--confirm <token>` from the gate's own refusal output
- `.wikitool-remotes.json` is unreadable or has no usable `allowed_push_urls` list -> Show the error to the user and stop - a malformed file is not permission, and fixing or deleting it is theirs to do
- Mass-Update Gate: `--threshold` (default 10) or more counted files would be committed, or the `--confirm` token does not match this changeset -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
- Rebase-review gate: `<remote>/<branch>` moved and both sides changed the same file; the output lists the upstream commits, the overlapping files and their diff -> Show the user the command's full output verbatim and stop. Once they have approved it, run the re-run line the output prints, which carries `--confirm-rebase <token>`. Without that token, or with a wrong, invented or superseded one, it exits 42 again with the current state
- Publish-Remote Gate: `.wikitool-remotes.json` exists and the push URL of `--remote` is not listed in it, or `--remote` resolves to no push URL -> Show the user the push URL it names and the allowed ones, and stop. This gate has no token and no flag: only the user resolves it, by adding the URL to that file
**NEVER**
- Never retry a failed git step unchanged, and never force (`--force`, `--force-with-lease`).
- Never pass a `--confirm` or `--confirm-rebase` token the user has not seen and approved.
- Never edit `.wikitool-remotes.json` to get past a Publish-Remote refusal - that is opening a gate on your own initiative.
**NOTES**
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
- Order: branch check and Publish-Remote Gate, then the reconcile with `<remote>/<branch>`, then the Mass-Update Gate, then `git add -A`, commit and push. `--no-push` skips all but the Mass-Update Gate and the commit.
- Reconcile: fetches `<remote>/<branch>`, fast-forwards when only the remote moved, rebases the local commits on top when both sides moved but touched disjoint files, and exits 42 (rebase-review gate) when both sides touched the same file. A refused reconcile performs no rebase attempt. The `--confirm-rebase` token covers the exact upstream state and the set of files touched on both sides.
- The push target must be the checked-out branch; this is checked before anything is staged. The unborn branch of a fresh `git init -b main` counts as checked out, so the first publish of a new instance works; a real detached HEAD is refused.
- With nothing new to stage, a local commit the remote lacks is still pushed: one left behind by an earlier publish whose push failed, or every commit when the remote answers but does not have the branch yet (a new, empty remote repository).
- A remote that cannot be reached is not read as lacking the branch: on a clean tree `publish` reports "Nothing to commit" and attempts no push.
- A rejected push gets exactly one more reconcile-and-push; never more than one.
- Mass-Update Gate: counts the files that would be committed, refuses with exit 42 at `--threshold` (default 10) or more, and prints a review report - a scale line (file count, total lines added/removed, status breakdown), attention notes where they apply (deletions by name, control-plane and harness-config touches, published pages, the largest single change, binaries), and every counted path grouped by area with its status and churn. The gate is evaluated before anything is staged, so a refused publish leaves the working tree untouched.
- Never counted and never shown for approval, but committed like everything else: anything under `work/`, and the files `wikitool` generates itself (`kb/index.md`, `kb/log.md`, `kb/provenance.md`, every `INDEX.md`). The refusal line accounts for both, by reason.
- The `--confirm` token covers each counted path, its contents and the publish target: a different file list or edited contents need a new clearance.
- Publish-Remote Gate: when the checkout carries `.wikitool-remotes.json` and the push URL of `--remote` is not listed in it, exits 42 before the reconcile fetches anything. The URL is read with `git remote get-url --push`, so a repointed remote does not pass on its name. An absent file means unrestricted; a malformed one is an error, not permission.
- `--path` (repeatable) scopes the whole operation - gate count, staging and commit - to that subtree.
- After a successful commit or push whose changed files include `tools/`, `types/`, `instructions/`, `AGENTS.md` or a path ending in `CONTRACT.md`, prints one reminder line: the phase past this point (an issue-body rewrite, `docs/` staleness, a changelog entry's accuracy) is not covered by `docs verify`, `instructions verify` or `pytest`. It is not a gate: no exit code change, nothing to clear, and silent for an ordinary content publish.
**SEE ALSO**
- `wikitool sync` - the same reconcile on its own, without committing or pushing
- `instructions/gates.md` - the gate procedure
- `instructions/setup-instance.md` - the first publish of a new instance
### Workshop runs and session budget
@@ -1078,7 +1140,7 @@ Scaffold `work/<runkey>/` for one workshop run.
**ON FAILURE**
- A collision is not transient: resume the existing run instead, or pass `--again` if the tree itself changed. Never create a numbered variant by hand
- Neither or both of `--input`/`--key` given, `--input` outside `raw/`, a `--key` that is empty or starts with `ingest-`, or the workshop already exists -> A collision is not transient: resume the existing run instead, or pass `--again` if the tree itself changed. Never create a numbered variant by hand
**NOTES**
@@ -1107,7 +1169,7 @@ Delete a finished workshop.
**ON FAILURE**
- For "not confirmed": check the listed files are no longer needed, confirm the conclusions are in `kb/`, then re-run with `--yes`
- Unknown run key, or `--yes` was not passed -> For "not confirmed": check the listed files are no longer needed, confirm the conclusions are in `kb/`, then re-run with `--yes`
**NOTES**
@@ -1160,7 +1222,7 @@ Clear the current session's (or every session's) iteration budget state.
**ON FAILURE**
- Get the user's approval, then re-run with `--yes`
- `--yes` not passed -> Get the user's approval, then re-run with `--yes`
**NOTES**
@@ -1215,7 +1277,7 @@ Print one type's full contract.
**ON FAILURE**
- Fix the name and retry
- Unknown type name -> Fix the name and retry
**NOTES**
@@ -1244,7 +1306,7 @@ Publish every `instructions/<name>/SKILL.md` into the harness skill directories.
**ON FAILURE**
- Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not; otherwise fix the named cause and retry
- No skills found under `instructions/`, or a target directory is not a published skill (no `SKILL.md`) and `--force` was not passed -> Check whether the flagged target holds anything worth keeping, then re-run with `--force` if not; otherwise fix the named cause and retry
**NOTES**
@@ -1273,7 +1335,7 @@ Check the instruction layer.
**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-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins
- Nothing found under `instructions/` at all, a malformed instruction or `SKILL.md`, a `SKILL.md` carrying a relative markdown link, a published copy that drifted from its source, an instruction nothing references (or, for `manual: true`, one that IS linked from AGENTS.md, CLAUDE.md, or a skill and so risks running implicitly), or something under `instructions/dev/` referenced from outside it and outside a `dist:strip` block -> Fix the flagged file, then re-run. For a relative link in a `SKILL.md`, rewrite it as a repo-root-relative plain path instead. For drift, re-run `sync` instead of hand-editing the published copy - the source under `instructions/` always wins
**NOTES**
@@ -1326,7 +1388,7 @@ Check the docs that mirror the code.
**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.yaml` if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region. For a dead link: fix the `../` count or the target's name
- A command, contract, or type-form mismatch was found, a type-spec's own frontmatter fails its schema, a shipped `.md`/`.template` cites an issue number, a reference file's table-of-contents region is missing or stale, or a reference file's relative markdown link does not resolve to an existing file -> Fix the documentation it names, then re-run. For a type-spec's own frontmatter: fix the field, or add a matching line to `types/type-spec.schema.yaml` if the field is legitimately new. For an issue reference: say what was decided instead of pointing at where, or move the pointer behind a `<!-- dist:strip-start/end -->` block. For a table of contents: run `docs toc --apply` - never hand-write the region. For a dead link: fix the `../` count or the target's name
**NOTES**
@@ -1355,7 +1417,7 @@ Create, refresh or remove the generated table-of-contents region.
**ON FAILURE**
- Nothing to fix - re-run with `--apply` to write what the dry run listed. If `docs verify` still reports a stale region afterwards, the file's `##` headings changed in between; run it again
- Never fails on content: a file with no `##` heading, or one at or under the threshold, is simply left without a region -> Nothing to fix - re-run with `--apply` to write what the dry run listed. If `docs verify` still reports a stale region afterwards, the file's `##` headings changed in between; run it again
**NOTES**
@@ -1434,7 +1496,7 @@ Score one traced session.
**ON FAILURE**
- Run `eval sessions` to see which ids exist. A session records nothing when telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in (`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe to retry
- No trace exists for the named session -> Run `eval sessions` to see which ids exist. A session records nothing when telemetry is off - `WIKI_TRACE=0`, or a distributed instance with no opt-in (`wikitool doctor` says which) - so an absent trace is not necessarily a fault. Safe to retry
**NOTES**
@@ -1465,7 +1527,7 @@ Write a contentless, distributable copy of this repo's machinery.
**ON FAILURE**
- Point `<target>` at an empty (or new) directory and retry. Never merge into a non-empty one by hand
- Target exists and is not empty, is not a directory, or the tree has no readable `VERSION` -> Point `<target>` at an empty (or new) directory and retry. Never merge into a non-empty one by hand
**NOTES**
@@ -1494,7 +1556,7 @@ Apply a stack update `dist export` produced - the write half of `version check`.
**ON FAILURE**
- For every refusal above: fix the named precondition and retry - none of them are transient. For a rejected `--take-release` path: 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-local` to 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
- 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 no `VERSION`/stamp/`files` block, a source version that is older than, equal to, or (without `--pre`) a pre-release relative to the installed one, a `--take-release` path that is not classified as locally changed (the one refusal a `--dry-run` also raises), or one or more locally changed files that neither `--keep-local` nor a `--take-release` answers for -> For every refusal above: fix the named precondition and retry - none of them are transient. For a rejected `--take-release` path: 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-local` to 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**
@@ -1523,7 +1585,7 @@ Print this instance's stack version and where it came from.
**ON FAILURE**
- Fix `VERSION` and retry
- `VERSION` is missing or unparseable -> Fix `VERSION` and retry
**NOTES**
@@ -1552,7 +1614,7 @@ Ask the origin's release feed whether a newer stack exists.
**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
- 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 -> 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**
@@ -1581,7 +1643,7 @@ Print one version's release notes.
**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
- An unparseable `--version`, an unreadable `VERSION` when `--version` is omitted, or a missing `CHANGES.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, or `version bump`), with `--offline`, or when the feed could not be reached or returned a release with an empty `body`. Every one of those failures names the stamp's `release_url` where it has one, so a run that cannot read the notes is still told where they are -> 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**
@@ -1610,7 +1672,7 @@ Raise or continue the one running candidate between two releases.
**ON FAILURE**
- **Not idempotent**: a second run escalates or continues the candidate again. If the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying
- More or fewer than one of `--major/--minor/--patch`, an empty `--title`, an unknown `--impact`, a missing `VERSION`/`CHANGES.md`, `VERSION` and the changelog's newest entry naming different versions, an escalation to a boundary crossing without `--breaking` or with neither a migration document nor `--no-migration`, `--breaking`/`--no-migration` on a bump that crosses nothing, or `--migration-required` combined with `--no-migration`, on a bump with no running candidate, with no `--no-migration` line to retract, or without a migration document already targeting the new base -> **Not idempotent**: a second run escalates or continues the candidate again. If the outcome is uncertain, read `VERSION` and the top of `CHANGES.md` before retrying
**NOTES**
@@ -1639,7 +1701,7 @@ List the running candidate's bump titles with their impact grade, or change one
**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
- A missing `VERSION`/`CHANGES.md`, `VERSION` and 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` -> 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**
@@ -1668,7 +1730,7 @@ Fix the running candidate: strip `VERSION`'s `-beta.N` suffix and close its `CHA
**ON FAILURE**
- **Not idempotent**: a second run fails outright once the suffix is gone. If the outcome is uncertain, read `VERSION` before retrying - a release-shaped `VERSION` means it already ran
- A missing `VERSION`/`CHANGES.md`, `VERSION` already a release (no running candidate), `VERSION` and the changelog's newest entry naming different versions, or (from two bumps on) an entry with no summary paragraph above the changesets -> **Not idempotent**: a second run fails outright once the suffix is gone. If the outcome is uncertain, read `VERSION` before retrying - a release-shaped `VERSION` means it already ran
**NOTES**
@@ -1723,7 +1785,7 @@ Show the migrations this instance still owes, in the order they must run.
**ON FAILURE**
- For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise
- `.wikitool-kb.json` is missing (content version undeclared), or `VERSION` is unreadable -> For a missing declaration: run `migrate baseline <version>` once, then retry. Safe to retry freely otherwise
**NOTES**
@@ -1752,7 +1814,7 @@ Compare `kb/` against a git revision on the invariants a content migration must
**ON FAILURE**
- Exit 1 from `--fail-on-error` means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it
- Only with `--fail-on-error`: an invariant changed. Also exits 1 if `--from` is not a revision in this repository -> Exit 1 from `--fail-on-error` means "act on the findings", not "the tool is broken". A finding is never fixed by re-running - it names a page and what changed on it
**NOTES**
@@ -1781,7 +1843,7 @@ Record one migration as applied, advancing `kb_version` in `.wikitool-kb.json`.
**ON FAILURE**
- **Not idempotent** for a required migration: it advances the chain. For "not the next link", run `migrate status` and apply them in the order it prints - never force the order. Recording an `offered` migration *is* idempotent and safe to repeat
- Unknown version, no `.wikitool-kb.json`, nothing outstanding, or a *required* version that is not the next link in the chain -> **Not idempotent** for a required migration: it advances the chain. For "not the next link", run `migrate status` and apply them in the order it prints - never force the order. Recording an `offered` migration *is* idempotent and safe to repeat
**NOTES**
@@ -1810,7 +1872,7 @@ Declare `kb_version` once, for an instance predating `.wikitool-kb.json`.
**ON FAILURE**
- Safe to re-run with the same version. If a declaration exists, it is almost always `migrate done` that was wanted
- Unparseable version, or a declaration already exists and `--force` was not passed -> Safe to re-run with the same version. If a declaration exists, it is almost always `migrate done` that was wanted
**NOTES**
@@ -1841,7 +1903,7 @@ Take a stack update into a private instance's branch, machinery only.
**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 either `git commit --no-edit` yourself or `git merge --abort`. If the postcheck after commit finds a leak, the merge commit already exists and is **not** rolled back automatically - inspect it by hand; this is a bug report, not a retry
- Dirty working tree, a merge already in progress, the remote does not resolve, git refused to open the merge at all (unrelated histories), or a real conflict remains in `tools/`/`types/`/`instructions/` after the content stages and stack-owned paths were restored -> **Not idempotent, and not safe to retry unchanged.** For a dirty tree or an in-progress merge: fix the named precondition and retry once. For a real conflict: **do not retry, do not force** - resolve the named paths by hand (take the upstream side, or re-file the local change as an issue against the public repo per `instructions/private-instance.md`) and either `git commit --no-edit` yourself or `git merge --abort`. If the postcheck after commit finds a leak, the merge commit already exists and is **not** rolled back automatically - inspect it by hand; this is a bug report, not a retry
**NOTES**
@@ -1870,7 +1932,7 @@ Compare two revisions: did anything under a content stage change except through
**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
- A leak was found (content changed under a content stage through a path that is not stack-owned), or `--since`/`--until` is not a revision in this repository -> A finding is not fixed by re-running - it names the paths that leaked. Fix the revision argument and retry for the second case
**NOTES**
@@ -1901,7 +1963,7 @@ Check that this instance is correctly configured.
**ON FAILURE**
- Each finding names its own fix command; re-run after applying it
- At least one check reported `FAIL` (a `WARN`, e.g. no remote or no `WIKITOOL_SESSION_ID`, does not exit 1) -> Each finding names its own fix command; re-run after applying it
**NOTES**