# Setup

> One-time onboarding - probes the machine, surveys the documents, grills the user, writes config.yaml, anchors the wiki in the instructions file, runs the first pass over the inboxes. Use when the workspace has no config.yaml yet, or the user asks to set up wikidoc.

- Skill: `smkg75/setup` (Agent Skill)
- Install (CLI): `npx skillmds@latest add smkg75/setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/smkg75/setup/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: smkg75 (https://skillmd.com/u/smkg75)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/smkg75/setup

---


# Setup

Run once in the life of an installation.

The output: a `config.yaml` filled from evidence found in the user's own
documents, instruction files that point at the wiki instead of duplicating it,
and a first real pass already run over every configured inbox. Work in
`$WIKIDOC_HOME` (default `~/.wikidoc`); create it if missing. Scripts live in
`scripts/` at the plugin root (`scripts/` sits beside `skills/`, two levels up
from this file); invoke them by absolute path.

## 1. PROBE

Find out what this machine can do by trying it, not by assuming from the
platform name.

```bash
python3 -VV
python3 -c "import importlib.util as u;print({m:bool(u.find_spec(m)) for m in ('yaml','pypdf','pypdfium2','send2trash','striprtf')})"
```

`yaml` is required (`python3 -m pip install --user pyyaml`; on a PEP 668 Python
add `--break-system-packages`, or make a venv and use its interpreter). The
others are optional: without the PDF libraries, PDFs go through vision; without
`send2trash`, removal falls back to a workspace folder. A slower pass, not a
wrong one.

Then the filesystem, in the user's document root. **Count the cloud
placeholders FIRST** — the other probes create empty files, and empty files are
exactly what that count looks for:

- **cloud placeholders** — `.icloud` files or zero-byte stand-ins: files that
  are not materialised have no bytes to classify.
- **xattr round-trip** — write an extended attribute on a scratch file, read it
  back. Failure means `memory.jsonl` is the only record, which is fine.
- **case sensitivity** — create `.probe`, then try `.PROBE`. One file means the
  volume folds case, and two names differing only by case are one document.

Done when each capability is a yes or a no you have observed, and the scratch
files are gone.

## 2. SURVEY

Three Explore agents in parallel, one zone each: **Desktop**, **Downloads**,
**Documents**. Each reports: volume (files, bytes), the document families that
repeat (same issuer, monthly), folders that look like inboxes rather than
homes, and every identifier pattern seen — company numbers, account numbers,
tax ids — with rough counts. Nothing is moved, nothing is written.

Done when you can name the top identifiers with their counts, the top folders
with their file counts, and the repeating families, for all three zones.

## 3. GRILL

Interview the user, every question anchored in a count — a question carrying
its own evidence gets an answer; an abstract one gets a shrug.

> One company number appears in 47 files — which company is that, and where do
> its documents live?
> 212 files sit on the Desktop — an inbox to empty, or a workspace to leave
> alone?
> Payslips appear under two employer names — same employer, or two?

Write `config.yaml` EARLY — after the first answers, not at the end — and
re-grill from it: read it back block by block; every block the user corrects is
a misunderstanding you almost shipped. Note context into `wiki/context.md` as
it comes — who is who, which entity was live when — not into your own head.

Cover: the entities and how each is recognised in text (most people have one or
two and no company; an entity with no identifier is normal — it matches on
text); what counts as sensitive here; what must never be touched; the tag
taxonomy; the corpus language.

Done when every open question is answered, or dated in `wiki/context.md`.

## 4. LAYOUT

The target tree — where documents will live. Offer three named options, then a
free choice:

- **by-entity** — one top folder per entity, life domains inside
  (`Personal/Banking/`, `AcmeCorp/Accounting/`).
- **by-domain** — one top folder per domain, entities inside
  (`Banking/Personal/`, `Banking/AcmeCorp/`).
- **flat-years** — shallow domains with years inside (`Taxes/2026/`), for a
  small corpus.
- **your own** — the user sketches it; restate it as a tree and have them
  confirm.

Then the inboxes, defaults on the table: **Downloads empty** (policy `empty` —
everything files out), **Desktop transit** (policy `transit` — work in progress
tolerated, residue proposed each pass), everything under Documents by entity.
Store each answer in `inboxes:` — an inbox file is always proposed, never
silently routed. An inbox **outside** `root` (Desktop and Downloads normally
are) is walked as a root of its own, and its files are keyed `~/Desktop/…` in
memory; nothing has to sit under `root` to be swept.

Done when the tree is in config `layout:` and every inbox has a policy the user
chose.

## 5. WRITE

Finish `config.yaml` with `config.example.yaml` as the shape — read it, do not
copy it; the comments are part of the contract. Each identifier from SURVEY
gets a pattern and a `validate:` (`luhn`, `iban`, or `none`) — a candidate that
fails its check is not an id. `anchors:` is set only when the instruction file
lives somewhere other than `~/.claude/CLAUDE.md`, the default the pointer check
reads (§6 says why that file). Seed rules from the SURVEY families, each
born `status: shadow`, counters at 0, `learned_from` filled — evaluated every
pass, never applied until the user promotes them. Create empty `memory.jsonl`;
complete `wiki/context.md`.

Then make the workspace a git repository: `git init` at its root, local, with
no remote — it holds private facts and stays on this machine. `.gitignore`:
`logs/`, `cache/`, `bench/`, `.trash/`, `.DS_Store`, `*.bak*`. First commit: `setup`.
From here on the repository is the backup: every pass and every tidy commits
before and after itself, so a change to `config.yaml`, to the ledger or to the
wiki is always one `git show` away, and one `git revert` from undone.

Done when `config.yaml` parses, `memory.py stats` prints an empty memory,
everyone the user named has an entry under `entities:`, and `git log` shows
the `setup` commit with no remote configured.

## 6. ANCHOR

The anchor lives in the instructions file loaded in **every** session —
`~/.claude/CLAUDE.md` for Claude Code — not in a file under the document root
nor in a one-line `@import` stub on the Desktop. A per-folder file only loads
in sessions started there, and the write rule below is exactly the one that
must hold everywhere: facts surface in sessions about something else. Merge
any per-folder instruction file into the global one, remove the stubs, and
leave `anchors:` unset — the check reads the global file by default.

Audit the user's instruction files (CLAUDE.md, AGENTS.md…). Two kinds of lines:

- **Process lines stay.** A hard rule gating an irreversible gesture —
  "removal = OS bin only, never rm" — MUST stay in the instructions file: it
  holds in sessions that never open the wiki.
- **Corpus facts move** to `wiki/context.md` — who is who, which folder holds
  what, entity histories. Each moved fact is replaced by a CONDITIONAL pointer,
  "situation → path", never a bare link:

> Filing or searching personal documents → read `~/.wikidoc/wiki/context.md`.
> A question about an entity's history → `~/.wikidoc/wiki/context.md`.

Pointers alone make the wiki read-only, and a corpus that is only ever read
goes stale. So the anchor also carries **one write rule**, and it says that the
wiki is written **in any session, without invoking this skill**: a durable fact
established by digging — an identifier, a surface, a correspondent, a
reference amount, an arbitration — is written to `wiki/` the moment it
surfaces, into `context.md` (durable facts), `decisions.md` (dated
arbitrations), `filing-patterns.md` / `trash-criteria.md` (observed
destinations and removal criteria), `log.md` (dated events, appended) or
`state.md` (the present: one line per dossier in flight, what stayed
unanswered),
then its line in `wiki/index.md` is updated — a fact absent from the index is
invisible. Writing is surgical: complete the existing section, never rewrite
the file; contradicting sources are recorded WITH the contradiction and what
settles it.

What the rule must reserve to an invoked pass is the pass itself — collect /
route / apply over files — and `memory.jsonl`, which records gestures on
files only. Writing to the wiki opens no pass and appends no memory line;
`state.md`'s open questions are asked at the start of a filing session, not in
a session about something else.

Back up the original to `<workspace>/legacy/` before touching it. Mechanically
check every pointer — each backticked path must resolve. Show the user the full
diff before writing anything.

Done when one global instructions file holds process and pointers only, the
backup exists, every pointer resolves, and the user approved the diff.

## 7. FIRST SWEEP

A real first pass, scoped to the inboxes by the selection order itself: inbox
files select ahead of the rest — so run `collect.py N` with N = the SUM of the
file counts of ALL the inboxes in `inboxes:`, not just Desktop + Downloads.
GRILL routinely surfaces a third inbox, and a sweep sized on two of them
starves the rest in silence; collect.py prints selected/remaining counts per
inbox — check that no inbox was left behind before going on. Full machinery
of `skills/tri/SKILL.md`, nothing weakened: Vision on every unread scan, Route, Decide with the user
answering blocking questions as they arise, the Apply dry-run read together,
then `--execute`.

The honest target: an `empty`-policy inbox (Downloads) emptied or nearly so; a
`transit` inbox (Desktop) reduced to its legitimate work in progress — a
Desktop full of live WIP will not visibly empty, and promising that is a
promise the pass cannot keep. Whatever remains is named, never waved at.

End on a **bilan**: what moved where (counts plus a few sample paths), and the
residues BY NAME — each one recorded `unanswered`, so the next pass selects it
first and opens with its question.

Then tell the user, explicitly: continue in this same conversation, or start a
fresh one — both work, `memory.jsonl` carries the state.

Done when the pass is archived under `logs/`, every inbox's remaining count is
one you can explain, and every residue is named in the bilan and recorded
`unanswered`.

