# Almanac — full reference

Long-form manual for the `almanac` CLI installed by the `codealmanac` npm package. The mini guide at `~/.claude/almanac.md` covers *when* to reach for each command; this covers *every flag, every return shape, every edge case*. Import with `@~/.claude/almanac-reference.md` on demand.

Groupings match `almanac --help`:

1. **Query** — `search`, `show`, `health`, `list`, `serve`
2. **Edit** — `tag`, `untag`, `migrate`, `topics ...`
3. **Wiki lifecycle** — `init`, `absorb`, `sync`, `ingest`, `garden`, `jobs`, `automation ...`, `reindex`
4. **Setup** — `setup`, `uninstall`, `doctor`, `update`

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).

A wiki gets scaffolded two ways: run `almanac init` yourself, or clone a repo that already has `.almanac/` committed (auto-registered on first query/edit command).

---

## 1. Full command matrix

### 1.1 Query

#### `almanac search [query]`

| Flag | Type | Default | Semantics |
|---|---|---|---|
| `[query]` | string | — | FTS5 MATCH against titles + bodies. Omit for pure-filter queries. |
| `--topic <name...>` | repeatable | `[]` | AND-intersect filter. Walks the DAG subtree — `--topic auth` matches `auth` or any descendant. |
| `--mentions <path>` | string | — | Pages referencing this path. Matches exact file, trailing-slash folders, and any file under a folder prefix. Case-insensitive. |
| `--since <duration>` | duration | — | Updated within window. Format: `<int>[smhdw]` (`2w`, `30d`, `48h`). By file mtime. |
| `--stale <duration>` | duration | — | Inverse of `--since`. |
| `--orphan` | bool | false | Pages with zero topics. |
| `--include-archive` | bool | false | Include archived pages. |
| `--archived` | bool | false | Archived pages only. |
| `--wiki <name>` | string | current repo | Target a specific registered wiki. |
| `--json` | bool | false | Structured JSON. |
| `--limit <n>` | int ≥0 | unbounded | Cap results. |

**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.
**`--json` schema:** JSON array of `{slug, title, updated_at, archived_at, superseded_by, topics}`. `title` is the page's frontmatter title (nullable when no frontmatter). `updated_at` is epoch seconds (file mtime). `archived_at` is epoch seconds or `null`. `superseded_by` is a slug string or `null`. `topics` is an array of topic slugs, sorted ascending. No `path` field — use `almanac show <slug> --path` (or `--json`) for the absolute path.
**Exit:** `0` always (empty result isn't an error). Arg-parse failures exit `1` with an `almanac:` error.

#### `almanac show [slug]`

Unified reader. Absorbs the old `info` and `path` commands — pick fields with flags.

| Flag | Default | Semantics |
|---|---|---|
| `[slug]` | — | Required unless `--stdin`. Slugs are kebab-canonicalized before lookup. |
| `--stdin` | false | Read slugs from stdin, one per line. JSON Lines output for `--json` mode. |
| `--wiki <name>` | current repo | Target a specific registered wiki. |
| `--json` | false | Structured JSON. Overrides every view/field flag. |
| `--body` | false | Body only. Guarantees exactly one trailing newline — shell redirect produces a well-formed file. |
| `--meta` | false | Metadata header only, no body. |
| `--lead` | false | First paragraph of the body only (cheap preview). |
| `--title` | false | Print title. |
| `--topics` | false | Print topics. |
| `--files` | false | Print file refs. |
| `--links` | false | Print outgoing wikilinks. |
| `--backlinks` | false | Print incoming wikilinks. |
| `--xwiki` | false | Print cross-wiki links. |
| `--lineage` | false | Print `archived_at` / `supersedes` / `superseded_by`. |
| `--updated` | false | Print updated timestamp. |
| `--path` | false | Print absolute file path (`info` + `path` replacement). |

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.

**Exit:** `0` on success, `1` if slug not found, non-zero on flag/input errors.

#### `almanac health`

Independent integrity categories. One failing doesn't skip the others.

| Flag | Default | Semantics |
|---|---|---|
| `--topic <name>` | — | Scope page-level checks to the topic + descendants. Scopes topic-level checks to the subtree. |
| `--stale <duration>` | `90d` | Threshold for the `stale` category. |
| `--stdin` | false | Restrict page-level checks to slugs from stdin. Intersects with `--topic`. |
| `--json` | false | Structured JSON. |
| `--wiki <name>` | current repo | Target a specific registered wiki. |

If `legacy-frontmatter` is non-empty, `health` writes a stderr warning that points to `almanac migrate legacy-sources`. `health` reports only; it does not mutate wiki files.

**Categories:** `orphans`, `stale`, `dead-refs`, `broken-links`, `broken-xwiki`, `missing-sources`, `unused-sources`, `legacy-frontmatter`, `unfixable-sources`, `duplicate-sources`, `empty-topics`, `empty-pages`, `slug-collisions`. Archived pages are exempt from most (see §4). Exit `0` always — the report IS the output.

#### `almanac list`

| Flag | Semantics |
|---|---|
| `--json` | Structured JSON. |
| `--drop <name>` | Remove a wiki from the registry. The **only** way entries are ever removed. Skips auto-register. |

#### `almanac serve`

Start the local read-only Almanac console. The console exposes registered wikis,
page/search/topic APIs, review state, and run/job transcript APIs. It does not
invoke AI, mutate wiki files, or install scheduler state.

| Flag | Default | Semantics |
|---|---|---|
| `--port <n>` | `0` | Port to bind. `0` chooses an available local port. |

### 1.2 Edit

#### `almanac tag [page] [topics...]`

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.

Flags: `--stdin`, `--wiki <name>`.

#### `almanac untag <page> <topic>`

Remove one topic. Idempotent (silent `0` if page wasn't tagged).

Flags: `--wiki <name>`.

#### `almanac migrate legacy-sources`

Rewrite safe legacy `files:` and string URL `sources:` frontmatter into structured `sources:` entries. The command does not invoke AI and preserves page body bytes.

Flags: `--topic <name>`, `--stdin`, `--wiki <name>`, `--json`.

#### `almanac migrate automation`

Replace a legacy macOS launchd plist that still runs `almanac capture sweep`
with the current scheduled `almanac sync` job, then remove the old plist. The
command preserves the legacy quiet window and interval when it can read them.

Flag: `--json`.

#### `almanac topics` (DAG management)

- `almanac topics list` — list all topics with page counts. `--json` emits an array of `{slug, title, description, page_count}`. `page_count` excludes archived pages (matches `topics show`). `parents`/`children` are only on `topics show <slug> --json`; use that when you need DAG edges.
- `almanac topics show <slug>` — description, parents, children, pages. `--descendants` includes pages tagged with descendant topics (walks the DAG subtree).
- `almanac topics create <name>` — `--parent <slug>` repeatable. Rejects if any parent slug doesn't exist.
- `almanac topics link <child> <parent>` / `almanac topics unlink <child> <parent>` — add/remove a DAG edge. `link` is cycle-checked (§5). `unlink` is idempotent.
- `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.
- `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.
- `almanac topics describe <slug> <text>` — set the topic's one-line description.

All topic subcommands accept `--wiki <name>`. `list` / `show` accept `--json`.

### 1.3 Wiki lifecycle

#### `almanac init`

Build the first wiki for this repo. Requires the selected provider to be installed and ready.

| Flag | Semantics |
|---|---|
| `--using <provider[/model]>` | Override the configured provider/model for this run. |
| `--background` | Start as a detached Almanac job. |
| `--json` | Emit structured JSON for background job start. Cannot be combined with foreground mode. |
| `--force` | Allow rebuilding an existing populated wiki. |
| `-y, --yes` | Confirm non-interactively. |

`init` runs foreground by default because first setup is an onboarding action. The Build operation receives the shared prompt base (`purpose`, `notability`, `syntax`) plus the Build algorithm prompt and runtime context.

#### `almanac absorb <inputs...>`

Update the wiki from one or more explicit files, folders, pull requests, issues,
or URLs.

| Flag | Semantics |
|---|---|
| `<inputs...>` | One or more files, folders, PRs, issues, or URLs to use as starting context. |
| `--using <provider[/model]>` | Override the configured provider/model for this run. |
| `--foreground` | Run attached instead of starting a background job. |
| `--json` | Emit structured JSON for background job start. Cannot be combined with `--foreground`. |
| `-y, --yes` | Confirm non-interactively. |

`absorb` treats the inputs as raw material, not the output. The agent updates
the wiki only when it finds durable project understanding. Mixed inputs are
valid: `almanac absorb docs/adr.md github:pr:123 https://example.com/spec`.

#### `almanac ingest <inputs...>`

Alias for `almanac absorb <inputs...>`. It exists for older habits and scripts;
new docs should prefer `absorb`.

#### `almanac sync`

Scan supported app transcript stores, find quiet Claude/Codex sessions that map
back to repos with `.almanac/`, reconcile each repo's sync ledger, and start
ordinary background Absorb jobs for new material. This is the work command used
by scheduled automation and can also be run manually for debugging.

| Flag | Semantics |
|---|---|
| `--from <apps>` | Comma-separated app list. Supports `claude`, `codex`, or both. |
| `--quiet <duration>` | Required transcript quiet window before sync. Default: `45m`. |
| `--using <provider[/model]>` | Override the configured provider/model for started Absorb jobs. |
| `status` subcommand | Discover and report eligible work without starting jobs or updating the ledger. |
| `--json` | Emit structured sync output. |

Sync passes the original transcript path to Absorb and adds cursor guidance from
`.almanac/jobs/sync-ledger.json`, so the agent focuses on lines/bytes after the
last successful absorb.

#### `almanac garden`

Maintain the wiki as a graph: page boundaries, links, topics, hubs, stale claims, archive/supersession chains, and synthesis quality.

| Flag | Semantics |
|---|---|
| `--using <provider[/model]>` | Override the configured provider/model for this run. |
| `--foreground` | Run attached instead of starting a background job. |
| `--json` | Emit structured JSON for background job start. Cannot be combined with `--foreground`. |
| `-y, --yes` | Confirm non-interactively. |

`garden` starts background by default.

#### `almanac jobs`

Inspect and manage Almanac jobs stored under `.almanac/jobs/`.

```bash
almanac jobs
almanac jobs list
almanac jobs show <job-id>
almanac jobs logs <job-id>
almanac jobs attach <job-id>
almanac jobs cancel <job-id>
```

Each jobs subcommand accepts `--json`. `attach` streams the JSONL event log until the run reaches `done`, `failed`, `cancelled`, or `stale`.

#### `almanac automation install | uninstall | status`

Manage scheduled Almanac tasks: sync, Garden, and CLI update.

| Command | Semantics |
|---|---|
| `almanac automation install` | Install macOS launchd sync and Garden jobs. Defaults: sync every `5h`, Garden every `4h`. |
| `almanac automation install --every 2h` | Customize the sync scheduler wakeup interval. |
| `almanac automation install --quiet 30m` | Customize the transcript quiet window passed to sync. |
| `almanac automation install update --every 1d` | Install only scheduled CLI self-update. |
| `almanac automation status` | Show whether scheduled tasks are installed and which commands they run. |
| `almanac automation uninstall` | Remove scheduled sync, Garden, and update jobs. |

See §7 for the scheduler and sync contract.

#### `almanac reindex`

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.

Flag: `--wiki <name>`.

### 1.4 Setup

#### `almanac setup`

Set up local Almanac access: choose the default agent/model, optionally install scheduled CLI self-update, install the two CLAUDE.md guides (`almanac.md`, `almanac-reference.md`) plus the `@~/.claude/almanac.md` import line, and install the managed Codex instructions. Idempotent.

| Flag | Semantics |
|---|---|
| `-y, --yes` | Skip prompts; use setup defaults. |
| `--agent <agent>` | Set the default provider. Accepts `claude`, `codex`, `cursor`, or optional shorthand like `claude/opus`. |
| `--model <model>` | Set the provider-local model during setup. Non-interactive equivalent of the model picker. |
| `--skip-automation` | Skip scheduled setup tasks, including CLI self-update. |
| `--sync-every <duration>` | Explicitly install scheduled sync/Garden and set the sync wakeup interval. Default: `5h`. |
| `--sync-quiet <duration>` | Explicitly install scheduled sync/Garden and set the transcript quiet window. Default: `45m`. |
| `--garden-every <duration>` | Explicitly install scheduled sync/Garden and set the Garden interval. Default: `4h`. |
| `--garden-off` | Explicitly install scheduled sync while disabling scheduled Garden. |
| `--auto-update` | Install scheduled CLI self-update. Default setup already enables this unless `--skip-automation` is present. |
| `--auto-update-every <duration>` | Set the scheduled CLI self-update interval. Default: `1d`. |
| `--skip-guides` | Opt out of the agent guides and Codex instructions. |
| `--auto-commit` | Opt into automatic git commits for wiki source changes. Default: off. |

Bare `almanac`, `almanac setup`, and the compatibility `npx codealmanac` bootstrap route here. Interactive setup chooses provider first, then provider-local model, then asks about CLI self-update and agent instructions. Codex with `gpt-5.5` is the built-in default. It does not ask about sync/Garden automation or auto-commit by default. `almanac --yes`, `almanac --agent codex --model gpt-5.5`, `almanac --skip-automation`, and `almanac --skip-guides` are the typical first-run invocations after install. Passing `--skip-automation --skip-guides` together short-circuits with a terse line — nothing was installed, no banner drawn. `--yes` and non-interactive setup install CLI self-update and agent instructions by default, but do not install sync/Garden automation and do not enable auto-commit unless `--auto-commit` is passed explicitly; they preserve an existing auto-commit opt-in.

#### `almanac uninstall`

Remove scheduled automation + guides + import line.

| Flag | Semantics |
|---|---|
| `-y, --yes` | Skip confirmations; remove everything. |
| `--keep-automation` | Don't remove the scheduler (guides still prompted unless `--yes`). |
| `--keep-guides` | Don't remove the guides or CLAUDE.md import (automation still prompted unless `--yes`). |

#### `almanac doctor`

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.

| Flag | Semantics |
|---|---|
| `--json` | Structured JSON. |
| `--install-only` | Report only on the install (skip the wiki section). |
| `--wiki-only` | Report only on the current wiki (skip the install section). |

**JSON shape:**
```json
{
  "version": "0.1.3",
  "install": [
    { "key": "install.path",   "status": "ok", "message": "..." },
    { "key": "install.sqlite", "status": "ok", "message": "..." },
    { "key": "install.auth",   "status": "problem", "message": "...", "fix": "run: claude auth login --claudeai" },
    { "key": "install.automation", "status": "ok", "message": "..." },
    { "key": "install.guides", "status": "ok", "message": "..." },
    { "key": "install.import", "status": "ok", "message": "..." }
  ],
  "agents": [
    {
      "id": "claude",
      "label": "Claude",
      "status": "ok",
      "readiness": "ready",
      "selected": true,
      "recommended": true,
      "installed": true,
      "authenticated": true,
      "model": "claude-sonnet-4-6",
      "providerDefaultModel": "claude-sonnet-4-6",
      "configuredModel": null,
      "account": "rohan@example.com",
      "detail": "rohan@example.com"
    }
  ],
  "wiki": [
    { "key": "wiki.repo",       "status": "info", "message": "repo: /abs/path" },
    { "key": "wiki.registered", "status": "ok",   "message": "registered as '...'" },
    { "key": "wiki.pages",      "status": "info", "message": "pages: 42" },
    { "key": "wiki.topics",     "status": "info", "message": "topics: 7" },
    { "key": "wiki.index",      "status": "info", "message": "index: rebuilt 2m ago" },
    { "key": "wiki.absorb",     "status": "info", "message": "last absorb: 1h ago (.absorb-<id>.log)" },
    { "key": "wiki.health",     "status": "ok",   "message": "almanac health reports 0 problems" }
  ]
}
```

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.

The report also includes an `## Updates` section (`updates: Check[]` in `--json`) with keys `update.status`, `update.last_check`, `update.notifier`, and `update.dismissed`. `update.status === "problem"` when a new version is available — mirrors the pre-command banner.

#### `almanac agents`

Provider-focused settings and readiness.

```bash
almanac agents list
almanac agents doctor
almanac agents use codex
almanac agents model claude claude-opus-4-6
almanac agents model claude --default
```

`agents use` writes the default provider; Codex is the built-in recommended default. `agents model` writes the provider-local model override; `--default`, `default`, or `null` resets the provider to its own default.

`init`, `absorb`, `sync`, `ingest`, and `garden` resolve provider settings in this order:

```text
--using flag > project config > user config > provider default
```

#### `almanac config`

Low-level scriptable settings surface.

```bash
almanac config list
almanac config list --show-origin
almanac config get agent.default
almanac config set auto_commit true
almanac config set agent.models.claude claude-opus-4-6
almanac config set --project agent.default codex
almanac config unset agent.models.claude
```

Supported keys: `update_notifier`, `auto_commit`, `agent.default`, `agent.models.claude`, `agent.models.codex`, `agent.models.cursor`, and `automation.sync_since`.
User config is stored at `~/.almanac/config.toml`. Project config lives at `.almanac/config.toml` and can override agent provider/model settings for a repo; `update_notifier`, `auto_commit`, and `automation.sync_since` remain user-level only because they control global side effects. Effective precedence is flag, environment, project config, user config, provider default.

#### `almanac update`

Upgrade command + the controls for the nag banner. See §11 for the full update-notifier architecture.

| Flag | Semantics |
|---|---|
| (none) | Run `npm i -g codealmanac@latest` synchronously in the foreground. Inherits stdio so you see npm's progress bar, permission prompts, and peer-dep warnings verbatim. |
| `--dismiss` | Mark the current `latest_version` as "don't nag". No install. Banner is suppressed until a newer version ships. |
| `--check` | Force a registry query now, bypassing the 24h cache. Prints the result. No install. |
| `--enable-notifier` | Deprecated alias for `almanac config set update_notifier true`. |
| `--disable-notifier` | Deprecated alias for `almanac config set update_notifier false`. |

**Exit codes:** `0` on successful install / check / dismiss / toggle. Install propagates npm's exit code on failure. `--check` exits `1` when the registry is unreachable.

**EACCES:** if `npm i -g` fails with a permission error, try `sudo npm i -g codealmanac@latest`, or switch to a version manager (nvm/volta/fnm) that doesn't require root. Almanac will never sudo on your behalf — silent privilege escalation would violate the trust contract.

### 1.5 `--stdin` pipe semantics

Commands that accept `--stdin`: `show`, `tag`, `health`.

- One slug per line; blank lines ignored; whitespace trimmed.
- Output order mirrors input order.
- Missing slugs don't abort the output — every found slug still gets printed, and each missing slug writes `almanac: no such page "<slug>"` to stderr.
- **Exit code reflects completeness.** `show --stdin` exits `1` if any slug was missing, `0` only when every requested slug resolved. This is what `xargs` + CI wants: a pipeline that silently drops slugs is a bug, not a feature. Use `|| true` or `; :` to continue past it when you genuinely don't care.
- `--stdin` must be explicit. No `isTTY` auto-detection (confusing under script redirection).

---

## 2. The unified `[[...]]` classifier

One syntax, four kinds. Rules applied in order:

1. **`:` before any `/`** → cross-wiki (`[[wiki:slug]]`)
2. **Contains `/`** → file (no trailing `/`) or folder (trailing `/`)
3. **Otherwise** → page slug wikilink

| Input | Classified | Why |
|---|---|---|
| `[[a:b/c]]` | xwiki `a`→`b/c` | colon before slash, rule 1 |
| `[[src/a:b]]` | file `src/a:b` | slash before colon, rule 2 |
| `[[./x]]` | file `x` | normalized; `./` stripped |
| `[[src/checkout/]]` | folder | trailing `/` |
| `[[foo\|display]]` | page `foo` | Obsidian pipe stripped |
| `[[  ]]` | null | empty after trim |

**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.

**Case sensitivity:** the indexer stores two forms per file/folder ref:
- `path` — lowercased, used for `--mentions` lookups (search is case-insensitive).
- `original_path` — as-written, used for filesystem `stat` in `health dead-refs` so case-sensitive filesystems (Linux, some Docker images) don't false-negative.

**Broken links** are recorded anyway (`wikilinks` table keeps the row), then surfaced by `health --broken-links`. Reindex is non-validating by design.

Cross-wiki refs live in their own table (`cross_wiki_links`), never lowercased.

---

## 3. Frontmatter schema

| Field | Type | Default | Purpose |
|---|---|---|---|
| `title` | string | H1 fallback | Display title. Missing → first H1 in body. |
| `topics` | string[] | `[]` | DAG tags. Kebab-cased on Absorb; duplicates collapsed. |
| `files` | string[] | `[]` | File/folder paths this page documents. Load-bearing for `--mentions`. Trailing `/` = folder. |
| `archived_at` | date / ISO string / epoch seconds | `null` | Non-null → excluded from default search. See §4. |
| `superseded_by` | slug | `null` | For archived pages: the active replacement. |
| `supersedes` | slug | `null` | For active pages: the archived predecessor. |

**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.

**Full example:**

```markdown
---
title: Checkout flow
topics: [flows, payments]
files:
  - src/checkout/handler.ts
  - src/checkout/
  - docker-compose.yml
archived_at: null
---

# Checkout flow

The flow starts at `src/checkout/handler.ts` when the browser POSTs
`/api/cart/submit`. The handler creates a Stripe PaymentIntent, writes an
inventory lock row to Supabase, returns the PI client secret. See
[[inventory-lock-gotcha]] for the deadlock we hit in March.
```

CRLF-terminated files are handled transparently — `show --body` strips frontmatter without leaving a stray `\r` at the body head.

---

## 4. Archive / lineage

Pages evolve in place. **Edit the existing page** when facts change — git history is the archive.

**Archive + supersede** is reserved for **fundamental reversals**: a central decision overturned, a system replaced wholesale, an incident re-opened.

**The test:** *is this an update to the old state, or a reversal of a central decision?* Update → edit. Reversal → archive + successor.

### Frontmatter shapes

Archived page:
```yaml
---
title: JWT sessions (archived)
topics: [auth, decisions]
archived_at: 2026-03-15
superseded_by: server-sessions
---
```

Successor:
```yaml
---
title: Server sessions
topics: [auth, decisions]
supersedes: jwt-sessions
files: [src/auth/session.ts, src/auth/middleware.ts]
---
```

Both files exist on disk. Both are indexed.

### Search scoping

- Default: active only.
- `--include-archive`: active + archived.
- `--archived`: archived only. Useful for retrospectives.

### Health exemptions for archived pages

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.

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.

---

## 5. DAG model and traversal

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.

```yaml
# topics.yaml
topics:
  auth:
    description: authentication, sessions, identity
    parents: []
  jwt:
    parents: [auth]
  sessions:
    parents: [auth]
  checkout:
    parents: [flows, payments]   # multi-parent
```

**`--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.

### Cycle prevention

Three layers:
1. **CHECK constraint** on `topic_parents` blocks self-loops (`child = parent`).
2. **Pre-insert traversal** walks parents upward before committing; refuses if `child` is reachable.
3. **Depth cap of 32** bails the traversal defensively. Real topic DAGs are ≤4 deep.

`almanac topics link A B` where A is already an ancestor of B fails: `almanac: link would create cycle: A → … → B → A`.

### Rename / delete side effects

`topics rename old new`:
1. Rewrite `topics.yaml` atomically (tmp + rename). New key written, old removed, parent edges migrated.
2. Rewrite every page whose `topics:` contains `old`. Body bytes preserved.
3. Reindex fires automatically on `topics.yaml` mtime bump.

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.

`topics delete slug`:
1. Remove from `topics.yaml`.
2. Untag every affected page.
3. **Does not cascade.** Children of the deleted topic become top-level. Run `almanac health --empty-topics` and re-parent or prune.

---

## 6. Shell-piping cookbook

Every command emits slugs one-per-line, so they compose.

**Find stale pages in a topic and tag them `review-needed`:**
```bash
almanac search --topic auth --stale 90d \
  | almanac tag --stdin review-needed
```

**Find pages referencing a just-deleted file:**
```bash
almanac search --mentions src/legacy/oauth.ts --include-archive
```

**Bulk move pages from an old topic to a new one:**
```bash
almanac topics create payments-v2 --parent payments
almanac search --topic old-payments | almanac tag --stdin payments-v2
almanac topics delete old-payments
```

**List pages that lack `files:` frontmatter for files they mention in prose:**
```bash
almanac search | while read slug; do
  info=$(almanac show "$slug" --json)
  prose=$(echo "$info" | jq -r '.file_refs[].path' | sort -u)
  fm=$(echo "$info" | jq -r '.files[]' | sort -u)
  missing=$(comm -23 <(echo "$prose") <(echo "$fm"))
  [ -n "$missing" ] && { echo "$slug:"; echo "$missing" | sed 's/^/  /'; }
done
```

**Open every orphan page in `$EDITOR`:**
```bash
almanac search --orphan | almanac show --stdin --path | xargs -n 1 "$EDITOR"
```

**Export a page's body to a standalone markdown file:**
```bash
almanac show checkout-flow --body > checkout-flow.md  # exactly one trailing \n
```

**Doctor a flaky install in CI:**
```bash
almanac doctor --json | jq '.install[] | select(.status == "problem")'
```

---

## 7. Scheduled sync

### Trigger

`almanac automation install` writes a macOS launchd job that periodically invokes `almanac sync`. The default cadence is `5h`; setup and automation install can change it with `--sync-every` / `--every`.

### What `almanac sync` does

1. Scan Claude and Codex transcript stores.
2. Ignore transcripts older than `automation.sync_since`, which setup records the first time sync is enabled.
3. Ignore transcripts whose mtime is still inside the quiet window.
4. Recover the transcript cwd/session metadata and map each transcript back to the nearest repo with `.almanac/`.
5. Reconcile `.almanac/jobs/sync-ledger.json` against background job records.
6. Start an ordinary background Absorb job only for new eligible material, with cursor guidance telling the agent where new lines begin.

The scheduler is only a wakeup mechanism. Sync owns transcript eligibility, dedupe, cursor state, and run enqueueing.

### `automation install | uninstall | status`

**`install`:**
- **Idempotent.** Twice -> one launchd plist, not two.
- **Records activation once.** The first install writes `automation.sync_since` in `~/.almanac/config.toml`; reinstalls preserve it.
- **Uses durable program arguments.** The plist invokes the absolute Node executable plus the resolved `dist/launcher.js` entrypoint, then `sync`.
- **Cleans legacy hooks privately.** Setup/install remove old CodeAlmanac-owned `almanac-capture.sh` commands from Claude/Codex/Cursor hook configs.

**`uninstall`:**
- Removes the scheduled sync plist.
- Also cleans legacy CodeAlmanac hook entries; unrelated user hooks stay.

**`status`:**
- Reports installed / not installed, plist path, schedule, and command. Non-interactive.

`almanac setup` installs CLI self-update and agent instructions by default. It installs sync/Garden automation only when explicit legacy sync/Garden setup flags are present. `almanac automation install` remains the normal manual path for recurring sync and Garden. `almanac uninstall` wraps `automation uninstall` alongside guide removal.

### Diagnosing "sync didn't run"

```bash
almanac doctor              # catch-all — reports automation state + last absorb age
almanac automation status   # scheduler entry
almanac sync status
almanac jobs
ls -lah .almanac/jobs/
```

Installed but no job: the scheduler has not reached its next wakeup, the transcript is still inside the quiet window, the transcript predates `automation.sync_since`, there is no new content past the ledger cursor, or no `.almanac/` exists upward from the transcript cwd (silent correct no-op).

### Diagnosing "sync ran but wrote nothing"

```bash
almanac jobs show <job-id>
almanac jobs logs <job-id>
```

Common causes:
- Provider auth is missing in the scheduler environment. `launchd` has a reduced environment; the installed plist preserves PATH for user-managed CLIs, but provider login still has to work headlessly.
- Transcript path didn't resolve or mapped to a repo without `.almanac/`. Sync reports skip reasons; use `sync status --json` for structured output.
- The writing agent found no durable wiki change.
- Session was pure-read with no decisions or discoveries. Correct no-op.

---

## 8. Multi-wiki model

### Registry at `~/.almanac/registry.json`

```json
{
  "wikis": [
    { "name": "openalmanac", "path": "/Users/me/code/openalmanac", "description": "…" },
    { "name": "codealmanac", "path": "/Users/me/code/codealmanac" }
  ]
}
```

### Registration paths

- **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.
- **`almanac init`** — creates and registers the wiki. `name` defaults to the repo basename; edit `~/.almanac/registry.json` if you need to rename it.
- **`almanac list --drop <name>`** — the only removal path. Skips auto-register so the removal isn't immediately undone.

### `--wiki <name>`

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 with a fix such as `run: almanac init`.

### Cross-wiki link resolution

`[[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.

### Unreachable targets

- Searches silently skip.
- `health --broken-xwiki` reports them.
- `show --wiki unreachable` exits `1` with a diagnostic.

---

## 9. Notability and writing conventions

The Build, Absorb, and Garden prompts enforce these during wiki-writing runs. Applying them yourself reduces rework.

### Patterns to avoid (bad → good)

**Significance inflation.**
- Bad: `The Stripe integration serves as a testament to our commitment to payment reliability.`
- Good: `The Stripe integration handles card payments. PaymentIntent is created at cart-submit; webhook confirmation completes the order.`

**Interpretive -ing clauses.**
- Bad: `The team migrated to async webhooks, highlighting their pragmatic approach.`
- Good: `The team migrated to async webhooks in March 2026 after the inventory-lock deadlock.`

**Vague attribution.**
- Bad: `Experts argue JWTs are unsuitable for sessions.`
- Good: `We moved off JWTs to server sessions in 2025 because refresh-token rotation required server state anyway.`

**Promotional language.**
- Bad: `Our groundbreaking approach delivers vibrant performance.`
- Good: `Rate limiting: sliding-window counter in Redis, 100 req / user / minute, in src/middleware/rate-limit.ts.`

**Hedging.**
- Bad: `While details are limited, it appears the cache might use LRU eviction.`
- Good: confirm from code, or cut the sentence.

**Empty evaluative sentences.** `This architecture is elegant and powerful.` → cut.

**Formulaic conclusions.** `In conclusion, the checkout flow demonstrates careful engineering.` → cut. Pages don't need conclusions.

### The four page shapes

**Entity** (a thing we depend on):
```yaml
---
title: Supabase
topics: [stack, database]
files: [src/lib/supabase.ts, backend/src/models/, docker-compose.yml]
---

# Supabase

PostgreSQL hosted on Supabase. Connection pooling via Supavisor. Client
singleton in src/lib/supabase.ts; backend models in backend/src/models/
use SQLAlchemy against the same DATABASE_URL (Doppler-managed).
```

**Decision** (a choice with tradeoffs):
```yaml
---
title: Server sessions (not JWTs)
topics: [auth, decisions]
supersedes: jwt-sessions
files: [src/auth/session.ts, src/auth/middleware.ts]
---

# Server sessions

We use server-side sessions, not JWTs. Session state lives in Redis, keyed
by a rotating cookie. Chosen because refresh-token rotation already required
server state for the revocation list, removing the main perceived benefit
of stateless JWTs.
```

**Flow** (a multi-file process):
```yaml
---
title: Checkout flow
topics: [flows, payments]
files: [src/checkout/, src/api/cart/submit.ts, backend/src/services/orders.py]
---

# Checkout flow

The browser POSTs /api/cart/submit. The handler creates a Stripe
PaymentIntent and an inventory lock row in orders (status=pending). Client
confirms the PaymentIntent. Stripe's webhook flips status=paid and releases
the lock.
```

**Gotcha** (something that bit us):
```yaml
---
title: Inventory-lock deadlock
topics: [gotchas, payments]
files: [backend/src/services/orders.py]
---

# Inventory-lock deadlock

Before March 2026, the Stripe webhook acquired the same row lock the
checkout path held. When Stripe retried a delayed webhook during a new
checkout for the same SKU, the two transactions deadlocked; Postgres killed
one, usually the webhook, leaving orders silently stuck in pending.
```

### General principles

- Every sentence contains a specific fact. If the sentence could describe any codebase, cut it.
- Neutral tone. `is`, not `serves as`.
- No speculation. "I don't know why X" is fine as an explicit note; a guess is not.
- Prose first. Bullets for genuine lists. Tables for structured comparison only.
- Reference code with `[[...]]`. Inline mentions are fine but only `[[...]]` gets indexed.
- List files in frontmatter. Pages about specific code need `files: [...]` to surface in `--mentions`.

---

## 10. Troubleshooting

### Catch-all: `almanac doctor`

When something feels off and you don't know where to start, run `almanac doctor`. It reports install state (binary, native binding, provider readiness, scheduled automation, guides, import line) and current-wiki state (registered, page/topic counts, index freshness, last absorb age, health problems). Every problem comes with a one-line `run: ...` fix. `--json` for scripting.

### "better-sqlite3 bindings missing"
Node version / arch mismatch with the prebuilt binary. `almanac doctor` reports it as `install.sqlite: problem` with the underlying error's first line. Fix:
```bash
npm rebuild better-sqlite3   # in the install directory
```
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`).

### "search returns nothing"

Two different outcomes to distinguish:
- **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.
- **An actual error on stderr.** Commander or `almanac:` prefix. That's a broken invocation; re-read the `--help`.

`--json` is silent on stderr — the `[]` array is the unambiguous empty signal.

### "pages don't show up in `--mentions`"

Missing `files:` frontmatter, OR path referenced only in inline prose (not via `[[...]]`). Inline prose isn't indexed. If neither: `almanac reindex`.

### "topics missing after rename"

`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`.

### "sync didn't run"

```bash
almanac doctor              # reports automation state + last absorb age + auth
almanac automation status   # scheduler installed?
almanac sync status --json
almanac jobs
ls -lah .almanac/jobs/
```

No jobs at all -> automation may be uninstalled, the scheduler has not reached its next interval, the transcript is still inside the quiet window, the transcript predates `automation.sync_since`, or the transcript maps to no repo with `.almanac/`. If automation is missing, `almanac doctor` reports `install.automation: problem` with `run: almanac automation install`.

### "slug collision warnings"

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.

### "dead-refs reports files that exist"

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`.

### Forensics files

- `.almanac/jobs/<job-id>.json` — Almanac job record with status, provider, model, timings, log path, and failure metadata.
- `.almanac/jobs/<job-id>.jsonl` — provider event log for the job. Read with `almanac jobs logs <job-id>`.
- `.almanac/jobs/sync-ledger.json` — sync cursor and pending-job state used to dedupe scheduled absorbs.
- `.almanac/jobs/sync.lock` — repo-local sync lock used to avoid overlapping sync enqueue races.

---

## 11. Updates: nag banner + manual install

Tier B design, per the pair review. No silent auto-install. The CLI self-schedules a detached background version check after each command; the next command's banner reflects what that check learned. Installing is always a foreground action the user chooses.

### How it works

```
command runs
   │
   ├─ pre:  announceUpdateIfAvailable()   ── reads ~/.almanac/update-state.json (sync)
   │                                         prints stderr banner if outdated
   │
   ├─ (command does its thing)
   │
   └─ post: spawn --internal-check-updates (detached, stdio: ignore)
                                            queries registry.npmjs.org
                                            writes ~/.almanac/update-state.json
                                            exits
```

### State files

- **`~/.almanac/update-state.json`** — written by the background worker + `almanac update`. Shape: `{last_check_at, installed_version, latest_version, dismissed_versions[], last_fetch_failed_at?}`. `last_check_at` is epoch seconds; `dismissed_versions` is a list of version strings the user muted via `--dismiss`. Missing / malformed → all read paths return defaults. Never break the CLI.
- **`~/.almanac/config.toml`** — contains `update_notifier` plus user-level agent defaults. Toggles whether the banner ever prints. Default `true`. Flip via `almanac config set update_notifier true` / `false`. Legacy `~/.almanac/config.json` is migrated on normal config reads.

### Cache behavior

- Background check runs at most once per 24h (comparing `last_check_at` to now).
- `almanac update --check` bypasses the cache for immediate status.
- Failed registry fetches bump `last_check_at` anyway so a one-shot failure doesn't hammer the registry on every subsequent invocation. The 24h cycle resumes as normal.
- Network timeout is 3s; longer would be noticeable in the background spawn.

### Dismissal semantics

`almanac update --dismiss` appends the current `latest_version` to `dismissed_versions[]`. The banner suppresses **only that specific version** — when a newer one ships, `latest_version` moves on and the banner reappears. Multiple consecutive dismissals accumulate; `dismissed_versions` is never trimmed automatically. If you find yourself dismissing every release, `almanac config set update_notifier false` is the right tool instead.

Dismissal does NOT prevent `almanac update` from installing. It only silences the banner.

### Test / CI gating

The post-command background spawn is gated on `process.env.NODE_ENV !== "test"`, `!process.env.VITEST`, and `!process.env.CODEALMANAC_SKIP_UPDATE_CHECK`. Test suites never fork update workers; CI pipelines that want to opt out set `CODEALMANAC_SKIP_UPDATE_CHECK=1`.

### Why not auto-install

From the pair review:
- Silent install without consent violates trust norms; users expect to choose when their tooling changes under them.
- `npm i -g` prefixes diverge across nvm, volta, fnm, and system Node; guessing wrong corrupts the PATH.
- Detached-child survival is fragile in Claude Code subprocesses, CI runners, and Windows — a mid-install kill leaves a half-linked binary.
- Mid-invocation binary swap breaks `import()` of dist chunks that haven't yet been loaded.

The manual `almanac update` in the foreground resolves all of these: user sees what's running, npm picks its own prefix, the child is owned by the user's shell, and the running process doesn't touch its own files.

---

## When in doubt

- `almanac --help` / `almanac <command> --help` — flags are always current for the installed build.
- `almanac doctor` — one command that reports everything relevant about install + current wiki.
- `.almanac/README.md` in the repo — the notability bar and topic taxonomy for *this* repo override anything here.
