codealmanac 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,686 @@
1
+ # codealmanac — full reference
2
+
3
+ Long-form manual for the `almanac` / `codealmanac` CLI. The mini guide at `~/.claude/codealmanac.md` covers *when* to reach for each command; this covers *every flag, every return shape, every edge case*. Import with `@~/.claude/codealmanac-reference.md` on demand.
4
+
5
+ Groupings match `almanac --help`:
6
+
7
+ 1. **Query** — `search`, `show`, `health`, `list`
8
+ 2. **Edit** — `tag`, `untag`, `topics ...`
9
+ 3. **Wiki lifecycle** — `bootstrap`, `capture`, `hook ...`, `reindex`
10
+ 4. **Setup** — `setup`, `uninstall`, `doctor`
11
+
12
+ Every query/edit command auto-registers the current repo in `~/.almanac/registry.json` on first run. Exceptions: `list --drop` (skips auto-register so the removal intent isn't undone) and the setup group (installers, not wiki commands — they never touch the registry).
13
+
14
+ There is no `almanac init` command. The two ways a wiki gets scaffolded are `almanac bootstrap` (agent reads the repo and seeds stub pages) and committing a `.almanac/` that someone else authored and cloning into it (auto-registered on first query command).
15
+
16
+ ---
17
+
18
+ ## 1. Full command matrix
19
+
20
+ ### 1.1 Query
21
+
22
+ #### `almanac search [query]`
23
+
24
+ | Flag | Type | Default | Semantics |
25
+ |---|---|---|---|
26
+ | `[query]` | string | — | FTS5 MATCH against titles + bodies. Omit for pure-filter queries. |
27
+ | `--topic <name...>` | repeatable | `[]` | AND-intersect filter. Walks the DAG subtree — `--topic auth` matches `auth` or any descendant. |
28
+ | `--mentions <path>` | string | — | Pages referencing this path. Matches exact file, trailing-slash folders, and any file under a folder prefix. Case-insensitive. |
29
+ | `--since <duration>` | duration | — | Updated within window. Format: `<int>[smhdw]` (`2w`, `30d`, `48h`). By file mtime. |
30
+ | `--stale <duration>` | duration | — | Inverse of `--since`. |
31
+ | `--orphan` | bool | false | Pages with zero topics. |
32
+ | `--include-archive` | bool | false | Include archived pages. |
33
+ | `--archived` | bool | false | Archived pages only. |
34
+ | `--wiki <name>` | string | current repo | Target a specific registered wiki. |
35
+ | `--json` | bool | false | Structured JSON. |
36
+ | `--limit <n>` | int ≥0 | unbounded | Cap results. |
37
+
38
+ **Default output:** one slug per line to stdout. When zero pages match, stdout is empty and stderr emits `# 0 results` (a breadcrumb so users can tell "matched nothing" apart from "command broken"). `--json` is silent on stderr — `[]` is the unambiguous empty signal there.
39
+ **`--json` schema:** JSON array of `{slug, title, updated_at, topics, path}`.
40
+ **Exit:** `0` always (empty result isn't an error). Arg-parse failures exit `1` with an `almanac:` error.
41
+
42
+ #### `almanac show [slug]`
43
+
44
+ Unified reader. Absorbs the old `info` and `path` commands — pick fields with flags.
45
+
46
+ | Flag | Default | Semantics |
47
+ |---|---|---|
48
+ | `[slug]` | — | Required unless `--stdin`. Slugs are kebab-canonicalized before lookup. |
49
+ | `--stdin` | false | Read slugs from stdin, one per line. JSON Lines output for `--json` mode. |
50
+ | `--wiki <name>` | current repo | Target a specific registered wiki. |
51
+ | `--json` | false | Structured JSON. Overrides every view/field flag. |
52
+ | `--raw` / `--body` | false | Body only (alias pair). Guarantees exactly one trailing newline — shell redirect produces a well-formed file. |
53
+ | `--meta` | false | Metadata header only, no body. |
54
+ | `--lead` | false | First paragraph of the body only (cheap preview). |
55
+ | `--title` | false | Print title. |
56
+ | `--topics` | false | Print topics. |
57
+ | `--files` | false | Print file refs. |
58
+ | `--links` | false | Print outgoing wikilinks. |
59
+ | `--backlinks` | false | Print incoming wikilinks. |
60
+ | `--xwiki` | false | Print cross-wiki links. |
61
+ | `--lineage` | false | Print `archived_at` / `supersedes` / `superseded_by`. |
62
+ | `--updated` | false | Print updated timestamp. |
63
+ | `--path` | false | Print absolute file path (`info` + `path` replacement). |
64
+
65
+ Combining field flags emits labeled sections in canonical order. `--meta` is the full labeled header; individual flags like `--title --topics` give you just those two sections.
66
+
67
+ **Exit:** `0` on success, `1` if slug not found, non-zero on flag/input errors.
68
+
69
+ #### `almanac health`
70
+
71
+ Eight independent categories. One failing doesn't skip the others.
72
+
73
+ | Flag | Default | Semantics |
74
+ |---|---|---|
75
+ | `--topic <name>` | — | Scope page-level checks to the topic + descendants. Scopes topic-level checks to the subtree. |
76
+ | `--stale <duration>` | `90d` | Threshold for the `stale` category. |
77
+ | `--stdin` | false | Restrict page-level checks to slugs from stdin. Intersects with `--topic`. |
78
+ | `--json` | false | Structured JSON. |
79
+ | `--wiki <name>` | current repo | Target a specific registered wiki. |
80
+
81
+ **Categories:** `orphans`, `stale`, `dead-refs`, `broken-links`, `broken-xwiki`, `empty-topics`, `empty-pages`, `slug-collisions`. Archived pages are exempt from most (see §4). Exit `0` always — the report IS the output.
82
+
83
+ #### `almanac list`
84
+
85
+ | Flag | Semantics |
86
+ |---|---|
87
+ | `--json` | Structured JSON. |
88
+ | `--drop <name>` | Remove a wiki from the registry. The **only** way entries are ever removed. Skips auto-register. |
89
+
90
+ ### 1.2 Edit
91
+
92
+ #### `almanac tag [page] [topics...]`
93
+
94
+ Add topics to a page. Auto-creates missing topics. Idempotent. Rewrites only the frontmatter block; body bytes preserved. `--stdin` tags every page-slug from stdin with the same topic set — in that mode all positionals are topics.
95
+
96
+ Flags: `--stdin`, `--wiki <name>`.
97
+
98
+ #### `almanac untag <page> <topic>`
99
+
100
+ Remove one topic. Idempotent (silent `0` if page wasn't tagged).
101
+
102
+ Flags: `--wiki <name>`.
103
+
104
+ #### `almanac topics` (DAG management)
105
+
106
+ - `almanac topics list` — list all topics with page counts. `--json` emits an array of `{slug, description, parents[], children[], page_count}`.
107
+ - `almanac topics show <slug>` — description, parents, children, pages. `--descendants` includes pages tagged with descendant topics (walks the DAG subtree).
108
+ - `almanac topics create <name>` — `--parent <slug>` repeatable. Rejects if any parent slug doesn't exist.
109
+ - `almanac topics link <child> <parent>` / `almanac topics unlink <child> <parent>` — add/remove a DAG edge. `link` is cycle-checked (§5). `unlink` is idempotent.
110
+ - `almanac topics rename <old> <new>` — rewrites `topics.yaml` first (atomic tmp+rename), then every affected page's `topics:` frontmatter. YAML-first so a mid-pass crash leaves the graph, not the pages, as the source of truth.
111
+ - `almanac topics delete <slug>` — removes from `topics.yaml`, untags every affected page. Does **not** cascade to children — orphaned children become top-level. Run `almanac health` to surface stragglers.
112
+ - `almanac topics describe <slug> <text>` — set the topic's one-line description.
113
+
114
+ All topic subcommands accept `--wiki <name>`. `list` / `show` accept `--json`.
115
+
116
+ ### 1.3 Wiki lifecycle
117
+
118
+ #### `almanac bootstrap`
119
+
120
+ Spawns an agent to create initial wiki stubs. Requires `ANTHROPIC_API_KEY` or a logged-in Claude subscription. `--quiet` suppresses per-tool streaming. `--model <model>` overrides the model. `--force` overwrites an existing populated wiki. Writes `.almanac/.bootstrap-<timestamp>.log`.
121
+
122
+ Bootstrap is the scaffolding path — it creates `.almanac/pages/`, `.almanac/topics.yaml`, `.almanac/README.md`, and stub entity pages based on what the agent reads in the repo.
123
+
124
+ #### `almanac capture [transcript]`
125
+
126
+ Run the writer/reviewer pipeline on a Claude Code session transcript. Usually automatic — the `SessionEnd` hook invokes this. Refuses if no `.almanac/` exists in cwd or any parent (capture maintains wikis, doesn't create them; run `almanac bootstrap` first).
127
+
128
+ | Flag | Semantics |
129
+ |---|---|
130
+ | `[transcript]` | Explicit path. Falls back to `--session` match or most-recent-by-cwd. |
131
+ | `--session <id>` | Target a specific session by ID. Matches filename under `~/.claude/projects/`. |
132
+ | `--quiet` | Suppress per-tool streaming; print only the final summary. |
133
+ | `--model <model>` | Override the agent model. |
134
+
135
+ Writes SDK transcript to `.almanac/.capture-<session-id>.log`. A writer subagent drafts pages; a reviewer subagent enforces notability + writing conventions (§9) before drafts land.
136
+
137
+ #### `almanac hook install | uninstall | status`
138
+
139
+ See §7 — the hook is complex enough to warrant its own section.
140
+
141
+ #### `almanac reindex`
142
+
143
+ Forces a full rebuild of `.almanac/index.db`. Rarely needed — every query calls `ensureFreshIndex` first. Use after manual `topics.yaml` edits or when clock skew defeats mtime checks.
144
+
145
+ Flag: `--wiki <name>`.
146
+
147
+ ### 1.4 Setup
148
+
149
+ #### `almanac setup` (alias: bare `codealmanac`)
150
+
151
+ Install the SessionEnd hook + the two CLAUDE.md guides (`codealmanac.md`, `codealmanac-reference.md`) + the `@~/.claude/codealmanac.md` import line. Idempotent.
152
+
153
+ | Flag | Semantics |
154
+ |---|---|
155
+ | `-y, --yes` | Skip prompts; install everything. |
156
+ | `--skip-hook` | Opt out of the SessionEnd hook. |
157
+ | `--skip-guides` | Opt out of the CLAUDE.md guides. |
158
+
159
+ Both `almanac setup` and bare `codealmanac` route here. `codealmanac --yes`, `codealmanac --skip-hook`, and `codealmanac --skip-guides` are the typical first-run invocations. Passing `--skip-hook --skip-guides` together short-circuits with a terse line — nothing was installed, no banner drawn.
160
+
161
+ #### `almanac uninstall`
162
+
163
+ Remove the hook + guides + import line.
164
+
165
+ | Flag | Semantics |
166
+ |---|---|
167
+ | `-y, --yes` | Skip confirmations; remove everything. |
168
+ | `--keep-hook` | Don't remove the SessionEnd hook (guides still prompted unless `--yes`). |
169
+ | `--keep-guides` | Don't remove the guides or CLAUDE.md import (hook still prompted unless `--yes`). |
170
+
171
+ #### `almanac doctor`
172
+
173
+ Read-only install + current-wiki health report. Every check reports a state; none of them mutate. Exit always `0` — doctor is a report, not a test.
174
+
175
+ | Flag | Semantics |
176
+ |---|---|
177
+ | `--json` | Structured JSON. |
178
+ | `--install-only` | Report only on the install (skip the wiki section). |
179
+ | `--wiki-only` | Report only on the current wiki (skip the install section). |
180
+
181
+ **JSON shape:**
182
+ ```json
183
+ {
184
+ "version": "0.1.3",
185
+ "install": [
186
+ { "key": "install.path", "status": "ok", "message": "..." },
187
+ { "key": "install.sqlite", "status": "ok", "message": "..." },
188
+ { "key": "install.auth", "status": "problem", "message": "...", "fix": "run: claude auth login --claudeai" },
189
+ { "key": "install.hook", "status": "ok", "message": "..." },
190
+ { "key": "install.guides", "status": "ok", "message": "..." },
191
+ { "key": "install.import", "status": "ok", "message": "..." }
192
+ ],
193
+ "wiki": [
194
+ { "key": "wiki.repo", "status": "info", "message": "repo: /abs/path" },
195
+ { "key": "wiki.registered", "status": "ok", "message": "registered as '...'" },
196
+ { "key": "wiki.pages", "status": "info", "message": "pages: 42" },
197
+ { "key": "wiki.topics", "status": "info", "message": "topics: 7" },
198
+ { "key": "wiki.index", "status": "info", "message": "index: rebuilt 2m ago" },
199
+ { "key": "wiki.capture", "status": "info", "message": "last capture: 1h ago (.capture-<id>.log)" },
200
+ { "key": "wiki.health", "status": "ok", "message": "almanac health reports 0 problems" }
201
+ ]
202
+ }
203
+ ```
204
+
205
+ Each check has a stable `key` safe for scripting. ✗ entries include a `fix` field with a one-line "run: …" hint. Parse `--json` and count `status === "problem"` for a pass/fail gate.
206
+
207
+ ### 1.5 `--stdin` pipe semantics
208
+
209
+ Commands that accept `--stdin`: `show`, `tag`, `health`.
210
+
211
+ - One slug per line; blank lines ignored; whitespace trimmed.
212
+ - Output order mirrors input order.
213
+ - Missing slugs don't abort — logged to stderr, pipeline continues. `show --stdin` writes a "not found" marker per slug and keeps exit `0` for pipeline resilience.
214
+ - `--stdin` must be explicit. No `isTTY` auto-detection (confusing under script redirection).
215
+
216
+ ---
217
+
218
+ ## 2. The unified `[[...]]` classifier
219
+
220
+ One syntax, four kinds. Rules applied in order:
221
+
222
+ 1. **`:` before any `/`** → cross-wiki (`[[wiki:slug]]`)
223
+ 2. **Contains `/`** → file (no trailing `/`) or folder (trailing `/`)
224
+ 3. **Otherwise** → page slug wikilink
225
+
226
+ | Input | Classified | Why |
227
+ |---|---|---|
228
+ | `[[a:b/c]]` | xwiki `a`→`b/c` | colon before slash, rule 1 |
229
+ | `[[src/a:b]]` | file `src/a:b` | slash before colon, rule 2 |
230
+ | `[[./x]]` | file `x` | normalized; `./` stripped |
231
+ | `[[src/checkout/]]` | folder | trailing `/` |
232
+ | `[[foo\|display]]` | page `foo` | Obsidian pipe stripped |
233
+ | `[[ ]]` | null | empty after trim |
234
+
235
+ **Paths with spaces** are allowed. **GLOB metacharacters** like `[id]`, `[...slug]`, `{a,b}`, `*` are stored literally — they're Next.js-style dynamic segments, not glob expressions.
236
+
237
+ **Case sensitivity:** the indexer stores two forms per file/folder ref:
238
+ - `path` — lowercased, used for `--mentions` lookups (search is case-insensitive).
239
+ - `original_path` — as-written, used for filesystem `stat` in `health dead-refs` so case-sensitive filesystems (Linux, some Docker images) don't false-negative.
240
+
241
+ **Broken links** are recorded anyway (`wikilinks` table keeps the row), then surfaced by `health --broken-links`. Reindex is non-validating by design.
242
+
243
+ Cross-wiki refs live in their own table (`cross_wiki_links`), never lowercased.
244
+
245
+ ---
246
+
247
+ ## 3. Frontmatter schema
248
+
249
+ | Field | Type | Default | Purpose |
250
+ |---|---|---|---|
251
+ | `title` | string | H1 fallback | Display title. Missing → first H1 in body. |
252
+ | `topics` | string[] | `[]` | DAG tags. Kebab-cased on ingest; duplicates collapsed. |
253
+ | `files` | string[] | `[]` | File/folder paths this page documents. Load-bearing for `--mentions`. Trailing `/` = folder. |
254
+ | `archived_at` | date / ISO string / epoch seconds | `null` | Non-null → excluded from default search. See §4. |
255
+ | `superseded_by` | slug | `null` | For archived pages: the active replacement. |
256
+ | `supersedes` | slug | `null` | For active pages: the archived predecessor. |
257
+
258
+ **Normalization:** YAML `Date` → epoch seconds; ISO string → `Date.parse`; raw number → `Math.floor`. Unrecognizable `archived_at` → `null` (page stays active; safer default). Unknown frontmatter fields tolerated silently. Malformed YAML → one-line stderr warning, treated as no frontmatter; the reindex keeps going.
259
+
260
+ **Full example:**
261
+
262
+ ```markdown
263
+ ---
264
+ title: Checkout flow
265
+ topics: [flows, payments]
266
+ files:
267
+ - src/checkout/handler.ts
268
+ - src/checkout/
269
+ - docker-compose.yml
270
+ archived_at: null
271
+ ---
272
+
273
+ # Checkout flow
274
+
275
+ The flow starts at `src/checkout/handler.ts` when the browser POSTs
276
+ `/api/cart/submit`. The handler creates a Stripe PaymentIntent, writes an
277
+ inventory lock row to Supabase, returns the PI client secret. See
278
+ [[inventory-lock-gotcha]] for the deadlock we hit in March.
279
+ ```
280
+
281
+ CRLF-terminated files are handled transparently — `show --raw` strips frontmatter without leaving a stray `\r` at the body head.
282
+
283
+ ---
284
+
285
+ ## 4. Archive / lineage
286
+
287
+ Pages evolve in place. **Edit the existing page** when facts change — git history is the archive.
288
+
289
+ **Archive + supersede** is reserved for **fundamental reversals**: a central decision overturned, a system replaced wholesale, an incident re-opened.
290
+
291
+ **The test:** *is this an update to the old state, or a reversal of a central decision?* Update → edit. Reversal → archive + successor.
292
+
293
+ ### Frontmatter shapes
294
+
295
+ Archived page:
296
+ ```yaml
297
+ ---
298
+ title: JWT sessions (archived)
299
+ topics: [auth, decisions]
300
+ archived_at: 2026-03-15
301
+ superseded_by: server-sessions
302
+ ---
303
+ ```
304
+
305
+ Successor:
306
+ ```yaml
307
+ ---
308
+ title: Server sessions
309
+ topics: [auth, decisions]
310
+ supersedes: jwt-sessions
311
+ files: [src/auth/session.ts, src/auth/middleware.ts]
312
+ ---
313
+ ```
314
+
315
+ Both files exist on disk. Both are indexed.
316
+
317
+ ### Search scoping
318
+
319
+ - Default: active only.
320
+ - `--include-archive`: active + archived.
321
+ - `--archived`: archived only. Useful for retrospectives.
322
+
323
+ ### Health exemptions for archived pages
324
+
325
+ Archived pages (as *source*) are exempt from `orphans`, `stale`, `dead-refs`, `broken-links`, `broken-xwiki`, `empty-pages`. Rationale: a retired page legitimately references retired files, has no need to be "kept fresh," and minimal stubs are fine.
326
+
327
+ Archived pages ARE still valid *targets* of broken-link checks — an active page linking to an archived page is fine (target exists); an active page linking to a slug with no file at all is flagged regardless.
328
+
329
+ ---
330
+
331
+ ## 5. DAG model and traversal
332
+
333
+ Topics form a DAG: each topic has zero or more parents; each page has zero or more topics. Structure in `.almanac/topics.yaml`, assignment in page frontmatter.
334
+
335
+ ```yaml
336
+ # topics.yaml
337
+ topics:
338
+ auth:
339
+ description: authentication, sessions, identity
340
+ parents: []
341
+ jwt:
342
+ parents: [auth]
343
+ sessions:
344
+ parents: [auth]
345
+ checkout:
346
+ parents: [flows, payments] # multi-parent
347
+ ```
348
+
349
+ **`--descendants`** walks the subtree rooted at the given topic. `almanac topics show auth --descendants` includes `auth`, `jwt`, `sessions`, and any page tagged with any of them. `almanac search --topic auth` applies the same walk implicitly.
350
+
351
+ ### Cycle prevention
352
+
353
+ Three layers:
354
+ 1. **CHECK constraint** on `topic_parents` blocks self-loops (`child = parent`).
355
+ 2. **Pre-insert traversal** walks parents upward before committing; refuses if `child` is reachable.
356
+ 3. **Depth cap of 32** bails the traversal defensively. Real topic DAGs are ≤4 deep.
357
+
358
+ `almanac topics link A B` where A is already an ancestor of B fails: `almanac: link would create cycle: A → … → B → A`.
359
+
360
+ ### Rename / delete side effects
361
+
362
+ `topics rename old new`:
363
+ 1. Rewrite `topics.yaml` atomically (tmp + rename). New key written, old removed, parent edges migrated.
364
+ 2. Rewrite every page whose `topics:` contains `old`. Body bytes preserved.
365
+ 3. Reindex fires automatically on `topics.yaml` mtime bump.
366
+
367
+ YAML-first order matters: if pages rewrote first and crashed midway, `topics.yaml` would describe an invalid state. YAML-first gives a clean rollback point.
368
+
369
+ `topics delete slug`:
370
+ 1. Remove from `topics.yaml`.
371
+ 2. Untag every affected page.
372
+ 3. **Does not cascade.** Children of the deleted topic become top-level. Run `almanac health --empty-topics` and re-parent or prune.
373
+
374
+ ---
375
+
376
+ ## 6. Shell-piping cookbook
377
+
378
+ Every command emits slugs one-per-line, so they compose.
379
+
380
+ **Find stale pages in a topic and tag them `review-needed`:**
381
+ ```bash
382
+ almanac search --topic auth --stale 90d \
383
+ | almanac tag --stdin review-needed
384
+ ```
385
+
386
+ **Find pages referencing a just-deleted file:**
387
+ ```bash
388
+ almanac search --mentions src/legacy/oauth.ts --include-archive
389
+ ```
390
+
391
+ **Bulk move pages from an old topic to a new one:**
392
+ ```bash
393
+ almanac topics create payments-v2 --parent payments
394
+ almanac search --topic old-payments | almanac tag --stdin payments-v2
395
+ almanac topics delete old-payments
396
+ ```
397
+
398
+ **List pages that lack `files:` frontmatter for files they mention in prose:**
399
+ ```bash
400
+ almanac search | while read slug; do
401
+ info=$(almanac show "$slug" --json)
402
+ prose=$(echo "$info" | jq -r '.file_refs[].path' | sort -u)
403
+ fm=$(echo "$info" | jq -r '.files[]' | sort -u)
404
+ missing=$(comm -23 <(echo "$prose") <(echo "$fm"))
405
+ [ -n "$missing" ] && { echo "$slug:"; echo "$missing" | sed 's/^/ /'; }
406
+ done
407
+ ```
408
+
409
+ **Open every orphan page in `$EDITOR`:**
410
+ ```bash
411
+ almanac search --orphan | almanac show --stdin --path | xargs -n 1 "$EDITOR"
412
+ ```
413
+
414
+ **Export a page's body to a standalone markdown file:**
415
+ ```bash
416
+ almanac show checkout-flow --raw > checkout-flow.md # exactly one trailing \n
417
+ ```
418
+
419
+ **Doctor a flaky install in CI:**
420
+ ```bash
421
+ almanac doctor --json | jq '.install[] | select(.status == "problem")'
422
+ ```
423
+
424
+ ---
425
+
426
+ ## 7. The capture hook
427
+
428
+ ### Trigger
429
+
430
+ Claude Code invokes `SessionEnd` hooks after each session. Payload on stdin:
431
+ ```json
432
+ { "session_id": "uuid", "transcript_path": "/abs/path.jsonl", "cwd": "/abs/repo/path" }
433
+ ```
434
+
435
+ ### What `hooks/almanac-capture.sh` does
436
+
437
+ 1. Parse payload with `jq`. Missing `jq` → exit 0 silently.
438
+ 2. Walk upward from `cwd` for a `.almanac/`. Bounded at filesystem root.
439
+ 3. Background `almanac capture "$TRANSCRIPT" --session "$SESSION_ID" --quiet`, redirect to `.almanac/.capture-$SESSION_ID.log`, `disown`.
440
+ 4. Exit always `0`. Capture failures must never break Claude Code's session-end path.
441
+
442
+ Falls back to `npx --no-install codealmanac` if `almanac` isn't on `PATH`.
443
+
444
+ ### `hook install | uninstall | status`
445
+
446
+ **`install`:**
447
+ - **Idempotent.** Twice → one entry, not two.
448
+ - **Refuses foreign `SessionEnd` entries** whose command doesn't end with `almanac-capture.sh`. Prints them, exits `1`. Users wire their own hooks (notifications, autocommit); we don't clobber.
449
+ - **Replaces stale almanac entries** — same filename, different absolute path (old install in a different `node_modules`).
450
+ - **Atomic** tmp + rename. Claude Code never sees a partial `settings.json`.
451
+
452
+ **`uninstall`:**
453
+ - Removes only entries whose command ends in `almanac-capture.sh`. Foreign entries stay.
454
+ - Drops `hooks.SessionEnd` if empty, then `hooks` if empty. File returns to pre-install shape.
455
+
456
+ **`status`:**
457
+ - Reports installed / not installed, the script path, the settings path. Non-interactive.
458
+
459
+ `almanac setup` wraps `hook install` alongside the guides. `almanac uninstall` wraps `hook uninstall` alongside guide removal. You rarely invoke `hook *` directly.
460
+
461
+ ### Diagnosing "capture didn't run"
462
+
463
+ ```bash
464
+ almanac doctor # catch-all — reports hook state + last capture age
465
+ almanac hook status # just the hook entry
466
+ ls -lah .almanac/.capture-*.log
467
+ ```
468
+
469
+ Installed but no log: `SessionEnd` didn't fire (rare, hard crash), or script bailed before backgrounding (add `set -x` to trace), or no `.almanac/` upward from `cwd` (silent correct no-op).
470
+
471
+ ### Diagnosing "capture ran but wrote nothing"
472
+
473
+ ```bash
474
+ tail -200 .almanac/.capture-<id>.log
475
+ ```
476
+
477
+ Common causes:
478
+ - `ANTHROPIC_API_KEY` not in the hook's environment. Claude Code's hook env is minimal; `~/.zshrc` is NOT sourced. Export via `~/.claude/settings.json`'s `env` key, or rely on `claude auth` OAuth credentials.
479
+ - Transcript path didn't resolve. Capture prints resolution status early.
480
+ - Reviewer rejected the draft for notability — rationale is in the log.
481
+ - Session was pure-read with no decisions or discoveries. Correct no-op.
482
+
483
+ ---
484
+
485
+ ## 8. Multi-wiki model
486
+
487
+ ### Registry at `~/.almanac/registry.json`
488
+
489
+ ```json
490
+ {
491
+ "wikis": [
492
+ { "name": "openalmanac", "path": "/Users/me/code/openalmanac", "description": "…" },
493
+ { "name": "codealmanac", "path": "/Users/me/code/codealmanac" }
494
+ ]
495
+ }
496
+ ```
497
+
498
+ ### Registration paths
499
+
500
+ - **Silent auto-register** — every query/edit command (except `list --drop`) calls `autoRegisterIfNeeded` on cwd. A repo with `.almanac/` but no registry entry → added with `name = basename(cwd)`, no description. Makes "cloned a repo with `.almanac/` committed" just work.
501
+ - **`almanac bootstrap`** — auto-registers as a side effect of scaffolding. `name` defaults to the repo basename; edit `~/.almanac/registry.json` or re-bootstrap to rename.
502
+ - **`almanac list --drop <name>`** — the only removal path. Skips auto-register so the removal isn't immediately undone.
503
+
504
+ ### `--wiki <name>`
505
+
506
+ Route the command at a specific registered wiki. Used when you're in one repo but querying another. Without `--wiki`, commands resolve to the wiki whose `path` is an ancestor of cwd. If none, commands error: `almanac: no .almanac/ found in this directory or any parent; run 'almanac bootstrap' first`.
507
+
508
+ ### Cross-wiki link resolution
509
+
510
+ `[[wiki:slug]]` → `{kind: "xwiki", wiki, target}` → row in `cross_wiki_links`. `health --broken-xwiki` checks: is `wiki` in the registry and does its `path` contain `.almanac/`? Currently does NOT descend into the target wiki's index to confirm the slug exists — deferred.
511
+
512
+ ### Unreachable targets
513
+
514
+ - Searches silently skip.
515
+ - `health --broken-xwiki` reports them.
516
+ - `show --wiki unreachable` exits `1` with a diagnostic.
517
+
518
+ ---
519
+
520
+ ## 9. Notability and writing conventions
521
+
522
+ The reviewer subagent enforces these during capture. Applying them yourself reduces rework.
523
+
524
+ ### Patterns to avoid (bad → good)
525
+
526
+ **Significance inflation.**
527
+ - Bad: `The Stripe integration serves as a testament to our commitment to payment reliability.`
528
+ - Good: `The Stripe integration handles card payments. PaymentIntent is created at cart-submit; webhook confirmation completes the order.`
529
+
530
+ **Interpretive -ing clauses.**
531
+ - Bad: `The team migrated to async webhooks, highlighting their pragmatic approach.`
532
+ - Good: `The team migrated to async webhooks in March 2026 after the inventory-lock deadlock.`
533
+
534
+ **Vague attribution.**
535
+ - Bad: `Experts argue JWTs are unsuitable for sessions.`
536
+ - Good: `We moved off JWTs to server sessions in 2025 because refresh-token rotation required server state anyway.`
537
+
538
+ **Promotional language.**
539
+ - Bad: `Our groundbreaking approach delivers vibrant performance.`
540
+ - Good: `Rate limiting: sliding-window counter in Redis, 100 req / user / minute, in src/middleware/rate-limit.ts.`
541
+
542
+ **Hedging.**
543
+ - Bad: `While details are limited, it appears the cache might use LRU eviction.`
544
+ - Good: confirm from code, or cut the sentence.
545
+
546
+ **Empty evaluative sentences.** `This architecture is elegant and powerful.` → cut.
547
+
548
+ **Formulaic conclusions.** `In conclusion, the checkout flow demonstrates careful engineering.` → cut. Pages don't need conclusions.
549
+
550
+ ### The four page shapes
551
+
552
+ **Entity** (a thing we depend on):
553
+ ```yaml
554
+ ---
555
+ title: Supabase
556
+ topics: [stack, database]
557
+ files: [src/lib/supabase.ts, backend/src/models/, docker-compose.yml]
558
+ ---
559
+
560
+ # Supabase
561
+
562
+ PostgreSQL hosted on Supabase. Connection pooling via Supavisor. Client
563
+ singleton in src/lib/supabase.ts; backend models in backend/src/models/
564
+ use SQLAlchemy against the same DATABASE_URL (Doppler-managed).
565
+ ```
566
+
567
+ **Decision** (a choice with tradeoffs):
568
+ ```yaml
569
+ ---
570
+ title: Server sessions (not JWTs)
571
+ topics: [auth, decisions]
572
+ supersedes: jwt-sessions
573
+ files: [src/auth/session.ts, src/auth/middleware.ts]
574
+ ---
575
+
576
+ # Server sessions
577
+
578
+ We use server-side sessions, not JWTs. Session state lives in Redis, keyed
579
+ by a rotating cookie. Chosen because refresh-token rotation already required
580
+ server state for the revocation list, removing the main perceived benefit
581
+ of stateless JWTs.
582
+ ```
583
+
584
+ **Flow** (a multi-file process):
585
+ ```yaml
586
+ ---
587
+ title: Checkout flow
588
+ topics: [flows, payments]
589
+ files: [src/checkout/, src/api/cart/submit.ts, backend/src/services/orders.py]
590
+ ---
591
+
592
+ # Checkout flow
593
+
594
+ The browser POSTs /api/cart/submit. The handler creates a Stripe
595
+ PaymentIntent and an inventory lock row in orders (status=pending). Client
596
+ confirms the PaymentIntent. Stripe's webhook flips status=paid and releases
597
+ the lock.
598
+ ```
599
+
600
+ **Gotcha** (something that bit us):
601
+ ```yaml
602
+ ---
603
+ title: Inventory-lock deadlock
604
+ topics: [gotchas, payments]
605
+ files: [backend/src/services/orders.py]
606
+ ---
607
+
608
+ # Inventory-lock deadlock
609
+
610
+ Before March 2026, the Stripe webhook acquired the same row lock the
611
+ checkout path held. When Stripe retried a delayed webhook during a new
612
+ checkout for the same SKU, the two transactions deadlocked; Postgres killed
613
+ one, usually the webhook, leaving orders silently stuck in pending.
614
+ ```
615
+
616
+ ### General principles
617
+
618
+ - Every sentence contains a specific fact. If the sentence could describe any codebase, cut it.
619
+ - Neutral tone. `is`, not `serves as`.
620
+ - No speculation. "I don't know why X" is fine as an explicit note; a guess is not.
621
+ - Prose first. Bullets for genuine lists. Tables for structured comparison only.
622
+ - Reference code with `[[...]]`. Inline mentions are fine but only `[[...]]` gets indexed.
623
+ - List files in frontmatter. Pages about specific code need `files: [...]` to surface in `--mentions`.
624
+
625
+ ---
626
+
627
+ ## 10. Troubleshooting
628
+
629
+ ### Catch-all: `almanac doctor`
630
+
631
+ When something feels off and you don't know where to start, run `almanac doctor`. It reports install state (binary, native binding, Claude auth, hook, guides, import line) and current-wiki state (registered, page/topic counts, index freshness, last capture age, health problems). Every ✗ comes with a one-line `run: …` fix. `--json` for scripting.
632
+
633
+ ### "better-sqlite3 bindings missing"
634
+ Node version / arch mismatch with the prebuilt binary. `almanac doctor` reports it as `install.sqlite: problem` with the underlying error's first line. Fix:
635
+ ```bash
636
+ npm rebuild better-sqlite3 # in the install directory
637
+ ```
638
+ On M-series Macs with x64+arm64 Node installs, bindings are arch-specific — rebuild in the arch you'll run from. Node ≥20 required (`engines.node`).
639
+
640
+ ### "search returns nothing"
641
+
642
+ Two different outcomes to distinguish:
643
+ - **Silent stdout, stderr says `# 0 results`.** The query ran and genuinely matched nothing — this is an answer, not a failure. Either the wiki doesn't cover that area yet, or the query needs broadening.
644
+ - **An actual error on stderr.** Commander or `almanac:` prefix. That's a broken invocation; re-read the `--help`.
645
+
646
+ `--json` is silent on stderr — the `[]` array is the unambiguous empty signal.
647
+
648
+ ### "pages don't show up in `--mentions`"
649
+
650
+ Missing `files:` frontmatter, OR path referenced only in inline prose (not via `[[...]]`). Inline prose isn't indexed. If neither: `almanac reindex`.
651
+
652
+ ### "topics missing after rename"
653
+
654
+ `topics rename` bumps `topics.yaml` mtime → next query's `ensureFreshIndex` catches up. Hand-edited `topics.yaml` without page rewrites leaves frontmatter out of sync — `almanac reindex` then audit with `almanac health --orphans --empty-topics`.
655
+
656
+ ### "capture didn't fire"
657
+
658
+ ```bash
659
+ almanac doctor # reports hook state + last capture age + auth
660
+ claude auth status # OAuth token present?
661
+ echo "${ANTHROPIC_API_KEY:0:10}" # API key fallback?
662
+ ls -lah .almanac/.capture-*.log
663
+ ```
664
+
665
+ No logs at all → script bailed pre-background. Add `set -x` to `hooks/almanac-capture.sh` to trace. If the hook itself isn't installed, `almanac doctor` reports `install.hook: problem` with `run: almanac setup --yes`.
666
+
667
+ ### "slug collision warnings"
668
+
669
+ Two files kebab-case to the same slug (`Checkout Flow.md` and `checkout-flow.md`). `health --slug-collisions` lists them. Rename one, grep `.almanac/pages/` for any `[[...]]` references, update them.
670
+
671
+ ### "dead-refs reports files that exist"
672
+
673
+ Case sensitivity on Linux. Schema v2 stores `original_path` for case-preserving stat; upgrade from pre-v2 requires `almanac reindex`. Dangling symlinks also fail `existsSync`.
674
+
675
+ ### Forensics files
676
+
677
+ - `.almanac/.capture-<session-id>.log` — per-session SDK transcript from capture. Writer + reviewer interleaved.
678
+ - `.almanac/.bootstrap-<timestamp>.log` — one per bootstrap. Gitignored by default.
679
+
680
+ ---
681
+
682
+ ## When in doubt
683
+
684
+ - `almanac --help` / `almanac <command> --help` — flags are always current for the installed build.
685
+ - `almanac doctor` — one command that reports everything relevant about install + current wiki.
686
+ - `.almanac/README.md` in the repo — the notability bar and topic taxonomy for *this* repo override anything here.