codealmanac 0.1.1 → 0.1.2
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/dist/codealmanac.js +983 -391
- package/dist/codealmanac.js.map +1 -1
- package/guides/mini.md +187 -0
- package/guides/reference.md +599 -0
- package/package.json +2 -1
- package/prompts/bootstrap.md +55 -10
|
@@ -0,0 +1,599 @@
|
|
|
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`, `path`, `info`, `topics`, `health`
|
|
8
|
+
2. **Edit** — `tag`, `untag`, `topics create|link|unlink|rename|delete|describe`, `reindex`
|
|
9
|
+
3. **Wiki lifecycle** — `init`, `list`, `bootstrap`, `capture`
|
|
10
|
+
4. **Install** — `hook install|uninstall|status`
|
|
11
|
+
|
|
12
|
+
Every command auto-registers the current repo in `~/.almanac/registry.json` on first run. Exceptions: `init` (registers explicitly) and `list --drop` (skips auto-register so the removal intent isn't undone).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Full command matrix
|
|
17
|
+
|
|
18
|
+
### 1.1 Query
|
|
19
|
+
|
|
20
|
+
#### `almanac search [query]`
|
|
21
|
+
|
|
22
|
+
| Flag | Type | Default | Semantics |
|
|
23
|
+
|---|---|---|---|
|
|
24
|
+
| `[query]` | string | — | FTS5 MATCH against titles + bodies. Omit for pure-filter queries. |
|
|
25
|
+
| `--topic <name...>` | repeatable | `[]` | AND-intersect filter. Walks the DAG subtree — `--topic auth` matches `auth` or any descendant. |
|
|
26
|
+
| `--mentions <path>` | string | — | Pages referencing this file (no trailing `/`) or folder (trailing `/`). Matches both `files:` frontmatter and `[[...]]` file refs, case-insensitive. |
|
|
27
|
+
| `--since <duration>` | duration | — | Updated within window. Format: `<int>[smhdw]` (`2w`, `30d`, `48h`). By file mtime. |
|
|
28
|
+
| `--stale <duration>` | duration | — | Inverse of `--since`. |
|
|
29
|
+
| `--orphan` | bool | false | Pages with zero topics. |
|
|
30
|
+
| `--include-archive` | bool | false | Include archived pages. |
|
|
31
|
+
| `--archived` | bool | false | Archived pages only. |
|
|
32
|
+
| `--wiki <name>` | string | current repo | Target a specific registered wiki. |
|
|
33
|
+
| `--json` | bool | false | Structured JSON. |
|
|
34
|
+
| `--limit <n>` | int ≥0 | unbounded | Cap results. |
|
|
35
|
+
|
|
36
|
+
**Default output:** one slug per line to stdout.
|
|
37
|
+
**`--json` schema:** `{wiki, results: [{slug, title, updated_at, topics, path}]}`.
|
|
38
|
+
**Exit:** `0` always (empty result isn't an error). `2` on flag validation failure.
|
|
39
|
+
|
|
40
|
+
#### `almanac show [slug]`
|
|
41
|
+
|
|
42
|
+
| Flag | Semantics |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `[slug]` | Required unless `--stdin`. Slugs are kebab-canonicalized before lookup. |
|
|
45
|
+
| `--stdin` | Read slugs from stdin. Pages separated by form-feed (`\f`) in output. |
|
|
46
|
+
| `--wiki <name>` | Target a specific registered wiki. |
|
|
47
|
+
|
|
48
|
+
Projections (if available in the installed build — check `--help`): `--raw` (body only), `--meta` (metadata only), `--lead` (first paragraph), `--backlinks` (pages linking in), `--links` (pages this links out to). Falls back to `almanac info` + `path` if missing.
|
|
49
|
+
|
|
50
|
+
**Exit:** `0` on success, `1` if slug not found, `2` on flag errors.
|
|
51
|
+
|
|
52
|
+
#### `almanac path [slug]`
|
|
53
|
+
|
|
54
|
+
Resolve slug → absolute file path. `--stdin` writes one path per input line, preserving order; missing slugs emit a blank line so output is 1:1.
|
|
55
|
+
|
|
56
|
+
#### `almanac info [slug]`
|
|
57
|
+
|
|
58
|
+
Metadata only (topics, refs, links, lineage). No body.
|
|
59
|
+
|
|
60
|
+
**`--json` schema:**
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"slug": "checkout-flow",
|
|
64
|
+
"title": "...",
|
|
65
|
+
"updated_at": 1713000000,
|
|
66
|
+
"archived_at": null,
|
|
67
|
+
"superseded_by": null, "supersedes": null,
|
|
68
|
+
"topics": [...],
|
|
69
|
+
"files": [...],
|
|
70
|
+
"wikilinks_out": [...],
|
|
71
|
+
"wikilinks_in": [...],
|
|
72
|
+
"file_refs": [{"path": "...", "is_dir": false}],
|
|
73
|
+
"cross_wiki_refs": [{"wiki": "...", "target": "..."}]
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### `almanac topics` (and subcommands)
|
|
78
|
+
|
|
79
|
+
`almanac topics` — list all with page counts. `--json` emits `[{slug, description, parents[], children[], page_count}]`.
|
|
80
|
+
|
|
81
|
+
`almanac topics show <slug>` — description, parents, children, pages. `--descendants` includes pages tagged with descendant topics (walks the DAG subtree).
|
|
82
|
+
|
|
83
|
+
#### `almanac health`
|
|
84
|
+
|
|
85
|
+
Eight independent categories. One failing doesn't skip the others.
|
|
86
|
+
|
|
87
|
+
| Flag | Default | Semantics |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `--topic <name>` | — | Scope page-level checks to the topic + descendants. Scopes topic-level checks to the subtree. |
|
|
90
|
+
| `--stale <duration>` | `90d` | Threshold for the `stale` category. |
|
|
91
|
+
| `--stdin` | false | Restrict page-level checks to slugs from stdin. Intersects with `--topic`. |
|
|
92
|
+
| `--json` | false | Structured JSON. |
|
|
93
|
+
| `--wiki <name>` | current repo | Target a specific registered wiki. |
|
|
94
|
+
|
|
95
|
+
**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.
|
|
96
|
+
|
|
97
|
+
### 1.2 Edit
|
|
98
|
+
|
|
99
|
+
#### `almanac tag [page] [topics...]`
|
|
100
|
+
|
|
101
|
+
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.
|
|
102
|
+
|
|
103
|
+
#### `almanac untag <page> <topic>`
|
|
104
|
+
|
|
105
|
+
Remove one topic. Idempotent (silent 0 if page wasn't tagged).
|
|
106
|
+
|
|
107
|
+
#### `almanac topics create <name>`
|
|
108
|
+
|
|
109
|
+
`--parent <slug>` repeatable. Rejects if any parent slug doesn't exist.
|
|
110
|
+
|
|
111
|
+
#### `almanac topics link <child> <parent>` / `topics unlink <child> <parent>`
|
|
112
|
+
|
|
113
|
+
Add/remove a DAG edge. `link` is cycle-checked (§5). `unlink` is idempotent.
|
|
114
|
+
|
|
115
|
+
#### `almanac topics rename <old> <new>`
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
#### `almanac topics delete <slug>`
|
|
120
|
+
|
|
121
|
+
Removes from `topics.yaml`, untags every affected page. Does **not** cascade to children — orphaned children become top-level. Run `almanac health` to surface stragglers.
|
|
122
|
+
|
|
123
|
+
#### `almanac topics describe <slug> <text>`
|
|
124
|
+
|
|
125
|
+
Set the topic's one-line description in `topics.yaml`.
|
|
126
|
+
|
|
127
|
+
#### `almanac reindex`
|
|
128
|
+
|
|
129
|
+
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.
|
|
130
|
+
|
|
131
|
+
### 1.3 Wiki lifecycle
|
|
132
|
+
|
|
133
|
+
#### `almanac init`
|
|
134
|
+
|
|
135
|
+
Scaffolds `.almanac/` in cwd. `--name <name>` sets the registry entry (default: basename of cwd). `--description <text>` stored in the registry. Creates `pages/`, `topics.yaml`, `README.md`, `index.db`, and adds `.gitignore` entries.
|
|
136
|
+
|
|
137
|
+
#### `almanac list`
|
|
138
|
+
|
|
139
|
+
`--json` for structured output. `--drop <name>` is the **only** way to remove a registry entry.
|
|
140
|
+
|
|
141
|
+
#### `almanac bootstrap`
|
|
142
|
+
|
|
143
|
+
Spawn an agent to create initial wiki stubs. Requires `ANTHROPIC_API_KEY`. `--quiet` suppresses per-tool streaming. `--model <model>` overrides the model. `--force` overwrites a populated wiki. Writes `.almanac/.bootstrap-<timestamp>.log`.
|
|
144
|
+
|
|
145
|
+
#### `almanac capture [transcript]`
|
|
146
|
+
|
|
147
|
+
Capture knowledge from a Claude Code session transcript. Requires `ANTHROPIC_API_KEY` or a logged-in Claude Code session. Refuses if no `.almanac/` exists — capture maintains wikis, doesn't create them.
|
|
148
|
+
|
|
149
|
+
**Transcript resolution order:** explicit positional → `--session <id>` matches filename under `~/.claude/projects/` → most recent transcript whose recorded `cwd` matches this repo.
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
### 1.4 Install
|
|
154
|
+
|
|
155
|
+
#### `almanac hook install | uninstall | status`
|
|
156
|
+
|
|
157
|
+
See §7 — the hook is complex enough to warrant its own section.
|
|
158
|
+
|
|
159
|
+
### 1.5 `--stdin` pipe semantics
|
|
160
|
+
|
|
161
|
+
Commands that accept `--stdin`: `show`, `path`, `info`, `tag`, `health`.
|
|
162
|
+
|
|
163
|
+
- One slug per line; blank lines ignored; whitespace trimmed.
|
|
164
|
+
- Output order mirrors input order (matters for `path`, `info`).
|
|
165
|
+
- Missing slugs don't abort — logged to stderr, pipeline continues. `show --stdin` writes a "not found" marker and keeps exit `0` for pipeline resilience; `path` / `info` exit `1` overall if any slug was missing.
|
|
166
|
+
- `--stdin` must be explicit. No `isTTY` auto-detection (confusing under script redirection).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 2. The unified `[[...]]` classifier
|
|
171
|
+
|
|
172
|
+
One syntax, four kinds. Rules applied in order:
|
|
173
|
+
|
|
174
|
+
1. **`:` before any `/`** → cross-wiki (`[[wiki:slug]]`)
|
|
175
|
+
2. **Contains `/`** → file (no trailing `/`) or folder (trailing `/`)
|
|
176
|
+
3. **Otherwise** → page slug wikilink
|
|
177
|
+
|
|
178
|
+
| Input | Classified | Why |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `[[a:b/c]]` | xwiki `a`→`b/c` | colon before slash, rule 1 |
|
|
181
|
+
| `[[src/a:b]]` | file `src/a:b` | slash before colon, rule 2 |
|
|
182
|
+
| `[[./x]]` | file `x` | normalized; `./` stripped |
|
|
183
|
+
| `[[src/checkout/]]` | folder | trailing `/` |
|
|
184
|
+
| `[[foo\|display]]` | page `foo` | Obsidian pipe stripped |
|
|
185
|
+
| `[[ ]]` | null | empty after trim |
|
|
186
|
+
|
|
187
|
+
**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.
|
|
188
|
+
|
|
189
|
+
**Case sensitivity:** the indexer stores two forms per file/folder ref:
|
|
190
|
+
- `path` — lowercased, used for `--mentions` lookups (search is case-insensitive).
|
|
191
|
+
- `original_path` — as-written, used for filesystem `stat` in `health dead-refs` so case-sensitive filesystems (Linux, some Docker images) don't false-negative.
|
|
192
|
+
|
|
193
|
+
**Broken links** are recorded anyway (`wikilinks` table keeps the row), then surfaced by `health --broken-links`. Reindex is non-validating by design.
|
|
194
|
+
|
|
195
|
+
Cross-wiki refs live in their own table (`cross_wiki_links`), never lowercased.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 3. Frontmatter schema
|
|
200
|
+
|
|
201
|
+
| Field | Type | Default | Purpose |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| `title` | string | H1 fallback | Display title. Missing → first H1 in body. |
|
|
204
|
+
| `topics` | string[] | `[]` | DAG tags. Kebab-cased on ingest; duplicates collapsed. |
|
|
205
|
+
| `files` | string[] | `[]` | File/folder paths this page documents. Load-bearing for `--mentions`. Trailing `/` = folder. |
|
|
206
|
+
| `archived_at` | date / ISO string / epoch seconds | `null` | Non-null → excluded from default search. See §4. |
|
|
207
|
+
| `superseded_by` | slug | `null` | For archived pages: the active replacement. |
|
|
208
|
+
| `supersedes` | slug | `null` | For active pages: the archived predecessor. |
|
|
209
|
+
|
|
210
|
+
**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.
|
|
211
|
+
|
|
212
|
+
**Full example:**
|
|
213
|
+
|
|
214
|
+
```markdown
|
|
215
|
+
---
|
|
216
|
+
title: Checkout flow
|
|
217
|
+
topics: [flows, payments]
|
|
218
|
+
files:
|
|
219
|
+
- src/checkout/handler.ts
|
|
220
|
+
- src/checkout/
|
|
221
|
+
- docker-compose.yml
|
|
222
|
+
archived_at: null
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
# Checkout flow
|
|
226
|
+
|
|
227
|
+
The flow starts at `src/checkout/handler.ts` when the browser POSTs
|
|
228
|
+
`/api/cart/submit`. The handler creates a Stripe PaymentIntent, writes an
|
|
229
|
+
inventory lock row to Supabase, returns the PI client secret. See
|
|
230
|
+
[[inventory-lock-gotcha]] for the deadlock we hit in March.
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 4. Archive / lineage
|
|
236
|
+
|
|
237
|
+
Pages evolve in place. **Edit the existing page** when facts change — git history is the archive.
|
|
238
|
+
|
|
239
|
+
**Archive + supersede** is reserved for **fundamental reversals**: a central decision overturned, a system replaced wholesale, an incident re-opened.
|
|
240
|
+
|
|
241
|
+
**The test:** *is this an update to the old state, or a reversal of a central decision?* Update → edit. Reversal → archive + successor.
|
|
242
|
+
|
|
243
|
+
### Frontmatter shapes
|
|
244
|
+
|
|
245
|
+
Archived page:
|
|
246
|
+
```yaml
|
|
247
|
+
---
|
|
248
|
+
title: JWT sessions (archived)
|
|
249
|
+
topics: [auth, decisions]
|
|
250
|
+
archived_at: 2026-03-15
|
|
251
|
+
superseded_by: server-sessions
|
|
252
|
+
---
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Successor:
|
|
256
|
+
```yaml
|
|
257
|
+
---
|
|
258
|
+
title: Server sessions
|
|
259
|
+
topics: [auth, decisions]
|
|
260
|
+
supersedes: jwt-sessions
|
|
261
|
+
files: [src/auth/session.ts, src/auth/middleware.ts]
|
|
262
|
+
---
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Both files exist on disk. Both are indexed.
|
|
266
|
+
|
|
267
|
+
### Search scoping
|
|
268
|
+
|
|
269
|
+
- Default: active only.
|
|
270
|
+
- `--include-archive`: active + archived.
|
|
271
|
+
- `--archived`: archived only. Useful for retrospectives.
|
|
272
|
+
|
|
273
|
+
### Health exemptions for archived pages
|
|
274
|
+
|
|
275
|
+
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.
|
|
276
|
+
|
|
277
|
+
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.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## 5. DAG model and traversal
|
|
282
|
+
|
|
283
|
+
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.
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
# topics.yaml
|
|
287
|
+
topics:
|
|
288
|
+
auth:
|
|
289
|
+
description: authentication, sessions, identity
|
|
290
|
+
parents: []
|
|
291
|
+
jwt:
|
|
292
|
+
parents: [auth]
|
|
293
|
+
sessions:
|
|
294
|
+
parents: [auth]
|
|
295
|
+
checkout:
|
|
296
|
+
parents: [flows, payments] # multi-parent
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**`--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.
|
|
300
|
+
|
|
301
|
+
### Cycle prevention
|
|
302
|
+
|
|
303
|
+
Three layers:
|
|
304
|
+
1. **CHECK constraint** on `topic_parents` blocks self-loops (`child = parent`).
|
|
305
|
+
2. **Pre-insert traversal** walks parents upward before committing; refuses if `child` is reachable.
|
|
306
|
+
3. **Depth cap of 32** bails the traversal defensively. Real topic DAGs are ≤4 deep.
|
|
307
|
+
|
|
308
|
+
`almanac topics link A B` where A is already an ancestor of B fails: `almanac: link would create cycle: A → … → B → A`.
|
|
309
|
+
|
|
310
|
+
### Rename / delete side effects
|
|
311
|
+
|
|
312
|
+
`topics rename old new`:
|
|
313
|
+
1. Rewrite `topics.yaml` atomically (tmp + rename). New key written, old removed, parent edges migrated.
|
|
314
|
+
2. Rewrite every page whose `topics:` contains `old`. Body bytes preserved.
|
|
315
|
+
3. Reindex fires automatically on `topics.yaml` mtime bump.
|
|
316
|
+
|
|
317
|
+
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.
|
|
318
|
+
|
|
319
|
+
`topics delete slug`:
|
|
320
|
+
1. Remove from `topics.yaml`.
|
|
321
|
+
2. Untag every affected page.
|
|
322
|
+
3. **Does not cascade.** Children of the deleted topic become top-level. Run `almanac health --empty-topics` and re-parent or prune.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## 6. Shell-piping cookbook
|
|
327
|
+
|
|
328
|
+
Every command emits slugs one-per-line, so they compose.
|
|
329
|
+
|
|
330
|
+
**Find stale pages in a topic and tag them `review-needed`:**
|
|
331
|
+
```bash
|
|
332
|
+
almanac search --topic auth --stale 90d \
|
|
333
|
+
| almanac tag --stdin review-needed
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Find pages referencing a just-deleted file:**
|
|
337
|
+
```bash
|
|
338
|
+
almanac search --mentions src/legacy/oauth.ts --include-archive
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
**Bulk move pages from an old topic to a new one:**
|
|
342
|
+
```bash
|
|
343
|
+
almanac topics create payments-v2 --parent payments
|
|
344
|
+
almanac search --topic old-payments | almanac tag --stdin payments-v2
|
|
345
|
+
almanac topics delete old-payments
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**List pages that lack `files:` frontmatter for files they mention in prose:**
|
|
349
|
+
```bash
|
|
350
|
+
almanac search | while read slug; do
|
|
351
|
+
info=$(almanac info "$slug" --json)
|
|
352
|
+
prose=$(echo "$info" | jq -r '.file_refs[].path' | sort -u)
|
|
353
|
+
fm=$(echo "$info" | jq -r '.files[]' | sort -u)
|
|
354
|
+
missing=$(comm -23 <(echo "$prose") <(echo "$fm"))
|
|
355
|
+
[ -n "$missing" ] && { echo "$slug:"; echo "$missing" | sed 's/^/ /'; }
|
|
356
|
+
done
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Open every orphan page in `$EDITOR`:**
|
|
360
|
+
```bash
|
|
361
|
+
almanac search --orphan | almanac path --stdin | xargs -n 1 "$EDITOR"
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## 7. The capture hook
|
|
367
|
+
|
|
368
|
+
### Trigger
|
|
369
|
+
|
|
370
|
+
Claude Code invokes `SessionEnd` hooks after each session. Payload on stdin:
|
|
371
|
+
```json
|
|
372
|
+
{ "session_id": "uuid", "transcript_path": "/abs/path.jsonl", "cwd": "/abs/repo/path" }
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### What `hooks/almanac-capture.sh` does
|
|
376
|
+
|
|
377
|
+
1. Parse payload with `jq`. Missing `jq` → exit 0 silently.
|
|
378
|
+
2. Walk upward from `cwd` for a `.almanac/`. Bounded at filesystem root.
|
|
379
|
+
3. Background `almanac capture "$TRANSCRIPT" --session "$SESSION_ID" --quiet`, redirect to `.almanac/.capture-$SESSION_ID.log`, `disown`.
|
|
380
|
+
4. Exit always `0`. Capture failures must never break Claude Code's session-end path.
|
|
381
|
+
|
|
382
|
+
Falls back to `npx --no-install codealmanac` if `almanac` isn't on `PATH`.
|
|
383
|
+
|
|
384
|
+
### `hook install | uninstall | status`
|
|
385
|
+
|
|
386
|
+
**`install`:**
|
|
387
|
+
- **Idempotent.** Twice → one entry, not two.
|
|
388
|
+
- **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.
|
|
389
|
+
- **Replaces stale almanac entries** — same filename, different absolute path (old install in a different `node_modules`).
|
|
390
|
+
- **Atomic** tmp + rename. Claude Code never sees a partial `settings.json`.
|
|
391
|
+
|
|
392
|
+
**`uninstall`:**
|
|
393
|
+
- Removes only entries whose command ends in `almanac-capture.sh`. Foreign entries stay.
|
|
394
|
+
- Drops `hooks.SessionEnd` if empty, then `hooks` if empty. File returns to pre-install shape.
|
|
395
|
+
|
|
396
|
+
**`status`:**
|
|
397
|
+
- Reports installed / not installed, the script path, the settings path. Non-interactive.
|
|
398
|
+
|
|
399
|
+
### Diagnosing "capture didn't run"
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
almanac hook status # installed?
|
|
403
|
+
ls -lah .almanac/.capture-*.log # any logs at all?
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
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).
|
|
407
|
+
|
|
408
|
+
### Diagnosing "capture ran but wrote nothing"
|
|
409
|
+
|
|
410
|
+
```bash
|
|
411
|
+
tail -200 .almanac/.capture-<id>.log
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Common causes:
|
|
415
|
+
- `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.
|
|
416
|
+
- Transcript path didn't resolve. Capture prints resolution status early.
|
|
417
|
+
- Reviewer rejected the draft for notability — rationale is in the log.
|
|
418
|
+
- Session was pure-read with no decisions or discoveries. Correct no-op.
|
|
419
|
+
|
|
420
|
+
---
|
|
421
|
+
|
|
422
|
+
## 8. Multi-wiki model
|
|
423
|
+
|
|
424
|
+
### Registry at `~/.almanac/registry.json`
|
|
425
|
+
|
|
426
|
+
```json
|
|
427
|
+
{
|
|
428
|
+
"wikis": [
|
|
429
|
+
{ "name": "openalmanac", "path": "/Users/me/code/openalmanac", "description": "…" },
|
|
430
|
+
{ "name": "codealmanac", "path": "/Users/me/code/codealmanac" }
|
|
431
|
+
]
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### Registration paths
|
|
436
|
+
|
|
437
|
+
- **`almanac init`** — explicit. Sets `name` and `description`.
|
|
438
|
+
- **Silent auto-register** — every other command (except `list --drop`) calls `autoRegisterIfNeeded` on cwd. Repo with `.almanac/` but no registry entry → added with `name = basename(cwd)`, no description. Makes "cloned a repo with `.almanac/` committed" just work.
|
|
439
|
+
- **`almanac list --drop <name>`** — the only removal path. Skips auto-register so the removal isn't immediately undone.
|
|
440
|
+
|
|
441
|
+
### `--wiki <name>`
|
|
442
|
+
|
|
443
|
+
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 wiki registered for this cwd. run 'almanac init' or pass --wiki.`
|
|
444
|
+
|
|
445
|
+
### Cross-wiki link resolution
|
|
446
|
+
|
|
447
|
+
`[[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.
|
|
448
|
+
|
|
449
|
+
### Unreachable targets
|
|
450
|
+
|
|
451
|
+
- Searches silently skip.
|
|
452
|
+
- `health --broken-xwiki` reports them.
|
|
453
|
+
- `show --wiki unreachable` exits `1` with a diagnostic.
|
|
454
|
+
|
|
455
|
+
---
|
|
456
|
+
|
|
457
|
+
## 9. Notability and writing conventions
|
|
458
|
+
|
|
459
|
+
The reviewer subagent enforces these during capture. Applying them yourself reduces rework.
|
|
460
|
+
|
|
461
|
+
### Patterns to avoid (bad → good)
|
|
462
|
+
|
|
463
|
+
**Significance inflation.**
|
|
464
|
+
- Bad: `The Stripe integration serves as a testament to our commitment to payment reliability.`
|
|
465
|
+
- Good: `The Stripe integration handles card payments. PaymentIntent is created at cart-submit; webhook confirmation completes the order.`
|
|
466
|
+
|
|
467
|
+
**Interpretive -ing clauses.**
|
|
468
|
+
- Bad: `The team migrated to async webhooks, highlighting their pragmatic approach.`
|
|
469
|
+
- Good: `The team migrated to async webhooks in March 2026 after the inventory-lock deadlock.`
|
|
470
|
+
|
|
471
|
+
**Vague attribution.**
|
|
472
|
+
- Bad: `Experts argue JWTs are unsuitable for sessions.`
|
|
473
|
+
- Good: `We moved off JWTs to server sessions in 2025 because refresh-token rotation required server state anyway.`
|
|
474
|
+
|
|
475
|
+
**Promotional language.**
|
|
476
|
+
- Bad: `Our groundbreaking approach delivers vibrant performance.`
|
|
477
|
+
- Good: `Rate limiting: sliding-window counter in Redis, 100 req / user / minute, in src/middleware/rate-limit.ts.`
|
|
478
|
+
|
|
479
|
+
**Hedging.**
|
|
480
|
+
- Bad: `While details are limited, it appears the cache might use LRU eviction.`
|
|
481
|
+
- Good: confirm from code, or cut the sentence.
|
|
482
|
+
|
|
483
|
+
**Empty evaluative sentences.** `This architecture is elegant and powerful.` → cut.
|
|
484
|
+
|
|
485
|
+
**Formulaic conclusions.** `In conclusion, the checkout flow demonstrates careful engineering.` → cut. Pages don't need conclusions.
|
|
486
|
+
|
|
487
|
+
### The four page shapes
|
|
488
|
+
|
|
489
|
+
**Entity** (a thing we depend on):
|
|
490
|
+
```yaml
|
|
491
|
+
---
|
|
492
|
+
title: Supabase
|
|
493
|
+
topics: [stack, database]
|
|
494
|
+
files: [src/lib/supabase.ts, backend/src/models/, docker-compose.yml]
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
# Supabase
|
|
498
|
+
|
|
499
|
+
PostgreSQL hosted on Supabase. Connection pooling via Supavisor. Client
|
|
500
|
+
singleton in src/lib/supabase.ts; backend models in backend/src/models/
|
|
501
|
+
use SQLAlchemy against the same DATABASE_URL (Doppler-managed).
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Decision** (a choice with tradeoffs):
|
|
505
|
+
```yaml
|
|
506
|
+
---
|
|
507
|
+
title: Server sessions (not JWTs)
|
|
508
|
+
topics: [auth, decisions]
|
|
509
|
+
supersedes: jwt-sessions
|
|
510
|
+
files: [src/auth/session.ts, src/auth/middleware.ts]
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
# Server sessions
|
|
514
|
+
|
|
515
|
+
We use server-side sessions, not JWTs. Session state lives in Redis, keyed
|
|
516
|
+
by a rotating cookie. Chosen because refresh-token rotation already required
|
|
517
|
+
server state for the revocation list, removing the main perceived benefit
|
|
518
|
+
of stateless JWTs.
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**Flow** (a multi-file process):
|
|
522
|
+
```yaml
|
|
523
|
+
---
|
|
524
|
+
title: Checkout flow
|
|
525
|
+
topics: [flows, payments]
|
|
526
|
+
files: [src/checkout/, src/api/cart/submit.ts, backend/src/services/orders.py]
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
# Checkout flow
|
|
530
|
+
|
|
531
|
+
The browser POSTs /api/cart/submit. The handler creates a Stripe
|
|
532
|
+
PaymentIntent and an inventory lock row in orders (status=pending). Client
|
|
533
|
+
confirms the PaymentIntent. Stripe's webhook flips status=paid and releases
|
|
534
|
+
the lock.
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
**Gotcha** (something that bit us):
|
|
538
|
+
```yaml
|
|
539
|
+
---
|
|
540
|
+
title: Inventory-lock deadlock
|
|
541
|
+
topics: [gotchas, payments]
|
|
542
|
+
files: [backend/src/services/orders.py]
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
# Inventory-lock deadlock
|
|
546
|
+
|
|
547
|
+
Before March 2026, the Stripe webhook acquired the same row lock the
|
|
548
|
+
checkout path held. When Stripe retried a delayed webhook during a new
|
|
549
|
+
checkout for the same SKU, the two transactions deadlocked; Postgres killed
|
|
550
|
+
one, usually the webhook, leaving orders silently stuck in pending.
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### General principles
|
|
554
|
+
|
|
555
|
+
- Every sentence contains a specific fact. If the sentence could describe any codebase, cut it.
|
|
556
|
+
- Neutral tone. `is`, not `serves as`.
|
|
557
|
+
- No speculation. "I don't know why X" is fine as an explicit note; a guess is not.
|
|
558
|
+
- Prose first. Bullets for genuine lists. Tables for structured comparison only.
|
|
559
|
+
- Reference code with `[[...]]`. Inline mentions are fine but only `[[...]]` gets indexed.
|
|
560
|
+
- List files in frontmatter. Pages about specific code need `files: [...]` to surface in `--mentions`.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## 10. Troubleshooting
|
|
565
|
+
|
|
566
|
+
### "dead-refs reports files that exist"
|
|
567
|
+
Case sensitivity on Linux. Schema v2 stores `original_path` for case-preserving stat; upgrade from pre-v2 requires `almanac reindex`. Dangling symlinks fail `existsSync` too.
|
|
568
|
+
|
|
569
|
+
### "pages don't show up in `--mentions`"
|
|
570
|
+
Missing `files:` frontmatter, OR path referenced only in inline prose (not via `[[...]]`). Inline prose isn't indexed. If neither: `almanac reindex`.
|
|
571
|
+
|
|
572
|
+
### "topics missing after rename"
|
|
573
|
+
`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`.
|
|
574
|
+
|
|
575
|
+
### "capture didn't fire"
|
|
576
|
+
```bash
|
|
577
|
+
almanac hook status
|
|
578
|
+
claude auth status # OAuth token present?
|
|
579
|
+
echo "${ANTHROPIC_API_KEY:0:10}" # API key fallback?
|
|
580
|
+
ls -lah .almanac/.capture-*.log
|
|
581
|
+
```
|
|
582
|
+
No logs at all → script bailed pre-background. Add `set -x` to `hooks/almanac-capture.sh` to trace.
|
|
583
|
+
|
|
584
|
+
### "slug collision warnings"
|
|
585
|
+
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.
|
|
586
|
+
|
|
587
|
+
### "better-sqlite3 bindings missing"
|
|
588
|
+
Node version / arch mismatch with the prebuilt binary. `npm rebuild better-sqlite3`. 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`).
|
|
589
|
+
|
|
590
|
+
### Forensics files
|
|
591
|
+
- `.almanac/.capture-<session-id>.log` — per-session SDK transcript from capture. Writer + reviewer interleaved.
|
|
592
|
+
- `.almanac/.bootstrap-<timestamp>.log` — one per bootstrap. Gitignored by default.
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## When in doubt
|
|
597
|
+
|
|
598
|
+
- `almanac --help` / `almanac <command> --help` — flags are always current for the installed build.
|
|
599
|
+
- `.almanac/README.md` in the repo — the notability bar and topic taxonomy for *this* repo override anything here.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codealmanac",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A living wiki for codebases, maintained by AI agents. Documents what the code can't say: decisions, flows, invariants, incidents, gotchas.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"wiki",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dist",
|
|
30
30
|
"prompts",
|
|
31
31
|
"hooks",
|
|
32
|
+
"guides",
|
|
32
33
|
"README.md",
|
|
33
34
|
"LICENSE"
|
|
34
35
|
],
|