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.
- package/README.md +53 -40
- package/dist/codealmanac.js +2522 -1402
- package/dist/codealmanac.js.map +1 -1
- package/guides/mini.md +223 -0
- package/guides/reference.md +686 -0
- package/package.json +2 -1
- package/prompts/bootstrap.md +55 -10
|
@@ -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.
|