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## Spacescontract, each with its entry description (--externalto cross mounts).… files --wiki <root>— markdown files reachable via the contract.… grep <pattern> [-i] [-F] --wiki <root>— regex line search over those files,-Ffor a literal string (a name carrying metacharacters sweeps exact); printsrel: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--stdinto 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 (amissing entryprints 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, andcheck-sizereads them. - Check caps with
check-size: pipe the planned content via--stdinbefore every write, new page or edit — a write that shrinks an over-cap file reportsok, 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>.mdand 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.mdfollow 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+filesfor the shape (add--externalonly 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 agrep '^tags:'sweep, when frontmatter is in use), the lastlog.mdline (when present), and git status (when.gitexists — read-only; never commit or push). -
Audit. Run
auditand 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-capandunreadablefiles.
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-caplog.mdtakes the archive roll instead (core block). - Link integrity — content, so yours: sweep the link shapes (
grep -F '[[' …,grep -F '](' …), judge each hit against thefilesinventory and the disk, and report dead ones with the nearest-named page as the likely target — a_template.mdbody 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-sizeany 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-inhot.mdwants 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 entryfinding prints, then trail it with a— description— the placement hint every later operation reads. - Insert the
## Spacesheading 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).
- Add the exact entry line a
-
Normalize: tags. Only when
_meta/taxonomy.mdexists 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
filesminus 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.
- Sweep the inventory with
-
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## Relatedsection 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
TENDline per the core block whenlog.mdexists, 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>.