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:
- Query —
search,show,health,list,serve - Edit —
tag,untag,migrate,topics ... - Wiki lifecycle —
init,absorb,sync,ingest,garden,jobs,automation ...,reindex - 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.--jsonemits an array of{slug, title, description, page_count}.page_countexcludes archived pages (matchestopics show).parents/childrenare only ontopics show <slug> --json; use that when you need DAG edges.almanac topics show <slug>— description, parents, children, pages.--descendantsincludes 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.linkis cycle-checked (§5).unlinkis idempotent.almanac topics rename <old> <new>— rewritestopics.yamlfirst (atomic tmp+rename), then every affected page'stopics:frontmatter. YAML-first so a mid-pass crash leaves the graph, not the pages, as the source of truth.almanac topics delete <slug>— removes fromtopics.yaml, untags every affected page. Does not cascade to children — orphaned children become top-level. Runalmanac healthto 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/.
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:
{
"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.
almanac agents list
almanac agents doctor
almanac agents use codex
almanac agents model claude claude-opus-4-6
almanac agents model claude --defaultagents 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:
--using flag > project config > user config > provider defaultalmanac config#
Low-level scriptable settings surface.
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.claudeSupported 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 --stdinexits1if any slug was missing,0only when every requested slug resolved. This is whatxargs+ CI wants: a pipeline that silently drops slugs is a bug, not a feature. Use|| trueor; :to continue past it when you genuinely don't care. --stdinmust be explicit. NoisTTYauto-detection (confusing under script redirection).
2. The unified [[...]] classifier#
One syntax, four kinds. Rules applied in order:
:before any/→ cross-wiki ([[wiki:slug]])- Contains
/→ file (no trailing/) or folder (trailing/) - 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--mentionslookups (search is case-insensitive).original_path— as-written, used for filesystemstatinhealth dead-refsso 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:
---
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:
---
title: JWT sessions (archived)
topics: [auth, decisions]
archived_at: 2026-03-15
superseded_by: server-sessions
---Successor:
---
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.
# 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:
- CHECK constraint on
topic_parentsblocks self-loops (child = parent). - Pre-insert traversal walks parents upward before committing; refuses if
childis reachable. - 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:
- Rewrite
topics.yamlatomically (tmp + rename). New key written, old removed, parent edges migrated. - Rewrite every page whose
topics:containsold. Body bytes preserved. - Reindex fires automatically on
topics.yamlmtime 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:
- Remove from
topics.yaml. - Untag every affected page.
- Does not cascade. Children of the deleted topic become top-level. Run
almanac health --empty-topicsand 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:
almanac search --topic auth --stale 90d \
| almanac tag --stdin review-neededFind pages referencing a just-deleted file:
almanac search --mentions src/legacy/oauth.ts --include-archiveBulk move pages from an old topic to a new one:
almanac topics create payments-v2 --parent payments
almanac search --topic old-payments | almanac tag --stdin payments-v2
almanac topics delete old-paymentsList pages that lack files: frontmatter for files they mention in prose:
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/^/ /'; }
doneOpen every orphan page in $EDITOR:
almanac search --orphan | almanac show --stdin --path | xargs -n 1 "$EDITOR"Export a page's body to a standalone markdown file:
almanac show checkout-flow --body > checkout-flow.md # exactly one trailing \nDoctor a flaky install in CI:
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#
- Scan Claude and Codex transcript stores.
- Ignore transcripts older than
automation.sync_since, which setup records the first time sync is enabled. - Ignore transcripts whose mtime is still inside the quiet window.
- Recover the transcript cwd/session metadata and map each transcript back to the nearest repo with
.almanac/. - Reconcile
.almanac/jobs/sync-ledger.jsonagainst background job records. - 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_sincein~/.almanac/config.toml; reinstalls preserve it. - Uses durable program arguments. The plist invokes the absolute Node executable plus the resolved
dist/launcher.jsentrypoint, thensync. - Cleans legacy hooks privately. Setup/install remove old CodeAlmanac-owned
almanac-capture.shcommands 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"#
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"#
almanac jobs show <job-id>
almanac jobs logs <job-id>Common causes:
- Provider auth is missing in the scheduler environment.
launchdhas 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; usesync status --jsonfor 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#
{
"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) callsautoRegisterIfNeededon cwd. A repo with.almanac/but no registry entry → added withname = basename(cwd), no description. Makes "cloned a repo with.almanac/committed" just work. almanac init— creates and registers the wiki.namedefaults to the repo basename; edit~/.almanac/registry.jsonif 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-xwikireports them.show --wiki unreachableexits1with 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):
---
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):
---
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):
---
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):
---
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, notserves 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:
npm rebuild better-sqlite3 # in the install directoryOn 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"#
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 withalmanac 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_atis epoch seconds;dismissed_versionsis a list of version strings the user muted via--dismiss. Missing / malformed → all read paths return defaults. Never break the CLI.~/.almanac/config.toml— containsupdate_notifierplus user-level agent defaults. Toggles whether the banner ever prints. Defaulttrue. Flip viaalmanac config set update_notifier true/false. Legacy~/.almanac/config.jsonis migrated on normal config reads.
Cache behavior#
- Background check runs at most once per 24h (comparing
last_check_atto now). almanac update --checkbypasses the cache for immediate status.- Failed registry fetches bump
last_check_atanyway 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 -gprefixes 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.mdin the repo — the notability bar and topic taxonomy for this repo override anything here.