Wiki Tend
One-shot maintenance: status, audit, normalize (tags + cross-links). Report first, repair only what's safe, and degrade gracefully wherever a convention the step depends on is absent.
The wiki model
A wiki is a folder whose index.md carries a ## Spaces heading. ## Spaces is the navigation contract: every space directly inside is listed there as - [label](path/index.md) — description, and tools traverse only what it lists. Spaces nest recursively; each space is itself a wiki one level down. Plain folders (no index.md) just group files. The markdown dialect is Obsidian — wikilinks, frontmatter, callouts, embeds; the companion obsidian-markdown and obsidian-bases skills cover the syntax.
Resolving the wiki
Resolution order: an explicit path from the user → the nearest CWD-ancestor folder that is a wiki, or carries a .wiki-spaces/ folder that is one (a folder's own space) → the wiki key in ~/.config/wiki-spaces/config. The user's words override the mechanics: "my wiki" means the configured one even when CWD sits inside another wiki (a company repo, say). When a CWD wiki and a different configured wiki both exist, announce which root you resolved; ask once if intent is ambiguous. Never silently operate on the wrong wiki.
The bundled script
scripts/ws.py sits next to this SKILL.md — stdlib python3 (3.9+), zero dependencies, read-only. It parses the contract, never the content: structure — traversal, scope, caps, drift — is the script's side; reading and judging meaning is yours. Invoke it by absolute path (your working directory is usually elsewhere):
python3 <skill-dir>/scripts/ws.py list --wiki <root> — spaces reachable via the ## Spaces contract, each with its entry description (--external to cross mounts).
… files --wiki <root> — markdown files reachable via the contract.
… grep <pattern> [-i] [-F] --wiki <root> — regex line search over those files, -F for a literal string (a name carrying metacharacters sweeps exact); prints rel:line: text, exits 1 on no match. The sweep primitive: a link worklist, a tag inventory, an escaping-reference check are each a pattern plus your judgment on the hits.
… check-size <target> [--stdin] --wiki <root> — cap verdict for a file; pipe planned content with --stdin to check before writing.
… audit --wiki <root> — contract drift, entries crossing a space boundary, over-cap or unreadable files, unhealthy mounts. Findings name their repair where one is safe to name (a missing entry prints the exact line to add); apply repairs as ordinary edits and re-run the audit to verify — the script never writes.
Trust the script's output over re-deriving structure by hand. Stdout is data; stderr carries the resolved root (audit prints it as its stdout header instead) and note: advisories naming whatever a walk skipped, the enclosing wiki when the root is nested inside one, and the configured wiki when the root is another — relay them when they could change the answer.
Trust scope and size discipline
Owned vs external is relative to the resolved root: anything under a folder named shared/ (at any depth), a git submodule, or a symlink into either is external; a symlink to a folder outside the tree answers to where it sits, like a clone. Reads cross owned spaces by default and enter external ones only when the user explicitly asks. Writes stay inside the targeted space; any other space — owned or external — is written only on explicit instruction.
Caps are UTF-8 bytes including frontmatter, keyed by basename: index.md 5000, log.md and hot.md 100000, any other *.md 15000.
- Caps belong to the user: a wiki that wants different ones sets them in
_meta/limits.md, and check-size reads them.
- Check caps with
check-size: pipe the planned content via --stdin before every write, new page or edit — a write that shrinks an over-cap file reports ok, progress, so repairs converge; a file checked on disk answers to the cap alone.
- An overflow is a signal about shape, not just size: distill the page, reshape the space, or promote a page that has grown into one — never truncate, never raise the cap.
- The exception is
log.md: an over-cap log rolls — move it whole to _archives/log-<YYYYMMDD>.md and start a fresh one — so the history archives, never shrinks.
Conventions are opt-in per wiki. Read the markers present at the scope root (the space the user targeted, else the resolved root) — log.md, _meta/taxonomy.md, _meta/limits.md, _meta/ignore.md, frontmatter on pages, _template.md, hot.md, .git — and degrade gracefully when one is absent. If log.md exists, append one line per operation:
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '<OP> <details>' >> <scope-root>/log.md
Procedure
Resolve the wiki (core block) and announce the root when it came from CWD or could be ambiguous. A freshly scaffolded wiki has nothing to tend — say so plainly rather than inventing work.
Detect conventions at the scope root (core block). Presence markers (log.md, taxonomy, hot.md) never inherit from a parent; _meta/limits.md, _meta/ignore.md, and _template.md follow their nearest-ancestor rule. Skip every mode whose marker is absent.
Pick the mode from the user's words:
| User says |
Mode |
| "wiki status", "what's in my wiki" |
status |
| "audit", "health check", "what needs fixing" |
audit |
| "fix tags", "normalize tags" |
normalize: tags |
| "link pages", "cross-reference" |
normalize: links |
| "tend wiki", "clean wiki" |
full sweep: status → audit → ask → normalize |
Status and audit only report. Normalize previews its changes and asks before writing — unless the user already said "fix" or "apply".
Status. list + files for the shape (add --external only when the user opted in). Report: spaces and their one-line descriptions, page counts per top-level folder, tag counts and the top 10 by usage (from a grep '^tags:' sweep, when frontmatter is in use), the last log.md line (when present), and git status (when .git exists — read-only; never commit or push).
Audit. Run audit and relay its findings in its own vocabulary. The script is the source of truth for these; don't re-derive them:
contract — a bare index (registered or not), a near-miss heading, a malformed or duplicate entry, an unregistrable child name, an entry naming a file rather than a space, an entry crossing another space's boundary, a dead second heading.
drift — an on-disk space not listed, or a listed entry with nothing on disk.
mount — a registered external mount that is no longer a wiki.
over-cap and unreadable files.
Add the judgment the script can't make:
- Remediation suggestions. For each over-cap file, suggest what its diagnosis calls for — distill (bloat) or reshape the space (siblings under a hub, a promotion, a regrouped layout), per
ws-update's overflow procedure; an over-cap log.md takes the archive roll instead (core block).
- Link integrity — content, so yours: sweep the link shapes (
grep -F '[[' …, grep -F '](' …), judge each hit against the files inventory and the disk, and report dead ones with the nearest-named page as the likely target — a _template.md body is exempt, its placeholder links being examples, not dead links. On a large wiki, sweep the spaces this pass touches and say what you skipped.
- Frontmatter completeness — only on pages that already carry frontmatter, only when the wiki uses a schema; mixed adoption is allowed.
- Structural prompts — a page pressing its cap (
check-size any page you suspect) with several distinct H2 sections is outgrowing its shape and is reshape-ready; a space whose index lists many accreted siblings wants a sub-space; a long-grown opt-in hot.md wants distilling into cold pages; a space holding one small page and no sub-spaces reads as over-structure — suggest demoting it to a page (ws-update's restructure reference).
Structural repair (close-out). Apply the safe structural repairs yourself, one edit each, re-auditing between rounds until the structural findings are gone — each re-audit re-derives findings from disk, so the rounds converge:
- Add the exact entry line a
missing entry finding prints, then trail it with a — description — the placement hint every later operation reads.
- Insert the
## Spaces heading a registered bare child lacks — only where the finding's hint asks for the insert; a near-miss hint asks for a rename, which stays reported as author intent.
- Remove an entry the audit says crosses another space's boundary — that space owns the deeper listing; register it there instead when it is missing.
Everything else is author intent — malformed or duplicate entries, unregistrable child names, entries naming files, near-miss headings, dead second headings, undeclared bare indexes, stale entries, unhealthy mounts, over-cap or unreadable files, and any dead links your own sweep surfaced — and stays reported for the user (offer to fix as a normal edit if the user says go; an undeclared bare index is a promotion decision).
Normalize: tags. Only when _meta/taxonomy.md exists at the scope root, and only over the scope's own pages — children spaces keep their own taxonomies; leave them out.
- Sweep the inventory with
grep '^tags:' --wiki <root>; read the frontmatter of any page the sweep leaves ambiguous — YAML shapes are yours to judge.
- Untagged pages are
files minus the sweep's pages, less the frontmatter-exempt special files: indexes, log.md, hot.md, _template.md.
- Flag non-canonical tags, over-tagged (>5) pages, and untagged pages; apply the alias mappings the taxonomy defines.
- An unknown tag on 2+ pages: suggest adding it to the taxonomy. A one-off: suggest the closest canonical tag.
Normalize: cross-links. Build a registry of scope-own pages (names, titles, frontmatter aliases when present); the special files (indexes, log.md, hot.md, _template.md) stay out of both the registry and the scan — navigation, history, scratch, and boilerplate neither give nor take links. Scan bodies for unlinked first mentions of registry entries and wrap the strongest matches as wikilinks — at most 2 new links per page, never inside code blocks or frontmatter, no self-links. Prefer inline linking; a trailing ## Related section is the fallback when no natural mention exists. On a large wiki, scan the spaces this pass touches and say what you skipped.
Log one TEND line per the core block when log.md exists, and close with the report.
Output
Per mode: a status table; an audit report quoting the script's findings plus your suggestions; normalize tables (tag → action, link added → where). End the full sweep with the one-line delta: issues before → after; left for you: <n>.
1---2name: ws-tend3description: Maintain the user's wiki's health. Use when the user says "tend wiki", "clean wiki", "audit wiki", "fix tags", "normalize tags", "link pages", "cross-reference", "wiki status", or wants a health check, tag audit, or cross-linking pass.4---56# Wiki Tend78One-shot maintenance: status, audit, normalize (tags + cross-links). Report first, repair only what's safe, and degrade gracefully wherever a convention the step depends on is absent.910<!-- ws:core -->11## The wiki model1213A wiki is a folder whose `index.md` carries a `## Spaces` heading. `## Spaces` is the navigation contract: every space directly inside is listed there as `- [label](path/index.md) — description`, and tools traverse only what it lists. Spaces nest recursively; each space is itself a wiki one level down. Plain folders (no `index.md`) just group files. The markdown dialect is Obsidian — wikilinks, frontmatter, callouts, embeds; the companion `obsidian-markdown` and `obsidian-bases` skills cover the syntax.1415## Resolving the wiki1617Resolution order: an explicit path from the user → the nearest CWD-ancestor folder that is a wiki, or carries a `.wiki-spaces/` folder that is one (a folder's own space) → the `wiki` key in `~/.config/wiki-spaces/config`. The user's words override the mechanics: "my wiki" means the configured one even when CWD sits inside another wiki (a company repo, say). When a CWD wiki and a different configured wiki both exist, announce which root you resolved; ask once if intent is ambiguous. Never silently operate on the wrong wiki.1819## The bundled script2021`scripts/ws.py` sits next to this SKILL.md — stdlib python3 (3.9+), zero dependencies, read-only. It parses the contract, never the content: structure — traversal, scope, caps, drift — is the script's side; reading and judging meaning is yours. Invoke it by absolute path (your working directory is usually elsewhere):2223- `python3 <skill-dir>/scripts/ws.py list --wiki <root>` — spaces reachable via the `## Spaces` contract, each with its entry description (`--external` to cross mounts).24- `… files --wiki <root>` — markdown files reachable via the contract.25- `… grep <pattern> [-i] [-F] --wiki <root>` — regex line search over those files, `-F` for a literal string (a name carrying metacharacters sweeps exact); prints `rel:line: text`, exits 1 on no match. The sweep primitive: a link worklist, a tag inventory, an escaping-reference check are each a pattern plus your judgment on the hits.26- `… check-size <target> [--stdin] --wiki <root>` — cap verdict for a file; pipe planned content with `--stdin` to check before writing.27- `… audit --wiki <root>` — contract drift, entries crossing a space boundary, over-cap or unreadable files, unhealthy mounts. Findings name their repair where one is safe to name (a `missing entry` prints the exact line to add); apply repairs as ordinary edits and re-run the audit to verify — the script never writes.2829Trust the script's output over re-deriving structure by hand. Stdout is data; stderr carries the resolved root (`audit` prints it as its stdout header instead) and `note:` advisories naming whatever a walk skipped, the enclosing wiki when the root is nested inside one, and the configured wiki when the root is another — relay them when they could change the answer.3031## Trust scope and size discipline3233Owned vs external is relative to the resolved root: anything under a folder named `shared/` (at any depth), a git submodule, or a symlink into either is external; a symlink to a folder outside the tree answers to where it sits, like a clone. Reads cross owned spaces by default and enter external ones only when the user explicitly asks. Writes stay inside the targeted space; any other space — owned or external — is written only on explicit instruction.3435Caps are UTF-8 bytes including frontmatter, keyed by basename: `index.md` 5000, `log.md` and `hot.md` 100000, any other `*.md` 15000.3637- Caps belong to the user: a wiki that wants different ones sets them in `_meta/limits.md`, and `check-size` reads them.38- Check caps with `check-size`: pipe the planned content via `--stdin` before every write, new page or edit — a write that shrinks an over-cap file reports `ok`, progress, so repairs converge; a file checked on disk answers to the cap alone.39- An overflow is a signal about shape, not just size: distill the page, reshape the space, or promote a page that has grown into one — never truncate, never raise the cap.40- The exception is `log.md`: an over-cap log rolls — move it whole to `_archives/log-<YYYYMMDD>.md` and start a fresh one — so the history archives, never shrinks.4142Conventions are opt-in per wiki. Read the markers present at the scope root (the space the user targeted, else the resolved root) — `log.md`, `_meta/taxonomy.md`, `_meta/limits.md`, `_meta/ignore.md`, frontmatter on pages, `_template.md`, `hot.md`, `.git` — and degrade gracefully when one is absent. If `log.md` exists, append one line per operation:4344```sh45printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '<OP> <details>' >> <scope-root>/log.md46```47<!-- /ws:core -->4849## Procedure50511. **Resolve the wiki** (core block) and announce the root when it came from CWD or could be ambiguous. A freshly scaffolded wiki has nothing to tend — say so plainly rather than inventing work.522. **Detect conventions at the scope root** (core block). Presence markers (`log.md`, taxonomy, `hot.md`) never inherit from a parent; `_meta/limits.md`, `_meta/ignore.md`, and `_template.md` follow their nearest-ancestor rule. Skip every mode whose marker is absent.533. **Pick the mode** from the user's words:54 | User says | Mode |55 |---|---|56 | "wiki status", "what's in my wiki" | status |57 | "audit", "health check", "what needs fixing" | audit |58 | "fix tags", "normalize tags" | normalize: tags |59 | "link pages", "cross-reference" | normalize: links |60 | "tend wiki", "clean wiki" | full sweep: status → audit → ask → normalize |6162 Status and audit only report. Normalize previews its changes and asks before writing — unless the user already said "fix" or "apply".634. **Status.** `list` + `files` for the shape (add `--external` only when the user opted in). Report: spaces and their one-line descriptions, page counts per top-level folder, tag counts and the top 10 by usage (from a `grep '^tags:'` sweep, when frontmatter is in use), the last `log.md` line (when present), and git status (when `.git` exists — read-only; never commit or push).645. **Audit.** Run `audit` and relay its findings in its own vocabulary. The script is the source of truth for these; don't re-derive them:65 - `contract` — a bare index (registered or not), a near-miss heading, a malformed or duplicate entry, an unregistrable child name, an entry naming a file rather than a space, an entry crossing another space's boundary, a dead second heading.66 - `drift` — an on-disk space not listed, or a listed entry with nothing on disk.67 - `mount` — a registered external mount that is no longer a wiki.68 - `over-cap` and `unreadable` files.6970 Add the judgment the script can't make:71 - **Remediation suggestions.** For each over-cap file, suggest what its diagnosis calls for — distill (bloat) or reshape the space (siblings under a hub, a promotion, a regrouped layout), per `ws-update`'s overflow procedure; an over-cap `log.md` takes the archive roll instead (core block).72 - **Link integrity** — content, so yours: sweep the link shapes (`grep -F '[[' …`, `grep -F '](' …`), judge each hit against the `files` inventory and the disk, and report dead ones with the nearest-named page as the likely target — a `_template.md` body is exempt, its placeholder links being examples, not dead links. On a large wiki, sweep the spaces this pass touches and say what you skipped.73 - **Frontmatter completeness** — only on pages that already carry frontmatter, only when the wiki uses a schema; mixed adoption is allowed.74 - **Structural prompts** — a page pressing its cap (`check-size` any page you suspect) with several distinct H2 sections is outgrowing its shape and is reshape-ready; a space whose index lists many accreted siblings wants a sub-space; a long-grown opt-in `hot.md` wants distilling into cold pages; a space holding one small page and no sub-spaces reads as over-structure — suggest demoting it to a page (`ws-update`'s restructure reference).756. **Structural repair (close-out).** Apply the safe structural repairs yourself, one edit each, re-auditing between rounds until the structural findings are gone — each re-audit re-derives findings from disk, so the rounds converge:76 <!-- ws:safe-repairs -->77 - Add the exact entry line a `missing entry` finding prints, then trail it with a `— description` — the placement hint every later operation reads.78 - Insert the `## Spaces` heading a *registered* bare child lacks — only where the finding's hint asks for the insert; a near-miss hint asks for a rename, which stays reported as author intent.79 - Remove an entry the audit says crosses another space's boundary — that space owns the deeper listing; register it there instead when it is missing.80 <!-- /ws:safe-repairs -->8182 Everything else is author intent — malformed or duplicate entries, unregistrable child names, entries naming files, near-miss headings, dead second headings, undeclared bare indexes, stale entries, unhealthy mounts, over-cap or unreadable files, and any dead links your own sweep surfaced — and stays reported for the user (offer to fix as a normal edit if the user says go; an undeclared bare index is a promotion decision).837. **Normalize: tags.** Only when `_meta/taxonomy.md` exists at the scope root, and only over the scope's own pages — children spaces keep their own taxonomies; leave them out.84 - Sweep the inventory with `grep '^tags:' --wiki <root>`; read the frontmatter of any page the sweep leaves ambiguous — YAML shapes are yours to judge.85 - Untagged pages are `files` minus the sweep's pages, less the frontmatter-exempt special files: indexes, `log.md`, `hot.md`, `_template.md`.86 - Flag non-canonical tags, over-tagged (>5) pages, and untagged pages; apply the alias mappings the taxonomy defines.87 - An unknown tag on 2+ pages: suggest adding it to the taxonomy. A one-off: suggest the closest canonical tag.888. **Normalize: cross-links.** Build a registry of scope-own pages (names, titles, frontmatter aliases when present); the special files (indexes, `log.md`, `hot.md`, `_template.md`) stay out of both the registry and the scan — navigation, history, scratch, and boilerplate neither give nor take links. Scan bodies for unlinked first mentions of registry entries and wrap the strongest matches as wikilinks — at most 2 new links per page, never inside code blocks or frontmatter, no self-links. Prefer inline linking; a trailing `## Related` section is the fallback when no natural mention exists. On a large wiki, scan the spaces this pass touches and say what you skipped.899. **Log** one `TEND` line per the core block when `log.md` exists, and close with the report.9091## Output9293Per mode: a status table; an audit report quoting the script's findings plus your suggestions; normalize tables (tag → action, link added → where). End the full sweep with the one-line delta: `issues before → after; left for you: <n>`.