# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`CITADEL_MODEL_CONTEXT_TOKENS` — state your model's context, get a chunk budget that fits it.**
  Large-source chunking was governed by one number, `CITADEL_MAX_SOURCE_CHARS`, whose 300 000-char
  default assumes a roomy hosted model. Against a **local** backend that assumption fails in a
  particularly expensive way: a 55-page PDF extracts to well under the threshold, so it plans a
  *single* pass, the session's context fills up part-way through, and the run dies — and because
  resume checkpoints only exist for sources that chunk, nothing was ever banked, so the next attempt
  starts from zero. Lowering the char threshold works, but it is the wrong unit (an operator knows
  their `n_ctx`, not a character count) and the wrong scope (it applies to every source in every
  workspace). The new knob is stated in **tokens** and derives the budget: the per-pass source window
  is capped at 10% of the stated context at ~4 chars/token, floored at 8 000 chars so the segment
  count can never run away. The fraction is deliberately small, because the source is only one claim
  on a session's window — every session also reads the rulebook (~12–13k tokens before a byte of
  source), searches the wiki, reads and writes pages, and carries every tool result along. The two
  knobs compose by `min()`, so `MAX_SOURCE_CHARS` remains the hard ceiling and `=0` there still means
  "never chunk": a stated context is a budget, never an override. Unset — the default — nothing
  changes at all. The second half of the bug fixes itself on the way: a source that now chunks is a
  source that now **checkpoints**, so an interrupted import continues at the segment it died on
  instead of re-buying the ones before it. `citadel doctor` gained a *chunk budget* line that shows
  what the two knobs actually resolve to, and WARNs when a stated context is small enough that the
  segment floor had to clamp it.
- **Segment edges no longer silently drop a fact they cut in half.** Segments are packed by whole
  paragraphs or whole lines — never mid-line — but never by *meaning*, so a sentence, table row, or
  list item can begin in one window and finish in the next; the rules said to ingest only what the
  segment contains and not to invent continuations, which is right about fabrication and wrong about
  loss. For the sources where the prepared file holds the WHOLE text and the window is only a line
  range (PDF extractions and transcripts), the rules now say the cut unit may be read past the edge
  to be seen whole, while ownership stays put: fold it in only if it **begins** inside your window,
  cite the range it actually occupies even where that crosses the edge, and leave a unit that began
  earlier to the pass that owned it. A physically sliced segment file is unchanged — there, the
  slice really is all there is. This matters more now that a stated model context makes segment
  boundaries more frequent.

- **`CITADEL_INCLUDE_PATTERNS` — an allowlist for discovery ("in `raw/`, read only `.pdf` and
  `.txt`").** Deciding what citadel reads was a one-sided affair: `CITADEL_IGNORE_PATTERNS` could
  name what to keep out, so a raw root that mostly holds files you do *not* want folded in had to be
  described backwards — every unwanted extension enumerated, and a new one silently ingested until
  someone noticed. The new knob says it forwards instead: when set, a file is discovered only if its
  **name** matches one of its case-insensitive globs. Written the way people say it — `.pdf`, `.txt`
  — or as real globs (`*.pdf`, `report-*.md`) or exact names (`notes.md`); a bare `pdf` is read as
  both `*.pdf` and the literal name, because `Makefile` is also a real filename. Filtered files cost
  exactly what an ignore match costs: skipped from the walk's own `stat`, never opened, never
  hashed, never recorded in the manifest or the failures catalog. The two lists compose as
  **deny-beats-allow** (whitelisting `*.db` does not resurrect `Thumbs.db`), and the escapes that
  matter stay open — an explicitly named path (`citadel ingest one-off.docx`) is ingested regardless,
  directories and git-repo sources are never matched (a repo is folded in as one digest, not as its
  files), and narrowing the allowlist over an existing corpus is **not** a deletion: the sweep
  confirms every candidate with `.exists()`, so excluded sources keep their pages and simply stop
  being re-checked.
- **What an allowlist filters out is visible, not inferred.** A filter that quietly empties the
  corpus is the one way this knob fails, and it fails looking like success — ingest finds no sources
  and reports a clean run. So the exclusion is reported at all three layers: a `Not included` section
  on the run report (and a stderr note naming the patterns in force), a `Not included` bucket in
  `citadel status` (`not_included` in `--json`, always present; the table row is hidden when no
  allowlist is configured), and a new `citadel doctor` **include patterns** check that says how many
  files the allowlist admits versus filters out — and WARNs when it admits nothing while files are
  there to admit, naming the usual causes (a typo, or a path-shaped pattern like `reports/*.pdf`
  when matching is per file name).

### Fixed

- **A session that deletes its own staging copy now fails with the real story.** A weak local model
  was observed inventing a "publish" step at the end of a long session: it copied its finished
  pages out of the per-source staging copy into the **live** wiki, deleted the staging directory as
  "done", and exited 0. Ingest then saw an empty staging tree next to a live wiki with pages and
  failed the source with the anti-emptying valve's generic *"refusing to promote: the session left
  the wiki with no content pages"* — technically true, entirely misleading, and in the variant where
  the agent deletes staging *without* touching the live wiki the same run would have been a
  **silent zero-change success** (money spent, nothing imported, source stamped done). A vanished
  staging directory is now detected right after the session, before any diff is taken — by an
  identity sentinel carrying a random, process-local token — not a bare existence check — so a
  deleted-then-recreated (or symlink-replaced) directory cannot impersonate the copy this run
  made, even by re-creating the sentinel file — and fails the source with its own reason —
  *"the staging wiki copy vanished or was replaced mid-session"* — that also names
  any files that appeared in the live wiki outside the staging discipline (serial runs only, where
  no concurrent promote can legitimately explain them). Those files are deliberately left in place
  for inspection: they were paid for, but they were **never validated by this run**, and the reason
  says so. The session prompt gained the matching instruction — the wiki directory the agent is
  given is where its work *stays*: there is no publish step, never move or copy its contents
  elsewhere, never delete or rename the directory itself. A session that destroys its staging and
  then dies (timeout, non-zero exit) reports both stories — the session's own error and the
  vanished staging behind it.
- **Transcript log filenames keep the source's basename.** The per-session transcript's filename
  label was truncated from the right at 80 characters, so for a source on a network share —
  `pdf.//host/share/very/deep/tree/Report.pdf` — everything recognizable was cut off and the log
  was named after the *middle of the directory tree*. The label is now truncated in the middle
  (head + tail kept), so both the kind/segment prefix and the file's own name survive; the 80-char
  cap itself (a Windows path-length guard) is unchanged.
- **The `+` in a pattern list is understood wherever you write it.** `CITADEL_IGNORE_PATTERNS`'
  extend-marker was only honored in front of the *whole* value, so the equally natural
  `+*.bak,+~backup*` — one `+` per entry, the way a list of independent additions reads — left the
  stray `+` inside every pattern but the first, and those then matched only filenames literally
  starting with `+`. Nothing said so. The marker now counts wherever it appears and is stripped from
  every entry that carries it; a pattern that must match a real leading `+` writes it as a character
  class (`[+]draft.md`), which fnmatch already reads as a literal.
- **A `.env` key assigned twice no longer loses its second half in silence.** The format has no
  multi-line values, so one line is one setting and the first assignment wins — reasonable, but
  invisible exactly where it bites: a list of globs reads like something you extend line by line.
  Repeated keys are now recorded as config warnings, which `citadel doctor`'s config check prints,
  and both the `.env` template and `docs/configuration.md` state the rule (one line, comma-separated)
  next to the settings it applies to.

- **`citadel curate --guidance "…" [paths…]` — tell curate what you want changed.** The offline
  detectors find structural problems, but some things only a person sees: pages named
  inconsistently, a family that belongs in `registries/`, a split that reads better the other way
  round. None of them trip a detector, so there was no way to ask for them short of hand-editing
  the wiki. The curate lifecycle now takes the same steer ingest does (`--guidance TEXT`, env
  `CITADEL_CURATE_GUIDANCE`, the shared 2000-char cap and the `--guidance ""` convention), and
  `citadel curate` gained the positional **paths** the API always supported — a page, a **folder**
  (`registries/`), or a **glob** (`registries/machine-*.md`). Two things make it useful rather than
  merely present: the steer travels in each cluster's **findings checklist** — curate's own
  instruction channel — so it lands as a listed finding instead of a second instruction competing
  with the improve-or-NOOP rule; and pages named under a steer are **planned even when every
  detector considers them clean** (the new `operator_guidance` reason code, ordered first: a human
  ask outranks a detection). A steer with no paths only rides along on the detector plan — fanning
  one out over a clean wiki would be one paid session per page — and a path that matches nothing is
  reported instead of silently doing nothing. The bound is explicit in the rules
  (`tasks/curate.md` § Operator guidance): a steer directs **structure** — naming, titles,
  `type`/folder routing, splits, merges, section order, cross-links — and can never change what the
  wiki says. No fact is added, dropped, reworded away from what its source states, or
  re-attributed; every fact keeps its `[^sN]` marker and its `## Sources` definition through
  whatever the steer moves, and the cluster's cited raw sources are re-read first, so a retitle
  follows the spelling the sources themselves use.
- **Each guided cluster knows which other pages the same steer is being carried out on.** A
  structural ask is routinely one instruction over many pages ("key these by product, not by
  project" re-pivots a whole folder), but curate runs one session per cluster — so without this each
  session answered the same instruction blind to the other N-1, which is how one re-pivot becomes
  three near-duplicate product pages. A guided cluster's findings now name the run's other guided
  anchors (elided past 20) and say they run in sequence, so a page one session creates is extended
  by the next instead of re-invented.
- **A curate cluster can no longer promote a broken cross-link.** Renaming and merging pages is what
  curate is for — and a `--guidance` steer makes renames the common case — but a rename is the one
  edit that reliably strands inbound links, and the mechanical rename-repair net only fires when the
  page KEEPS its title, which a *retitle* by definition does not. So a steered rename used to
  promote cleanly and leave the wiki carrying a dangling link until someone ran `citadel lint`.
  Every cluster session now runs under a no-new-broken-links post-condition (ingest's existing
  `extra_check` seam, so it is checked against staging BEFORE the promote): a cluster whose edit
  leaves a cross-link pointing at a page that does not exist is failed and rolled back whole.
  Compared by dangling target against a per-cluster baseline of the live wiki, never by
  `(page, target)` pair — a cluster moves pages, so a link that was already dangling would look
  brand new the moment its page is renamed, and inherited damage never fails a cluster.
- **Sessions may consult a bounded number of RELATED sources** (`CITADEL_RELATED_SOURCES`, default
  3; `0` turns it off). Sources arrive one at a time; knowledge does not. A fault report saying
  "HAL-7 down since March" is nearly worthless while the machine register that defines HAL-7 sits
  unread two files away — and the fact that joins them ("the Hallenkran in bay 7 has been out of
  service since March") is exactly what the wiki exists for. A source-reading session is now told a
  per-session budget of OTHER raw files it may open to resolve something its own source uses but
  never explains, and `core.md` § Related sources bounds the lookup: only for a concrete unexplained
  term the wiki's pages do not already resolve; the candidate is found **through the wiki** (the
  page that explains it names its raw file in `## Sources`); the fewest, most promising files, then
  stop at the budget — never a sweep of the raw tree; anything taken **cites the file it came
  from** (a fact resting on two sources cites both), never the source of record's marker; and the
  consulted source is not folded in wholesale, since it has its own ingest session. The budget joins
  the resume checkpoint's identity, so a chunked source interrupted under one budget is never
  continued under another.
- **`citadel ingest --guidance "…"` — steer a run's sessions with one sentence.** The rules tree
  says how the wiki is built in general, and the workspace `rules/local.md` holds permanent house
  rules — but there was no way to hand ONE run a steer like "create one machine registry from the
  maintenance lists" or "focus on the 2026 figures" without editing a rules file and remembering
  to revert it. `--guidance TEXT` (env: `CITADEL_INGEST_GUIDANCE`; MCP: the new optional
  `guidance` argument on `wiki_ingest`, scoped to that one call) appends the text to each
  source-reading session's prompt as an operator-guidance bullet. Guidance steers routing and
  emphasis WITHIN the rules — the prompt says explicitly that it never overrides the citation,
  grounding, or off-limits rules — and it never reaches delete-cleanup or curate sessions, which
  have no source to steer. Capped at 2000 chars on the EFFECTIVE steer, flag or env alike (the
  prompt must stay argv-safe; anything longer belongs in `rules/local.md`), collapsed to one line
  (the prompt bullet is line-shaped), `--guidance ""` disables an env-set steer for one run (the
  MCP argument mirrors that: omitted lets the env steer stand, `""` disables it, and calls are
  serialized so one caller's steer can never bleed into another's run), and the steer joins the
  resume checkpoint's identity so a chunked source interrupted under one guidance is never
  continued under another.

### Changed

- **All three supported CLIs are now recognized as vision-capable backends.** Nothing in code ever
  gated image reading on the backend — `CITADEL_IMAGE_SUPPORT` and `CITADEL_PDF_MODE=images` run
  the same session whatever CLI is configured — but `citadel doctor` warned on *any* non-claude
  backend that images mode "may silently ingest PDF text only", and the rules/docs said images mode
  needs the claude CLI. Verified in practice: copilot and agy render PDF pages and PPTX-embedded
  images visually just like claude (given only a file path, copilot reads figures straight out of a
  PDF page). `doctor` now keeps a known-vision set (claude, copilot, agy) and warns only for a
  backend outside it; `formats/pdf.md`, the troubleshooting guide, and the verify-corpus protocols
  name all three. An unrecognized custom CLI still WARNs — against a reader that cannot render
  pixels, images mode silently degrades to text-only, which is exactly what the warning exists to
  catch — and a proxy-redirected **local model** still needs to be vision-capable itself (the
  `docs/configuration.md` caveat; doctor cannot see through the CLI to the model).

- **Registry creation triggers more reliably.** The `Registry` rules (shipped in 0.6.0) fired
  cleanly on sources that *are* enumerations, but three gaps let real-world registries silently not
  happen: an enumeration **embedded** in a prose source (a table, an appendix, a price list at the
  end of an offer) could miss the genre judgment because the source as a whole doesn't "read like"
  a catalog; the ingest task brief never asked the enumeration question at all; and reconcile's
  "keep the existing genre treatment" read as if a NEW appendix in a changed source may not become
  a registry. The rules tree now closes all three, with no code change: `core.md` § Genres states
  that a genre can match a *part* of the source, `tasks/ingest.md` gains an explicit
  **enumeration check** (list-shaped sections are registry material; a mentioned keyed entity is
  looked up in `registries/` first), `tasks/reconcile.md` clarifies that treatment-keeping covers
  existing content while newly added content gets fresh genre judgment, and `genres/registry.md`
  spells out the section-level trigger plus a new **"Any source maintains the rows"** section —
  a status change, a new member, or a promotion can come from any source that mentions a keyed
  entity, so rows stop forking into loose facts on topic pages.

- **A failed deletion cleanup now says how to repair what it left behind.** When a cleanup session
  cannot strip a removed source's provenance, the run reports which pages still cite the vanished
  file — but the footer under `Errors:` only offered the generic "it stays in the failures catalog
  and is retried next run". That is not the whole story for this one failure: unlike every other
  failed source (which is rolled back, leaving the wiki untouched), a failed cleanup leaves a real
  defect behind — a dangling `[^sN]` to a file that no longer exists — and retrying the source does
  not help while the agent is the thing that is broken. The report now adds a line naming the
  offline tools that see and fix it: `citadel lint` (which lists it under *Fabricated/missing
  sources* and exits 3) and `citadel curate`. Shown once per run however many cleanups failed, and
  only for that failure kind.

- **`citadel ingest --reingest <paths>` — re-import an already-ingested source from scratch.**
  `--force` runs a reconcile, which deliberately keeps the source's existing treatment (genre,
  page structure) and only verifies/updates its facts — so a source first ingested under an old
  rulebook or a weak model never gets re-thought, and "all ok, nothing new" is the correct
  reconcile verdict even when a fresh read would organize it completely differently. `--reingest`
  is the escape hatch: for each named tracked source it first runs a `kind="delete"` cleanup
  session that strips the source's previous facts from the wiki (dropping its manifest entry),
  then — in the same run, deletions always run first — ingests it as a brand-new source under the
  current model, rules, and wiki state (fresh cross-links included). This replaces the manual
  move-the-file-out / full ingest / move-it-back / ingest dance. A failed cleanup refuses the
  fresh session for that source (nothing is written on top of the old facts), tracked repos take
  the first-time `repo` brief over a full digest, and the flag requires explicit paths like
  `--force` (exit 2 without them; `--force` and `--retry` refuse to combine with it). The
  `tasks/delete.md` brief now covers the still-on-disk cleanup case.

### Fixed

- **One invalid page no longer fails every source that touches it.** Ingest validates each page an
  agent session changed and rolls the whole source back on any error — which was blaming a source
  for damage it inherited. If a page is *already* invalid when a session touches it (a failed
  deletion cleanup left a dangling `[^sN]` behind, a raw file moved out from under a `resource`, a
  hand edit landed), the next source that merely appends a fact to that page failed validation, was
  rolled back in full, and failed again on every subsequent run: the session was paid for, nothing
  was kept, and every source whose facts belonged on that page was stuck until a human found and
  fixed it by hand. One broken page could take a whole corpus down. Validation now compares against
  the page as the source *found* it: an error whose exact `(category, detail)` was already there is
  not this source's damage — it is reported as a run warning (*pre-existing problems on pages this
  run touched*) instead of failing the source. Every other error still fails it, so nothing a
  session actually breaks can reach the live wiki. The same carve-out applies to the resume-replay
  guard, so a poisoned page can no longer cost a chunked source its checkpoint (and with it every
  paid segment).

### Added

- **`CITADEL_STALL_LIMIT` — stop a run whose agent has stopped working.** An agent CLI can fail in a
  way that does not look like failure: it self-updates mid-run into a build that cannot launch its
  own tools, a permission is revoked, a sandbox denies every subprocess. The session still exits
  `0`, still reports the tokens it spent, and still returns an empty diff — so every existing guard
  passes and the run bills a session for every remaining source while producing nothing. After 3
  **consecutive** sources come back having changed no page at all (configurable; `0` disables), the
  run now stops dispatching, reports why (*STOPPED EARLY*, with what to check), and leaves the
  un-attempted sources pending for a plain re-run. Only sources that were expected to change
  something count — a fresh source, or a deletion cleanup planned because something still cites the
  source; a reconcile's "nothing changed" is a real verdict (what a healthy `citadel refresh` slice
  looks like) and never counts, and any source that does change something resets the counter.
  Applies to `--jobs N` runs too: queued sources are cancelled and in-flight ones finish and are
  recorded normally.

### Changed

- **Forced/refresh reconciles now re-read with fresh eyes, not just re-verify.** The
  `tasks/reconcile.md` brief (driving `ingest --force` and every `citadel refresh` session) now
  explicitly instructs the agent that a re-read is more than verification: mine the unchanged
  source for wiki-worthy facts the first ingest missed, cross-link against today's wiki (pages
  that did not exist back then may now be the right home or neighbor), and strip claims the
  source never actually supported — while still keeping the existing genre treatment (structure
  churn stays `--reingest`'s job).

## [0.6.0] - 2026-07-30

### Changed (breaking)

- **The `gemini` backend is replaced by `agy` (Google's Antigravity CLI).** The Gemini CLI it
  wrapped no longer exists, so `CITADEL_LLM_CLI=gemini` now fails immediately with a migration
  hint instead of shelling out to a missing binary (`citadel doctor` FAILs on it too). The new
  backend runs `agy -p <prompt> --output-format stream-json --dangerously-skip-permissions`, grants
  out-of-workspace directories with a repeatable `--add-dir`, and reports its token usage through
  the closing `result` event — the `--session-summary` stats-file machinery is gone with the CLI it
  probed for. Rename `GEMINI_CLI_PATH` to `AGY_CLI_PATH` if you set it.
- **`CITADEL_INGEST_MODEL` now applies to every backend, and defaults to unset.** It used to be a
  claude-only knob hard-defaulted to `sonnet`, inert on the others (`citadel doctor` actively warned
  about it). `claude`, `copilot` and `agy` all honor `--model`, so it is passed through to all
  three; leaving it unset runs the CLI's own default model, and the per-backend `COPILOT_MODEL` /
  `GEMINI_MODEL` guesswork is gone (the model recorded per source is now the one the backend
  *reported*). Set it explicitly if you were relying on the old implicit `sonnet`.

### Added

- **Zero-page and failed sources are visible — and retryable with one command.** A brand-new
  source whose session "succeeds" with zero page changes used to be marked done and never looked
  at again, indistinguishable from a healthy ingest. Now: the run report carries a WARNING section
  (`IngestReport.no_pages`) plus a `log.md` line for each such source, the live progress renders
  its `no changes` verdict in yellow, and `citadel status` marks every ingested source that NO
  wiki page cites with `NO PAGES (nothing cites this source)` (also `"uncited"` in `--json`),
  ending with a retry hint whenever anything is stuck. The new **`citadel ingest --retry`**
  re-runs the whole stuck set without naming paths: every failed source still on disk (errored /
  timed-out / unreadable — deliberate `duplicate` skips excluded) plus every ingested-but-uncited
  source as a forced reconcile. It prints the computed set first, refuses `--force`/explicit
  paths, and exits 0 cleanly when there is nothing to retry. The failed-run report also points at
  the retry lane from its Errors section.
- **`citadel doctor` warns when `CITADEL_INGEST_MODEL` shadows `COPILOT_MODEL`.** The copilot
  backend also reads its own `COPILOT_MODEL` env var, and the `--model` flag citadel passes
  overrides it. A user who points copilot at a BYOK provider (`COPILOT_PROVIDER_BASE_URL`, a
  local Ollama-style endpoint) and names the model in `COPILOT_MODEL` — while a stale
  `CITADEL_INGEST_MODEL` in `.env` still names a model that provider does not serve — got an
  instant per-source failure with no hint at the cause. The ingest-model check now WARNs when
  both are set and disagree, naming the shadowing and the provider that must serve the
  requested model.

- **`Registry` pages — enumerable sources are captured completely.** A source that is (or
  contains) a uniform enumeration of like entries — a machine/asset inventory, an error-code
  catalog, a customer roster, a product/price list — now folds into ONE `type: Registry` page
  (new folder `wiki/registries/`): one cited row per entry with the finest available locator,
  instead of a representative subset with the tail compressed into a single footnote. The
  granularity floor is unchanged — an entry earns its own page (of its own kind) only once it
  carries several independent cited facts, and its registry row becomes a link + one-line gloss.
  The behavior lives in the rules tree (new `genres/registry.md` brief, a `Registry` row +
  decision-procedure step + `## Registries` contract in `schema.md`, floor/essence carve-outs in
  `core.md`); code-side the type is routed by `okf.folder_for_type`, and curate's over-length
  guidance is now type-aware — an oversized registry splits by entry range/subclass into sibling
  registries, never along topics. Known limit: a list source over `CITADEL_MAX_SOURCE_CHARS`
  (~300k chars) is chunked through segment files whose line numbers rebase, so per-row
  `lines A-B` locators of such oversized lists are not offline-verifiable yet.
- **The model a source was imported with is now the model that actually ran.** Each backend's own
  session envelope is parsed for the serving model — claude's `modelUsage` map (the PRIMARY entry
  by token volume, so the small model claude routes side work to never wins), copilot's JSONL
  `assistant.message`/`model.call_start` events, agy's opening `init` event — and that id is what
  gets stamped into the manifest as `<cli>:<model>`. The configured label is only a fallback for a
  backend that named nothing (an Ollama/proxy setup, or agy left on its own default), so the wiki
  can no longer claim a model that never served it.
- **copilot spend is recorded in AI credits — and priced.** copilot quotes no dollars, so its
  `totalNanoAiu` counter (the `N AIC used` figure its interactive session footer shows;
  1 AIC = 1e9 nanoAiu) is stamped as a new `aic` field, and converted to `cost_usd` at GitHub's
  fixed published $0.01/credit — so a mixed claude+copilot corpus finally has ONE comparable cost
  total, with the un-derived credits always shown beside the dollars (`$0.0251 (2.5083 AIC)`).
  `citadel status` gained a `Recorded AI credits` total and a `--json` `aic_total`. The retired
  `premiumRequests` counter is deliberately not recorded — GitHub replaced it with credits, and it
  was far too coarse to describe a session (every ingest is "1"). copilot also runs under
  `--output-format json` now — `-s` trimmed away the very envelope that names the model and counts
  the spend.
- **An authentication failure under hermetic isolation now retries once without it.** On machines
  where the CLI's credentials live in exactly the personal configuration `--bare` skips (managed
  containers, an `apiKeyHelper`, some WSL setups), *every* session failed with "Not logged in"
  until you found `CITADEL_HERMETIC=0` — while the same CLI worked fine interactively. Isolation is
  a hardening nicety; ingesting at all is the product. The retry is scoped to that exact signature
  (an isolation flag really was passed **and** the message is auth-shaped), so a genuinely
  logged-out CLI still fails instead of looping.
- **Full provenance under every source in `citadel view` and `sources/index.md`.** The viewer's
  source reader and hover popover show the model, what the import cost (dollars or premium
  requests), the token split and the last-checked date; the generated sources catalog grew matching
  `Cost` / `Tokens` / `Checked` columns (`—` where the backend reported nothing — an unknown figure
  is never rendered as a measured zero).
- **The ingest console is now a live `rich` display, and it shows what each source cost.** On a
  terminal the run renders a live region — one spinner row per IN-FLIGHT source plus an overall
  bar — while finished sources scroll away above it as permanent verdict lines that now carry the
  spend and the model that actually ran (`[2/3] OK raw/notes.md 18.4s 2 created $0.0123
  1.2k in / 456 out claude-opus-5`; only fields the backend genuinely reported appear). This also
  repairs `--jobs N`, which used to switch the animation OFF entirely: a single
  carriage-return-rewritten line could name only one source, so several in flight had nothing to
  show. `rich` joins `mcp`/`pyyaml`/`pypdf` as a runtime dependency (pure-Python, two small
  transitive packages). ASCII-only output and the ASCII spinner frames are preserved deliberately,
  so a legacy Windows code page can still encode every line, and console failures are still
  swallowed — output must never break a run that already spent money.

- **`CITADEL_MAX_SOURCE_BYTES` — a discovery size ceiling.** Ignore patterns matched *names*;
  nothing matched **size**, so a raw root that also holds machine data (the reported case: 617
  `.tdms` sensor dumps, 10.6 GB in one folder) was stream-hashed in full on every first scan just to
  end up recorded as unreadable binary. A file over the ceiling is now skipped from the walk's own
  `stat` — never opened, never hashed, never written to the manifest or the failures catalog — and
  *reported*: a stderr NOTE, an **Oversized** section on the run report, and an **Oversized** bucket
  in `citadel status` (`--json` included). Off by default (`0` = no limit), because silently
  dropping a large-but-legitimate source — a 2 GB lecture recording, a scanned archive PDF — would
  be worse than a slow scan. Explicitly named paths bypass it, and an already-ingested source that
  later crosses the ceiling stays in the wiki; it just stops being re-checked (the deletion sweep's
  positive `.exists()` confirmation is what keeps it from reading as vanished).
- **Two `citadel doctor` checks**: **wiki placement** (WARNs when the wiki dir sits inside a walked
  raw root) and **child paths** (names the working directory agent sessions will actually run in,
  WARNing when only a UNC spelling of the workspace is known).

### Fixed

- **A failed copilot session now reports its actual error, not JSONL stream noise.** copilot
  exits non-zero with an *empty* stderr; the message that names the fix — e.g. a BYOK
  (Ollama-style) provider that does not serve the configured `CITADEL_INGEST_MODEL`
  (*"Model '…' not found on provider at http://… (HTTP 404)"*) — travels as a `session.error`
  event near the *end* of its `--output-format json` stdout stream, while the stream *opens* with
  ephemeral MCP/skills status events. The failure reason citadel records (the run report, the
  failures catalog, `citadel status`) was the first 500 characters of raw stdout — exactly that
  opening noise — so the real error only surfaced in a `--log-dir` transcript. The `session.error`
  message(s) are now extracted and preferred (collapsed onto one line, duplicates folded), which
  also lets an auth-shaped copilot failure reach the hermetic-retry detection; a stream naming no
  `session.error` keeps the old stderr-then-stdout truncation.

- **A long absolute source key no longer floods the ingest console.** `config.display_key` collapsed
  a path to `raw/<below>` only when it matched a configured root as a string — so a Windows drive
  letter mapped to a share (root configured as `T:\proj\raw`, key arriving as
  `//fileserver.corp.example/proj/raw/...`) matched nothing and printed the whole UNC path, FQDN
  host and all, on every line. It now falls back to cutting at the last path segment *named* like a
  configured root, and finally to a `.../`-marked tail clip for a key under no known root at all.
  Verdict lines additionally clip the key from the LEFT (keeping the identifying filename) rather
  than clipping the line from the right, so a narrow terminal never drops the cost and model at the
  end of the line.

- **Explicitly requested ingest paths now expand `~`.** Every other configured path already did
  (`config._resolve_dir_entry`, `workspace.init`, `CITADEL_WORKSPACE`); the `citadel ingest
  <paths…>` / `wiki_ingest` arguments were the outlier. A POSIX shell expands `~` before citadel
  sees it, but Windows `cmd.exe` — and PowerShell, for a native binary's arguments — does not, so
  `citadel ingest ~/ws/raw/notes.md` arrived as a literal `~` directory and stat'ed away to
  nothing. Expansion uses `os.path.expanduser`, not `Path.expanduser`, which raises on an
  unresolvable home (`~nosuchuser`) — discovery must never raise on user input.
- **The wiki can no longer become one of its own raw sources.** With a raw root *above* the wiki
  (`CITADEL_RAW_DIRS=T:\` and the wiki at `T:\llmWiki\data-science\wiki` — a normal way to say "scan
  this whole drive"), discovery walked the wiki's generated pages back in as sources, run after run,
  each pass citing the last; the only defense was a hand-written `CITADEL_IGNORE_PATTERNS` entry.
  Discovery now prunes the wiki directory out of every walk (lexically, so it costs nothing per
  entry and never blocks on a dead mount), refuses a raw root that *is* the wiki without arming the
  deletion sweep for it, refuses an explicitly named path inside the wiki, and announces the
  exclusion once per run. The run-start migration sweep additionally clears any page an earlier run
  had already self-ingested out of the manifest and the failures catalog, so `wiki/sources/index.md`
  stops carrying them.
- **Agent sessions no longer run with a UNC working directory on Windows mapped drives.**
  `Path.resolve()` rewrites `T:\team-wiki` into `\\fileserver\share\team-wiki` regardless of which
  spelling the `.env` used, and that resolved path was what citadel passed as the agent CLI's `cwd`
  — where the copilot backend refuses to work at all ("environment blocks UNC/network paths") and
  git treats the spelling as a separate repository (`safe.directory` / `core.filemode`). The
  resolved form remains the one identity everywhere (manifest keys, root containment, staging vs.
  live); what child processes are handed — the agent CLI's `cwd` and its `--add-dir`/
  `--include-directories` grants, and git's `-C` — is now the **non-resolved** spelling citadel
  already held (the `.env` value, or the drive the process was launched from), and every path
  UNDER an aliased directory inherits it — which is what reaches the *derived* paths, the common
  case: with a workspace on `T:\`, the wiki is just `WORKSPACE_ROOT / "wiki"` with no
  `CITADEL_WIKI_DIR` to spell it out, and it is exactly what git gets as `-C`. Nothing is guessed:
  an alias is recorded only where both spellings of one directory are known *and* resolution turned
  a non-UNC path into a UNC one, so POSIX layouts and ordinary Windows paths are byte-for-byte
  unchanged.

## [0.5.0] - 2026-07-25

### Added

- **Bounded parallel ingest — `citadel ingest --jobs N`** (the 2026-07 audit's backlog #11, and
  with it its finding 1.2.5). Ingest was strictly serial because the per-source staging redirect
  *assigned* `config.WIKI_DIR` and `os.environ` in place: one process could only ever be inside one
  redirect. That redirect is now context-local (a `ContextVar` behind `config.wiki_dir()` and
  friends; child processes get their wiki through an explicit per-spawn env), which is what lets N
  sources stage at once — the isolation primitive, a per-source staging copy, was already there.
  `--jobs N` (or `CITADEL_JOBS`) folds in that many sources concurrently. **Every guarantee is
  unmoved**: one promote per source, all-or-nothing, nothing partial on the live wiki, the manifest
  still saved per completed source. What is shared is serialized rather than raced — one lock guards
  the two moments that touch the live wiki (cloning it, promoting onto it), while the minutes-long
  sessions run fully in parallel; the promote is *base-aware*, pruning only what its own clone had
  and its staging lacks, so a concurrent source's new page is never deleted as "the agent removed
  it"; and a promote whose pages have moved since the clone is refused **before** it writes
  anything, with that source re-run serially at the end of the run, where it sees the winner's page
  and merges into it (the report lists these under *Re-run serially* — the one place parallelism
  costs a session a serial run would not have). Report, manifest, and failures bookkeeping stay on
  the main thread, so those writes are as single-threaded as they ever were, and an interrupt
  cancels queued sources while still recording work already promoted. The default stays **1** —
  serial, line for line as before — because the real cost is not safety but **cross-linking**:
  concurrent sessions cannot see each other's new pages. Best on a large backlog of unrelated
  sources (see docs/recipes.md), and available on `citadel refresh --jobs N` too — a refresh slice is ordered by last-checked time rather than by topic, so its sources are usually unrelated; `curate` remains serial by design. Chunked sources needed one more
  rule, found by an adversarial cross-review and reproduced offline before it was fixed: a resume
  checkpoint's delta is measured against the wiki its source was **cloned from**, never against a
  live wiki that has since moved on. Measured against live, a page a concurrent source *created*
  read as a deletion this source had made, and one it *rewrote* read as this source's change
  carrying stale bytes — and because a checkpoint is durable, that delta outlived the parallel run
  and could prune a fully-ingested source's page in a later, even strictly serial, run. The base
  state a replay is guarded against now comes from that same snapshot, so a page another source
  touched fails the guard instead of passing it.
- **`citadel serve --http` — the opt-in Streamable HTTP transport** (the 2026-07 audit's backlog
  #12, closing the last open item of its § 3.1 MCP-surface gap). Stdio remains the default and is
  unchanged; `--http` serves the SAME thirteen tools, four prompts, and `wiki://` resources over
  MCP's Streamable HTTP transport, so a client that does not run on your machine — claude.ai, a
  phone through a tunnel — can reach the wiki. Binding a port is the only network surface citadel
  has, so the posture is strict rather than convenient: a **bearer token is mandatory**
  (`CITADEL_HTTP_TOKEN`, at least 16 characters — the server refuses to start without one, and
  there is no unauthenticated mode), it is checked in a small ASGI wrapper *before* the MCP session
  layer (constant-time compare, 401 + `WWW-Authenticate`, identical answer for a missing and a
  wrong token, so an unauthenticated caller cannot even open a session), the bind defaults to
  loopback (`CITADEL_HTTP_HOST`, a public bind warns — this transport is plain HTTP and belongs
  behind a TLS-terminating tunnel: cloudflared, tailscale, `ssh -R`), and the SDK's DNS-rebinding
  protection is enabled against the bound host so a web page cannot drive the server through your
  browser: a request's `Host` must be a name the server accepts — derived from the bind address, or
  named in `CITADEL_HTTP_ALLOWED_HOSTS` for the deployments where the client addresses something
  else (a tunnel forwards its own hostname; a wildcard `0.0.0.0` bind is a name nobody sends, so it
  refuses to start until the knob is set rather than rejecting every request it gets) — and a
  request carrying a browser `Origin` is refused unless `CITADEL_HTTP_ALLOWED_ORIGINS` admits it,
  since the MCP clients this serves are not browsers. `--read-only` (`CITADEL_HTTP_READ_ONLY=1`) disables the two writers — `wiki_capture` and
  `wiki_ingest`, the latter of which spawns your coding-agent CLI on the serving host — while
  leaving the eleven readers and the *advertised* tool list untouched, so a client's tool list never
  silently changes shape. `--host`/`--port`/`--path` mirror the env knobs; the same flags on a stdio
  run are a usage error rather than a silently ignored setting. No new dependency (starlette and
  uvicorn already ship with `mcp`), no OAuth machinery (citadel has no user model — the token is the
  user), and `citadel doctor` gained an "HTTP serve" line that reports where the server *would*
  listen, whether the writers are exposed, and warns about a too-short token or a public bind before
  you ever start it.

- **Resumable chunked ingest** (the 2026-07 audit's backlog #9): a large source folded in over
  several segments no longer throws away the earlier segments' paid agent work when a run dies at
  segment N. Each completed segment banks the delta it produced — the *promote that would have
  happened*, computed with the promote's own file-level view, so link repairs and any non-page file
  are in it — as a checkpoint in `.citadel_resume/` beside the wiki (new `citadel/resume.py`,
  `CITADEL_RESUME`, on by default). The next run replays that delta into its fresh staging copy and
  opens at segment N; even a promote that failed *after* the last segment now replays for free,
  with no session at all. **Promotion is unchanged**: still exactly once, still only after the last
  segment, and a failure still leaves the live wiki byte-for-byte as it was — the guarantee did not
  move, only the bill. Every reuse is guarded offline before a single token is spent — identity
  (source sha256, ingest model, rules version, the segment plan's *content*, and the prompt-shaping
  knobs `rules_version` does not cover: wiki language, style profiles, PDF/image mode), blob
  integrity, per-page base state in the live wiki (so a page another source changed in between is
  never clobbered and a banked deletion never destroys newer work), re-validation of the replayed
  pages plus a no-new-broken-links check, an attempt cap so a deterministically failing segment
  cannot wedge a source into failing cheaply forever, and run-lock ownership so a stalled run never
  writes over the run that reclaimed its lock. **Every guard failure falls back to a full restart
  at segment 1 in the same run** — the pre-resume behavior is the floor, never the failure mode.
  Cost accounting stays honest on both sides: the run report counts only what that run spent, while
  the manifest stamps the whole cost of importing the source across the runs that paid it (so
  `citadel status` never under-reports a resumed source). `citadel doctor` lists what is waiting.
  Deviation from the audit's scoped design (`claude --resume` session IDs): checkpoints are
  backend-agnostic — copilot/gemini expose no session-resume flag, and a resumed conversation would
  anyway find the staging tree it edited deleted; replaying the delta keeps the work instead of
  re-running it. No rules change was needed — a resumed segment sees exactly the wiki an unbroken
  run's segment would have seen.

- **Sync & scheduling recipes** (the 2026-07 audit's backlog #13 + #14, docs-only). New
  `docs/recipes.md`, linked from the docs hub: multi-device sync under the one-writer-many-readers
  rule (wiki-history git + a private remote as the recommended lane, whole-workspace git with the
  exact ignore list, sync services scoped to capture-inbox + read-only mirrors, three
  phone-reading paths incl. the one-file viewer), and scheduled runs (why unattended runs are safe
  by design; the canonical nightly-`ingest`/weekly-`refresh --min-age-days` pair; concrete cron,
  systemd-user-timer, and Windows Task Scheduler setups; plus the "smart cron" lane — a scheduled
  agent session that runs the commands and triages the report). No code changes.

- **Tests for the two riskiest hand-rolled surfaces** (the 2026-07 audit's backlog #8). The
  222-line CFBF (OLE2) binary parser in `citadel/extract_ole.py` — previously exercised for real
  only by the kontor corpus, which needs a live LLM run — now has a spec-shaped CFBF *writer*
  fixture in `tests/test_extract_ole.py` pinning every container path offline: mini-FAT/
  mini-stream storage, multi-sector streams and directory chains, a DIFAT sector chain past the
  header's 109 slots, the BIFF5 `Book` fallback and `Workbook` preference, and the corruption
  guards (FAT and mini-FAT cycles terminate, out-of-file chains / truncation / a bad sector
  shift / a rootless container all degrade to whole-file salvage, never a crash). And the
  ~2k-line offline-viewer `app.js` — previously asserted on only as a built HTML string — gains
  a headless-Chromium smoke test (`tests/test_viewer_browser.py`): boot + sidebar render,
  search filter/recover, hash-routed page open, the citation hover popover, and `/`-to-search
  keyboard focus, with zero JS page errors asserted throughout. The browser test is opt-in (a
  new `browser` dependency group; it self-skips without playwright, so the default suite stays
  offline and browser-free) and runs in CI as the dedicated ubuntu `viewer-smoke` job;
  `CITADEL_TEST_BROWSER` points it at a pre-provisioned chromium build where downloads are
  unavailable.

- **Hermetic agent sessions + an honest model knob** (the 2026-07 audit's backlog #6). Ingest
  sessions now run **hermetically by default**: when the installed backend CLI advertises a
  session-isolation flag (claude `--bare` — skip user hooks/`CLAUDE.md`/MCP discovery), it is
  appended to every agent invocation, so a user's personal `~/.claude` configuration no longer
  leaks into citadel's sessions — they run on the rules tree alone, reproducibly. The flag is
  feature-probed per binary against `--help` with an exact flag-token match (the same
  discipline as gemini's `--session-summary` probe), so an older CLI without the flag runs
  byte-for-byte as before; `CITADEL_HERMETIC=0` deliberately re-admits the personal config.
  copilot/gemini document no equivalent flag today, so nothing is passed there. And `citadel
  doctor` gained the honest-knob advisory: a WARN when `CITADEL_INGEST_MODEL` is explicitly set
  while the backend is copilot/gemini — only the claude backend is passed `--model`, so the knob
  selects nothing there; the warning names the backend's own env var (`COPILOT_MODEL` /
  `GEMINI_MODEL`) as the real selector.

- **MCP prompts + resources** (the 2026-07 audit's backlog #4). Beyond its thirteen tools,
  `citadel serve` now publishes four workflow **prompts** — `wiki_answer` (answer strictly from
  the cited wiki via the orient → search → read → cite ladder), `wiki_verify` (resolve one page's
  every `[^sN]` citation through `wiki_raw` and report supported/unsupported per fact),
  `wiki_capture_note` (record ONE attributed statement via `wiki_capture`), and `wiki_health`
  (`wiki_status` + `wiki_lint` + the single next maintenance action) — surfaced by clients as
  slash-command-like entries, so the recommended tool flows are invokable instead of narrated.
  The wiki's documents are also addressable as `wiki://` **resources**: `wiki://index`,
  `wiki://sources`, and `wiki://tags` mirror their tool twins byte-identically, and one
  `wiki://page/{folder}/{name}` template serves any page's full uncapped text. Resources share
  the tools' never-raise contract (a missing page or unsafe path reads back as an `error: …`
  body, never a crashed server); subscribe/`listChanged` notifications are deliberately omitted
  — the wiki only changes through staged ingest/curate runs, so re-reading after `wiki_ingest`
  is the refresh model. Pure `citadel/server.py` additions; every tool, its annotations, and the
  CLI twins are unchanged.

- **PDF text-layer pre-pass** (the 2026-07 audit's backlog #10 — the PDF half of its
  "locators into PDF/Office/image sources are never offline-verified" finding).
  [pypdf](https://pypi.org/project/pypdf/) ships as a **bundled runtime dependency** (PDFs are one
  of the commonest `raw/` source classes, so the offline-verifiable path is on by default — pypdf
  is pure-Python, BSD, with no transitive runtime deps; `pip install cite-citadel[pdf]` stays a
  no-op compat alias). With the new `CITADEL_PDF_TEXT` knob (default `auto` = on when pypdf
  imports), a genuine PDF (`%PDF-` magic) ingests through a new `citadel/pdftext.py`
  seam: its embedded text layer is extracted ONCE per content into a `[p. N]`-page-marked,
  line-stable text the agent reads under new `pdf`/`pdf-reconcile` kinds while citing the
  original `.pdf` — so PDF citations carry real `lines A-B` locators instead of agent-trusted
  `p. N` ones. The extraction is cached content-addressed in `.citadel_pdftext/` beside the wiki
  (pruned by sha when the source is deleted or its bytes change, guarded so byte-identical
  siblings keep the shared entry) and doubles as the offline
  verification text: `citadel lint`/curate verify PDF line locators against it, `wiki_raw`/
  `citadel raw` serve and slice it, and the viewer embeds it ("Text layer extracted from the
  original PDF"). Large PDFs now chunk correctly — line windows over ONE full extraction (the
  audio contract: numbering never rebases) instead of being unchunkable. Everything is
  best-effort by design: `CITADEL_PDF_TEXT=0`, a scanned/encrypted/corrupt PDF
  (or any extraction failure) falls back per source to the pre-existing agent-native read —
  the pre-pass can never fail a session — and `p. N` page locators stay legal for the fallback
  and for figure-only facts in `CITADEL_PDF_MODE=images` (which still has the agent open the
  original PDF for figures on top of the extraction). `citadel doctor` gained a "PDF text"
  advisory (WARN only if the bundled pypdf was force-removed); `formats/pdf.md` teaches
  both setups.

- **Conversational capture bridge** (the 2026-07 audit's backlog #5 — the last of the audit's
  three named access-side gaps). A new `wiki_capture` MCP tool (the server's 13th tool, its
  second mutating one) and its CLI twin `citadel capture <text> [--from WHO] [--topic T]`
  (`-` reads stdin) append ONE dated, attributed note from a conversation to a monthly capture
  log under the primary raw root (`raw/captures/YYYY-MM.md`, created with an explanatory
  header; the append is atomic and serialized by a per-log cross-process lock, so two
  concurrent captures can never silently drop each other's note). The log is an ordinary raw
  SOURCE — capture never touches the wiki: the next
  ingest/`wiki_ingest` folds the entry in through the normal staged, validated lifecycle with
  real `[^sN]` line-locator citations (the tool reports the appended line range — the future
  locator), and a later append changes the log's sha so the run after it RECONCILES
  (update-don't-re-append). Captured statements enter the wiki as attributed claims ("X said Y
  on DATE"), never as bare facts; empty and oversized texts (>100k chars — a whole transcript
  belongs in raw/ as its own file) are refused loudly, and a log outside the configured
  `CITADEL_RAW_DIRS` walk roots is flagged with the explicit-ingest hint instead of silently
  never being discovered. The new [docs/capture.md](docs/capture.md) documents both capture
  lanes: the tool for single notes, and the save-the-transcript-as-a-file convention
  (`raw/conversations/…`) for whole conversations, which the existing chat/transcript genre
  briefs already govern.
- **Transcript ingest for audio/video sources via a whisper-CLI seam** (the 2026-07 audit's
  backlog #3). With `CITADEL_AUDIO_SUPPORT=1` (opt-in, default off), a recognized recording
  (`.mp3`/`.wav`/`.m4a`/`.mp4`/`.mkv`/…, extension AND magic bytes) is transcribed ONCE through a
  local whisper-class CLI — a shell-out seam exactly like the agent CLI, no SDK and no API key
  (`CITADEL_WHISPER_CLI`, openai-whisper flag convention; whisper-ctranslate2 and mlx_whisper are
  drop-in compatible; `CITADEL_WHISPER_MODEL` and a separate `CITADEL_WHISPER_TIMEOUT` tune it) —
  and folded in by the new `audio`/`audio-reconcile` session kinds reading the `[HH:MM:SS]`-stamped
  transcript under the new `formats/transcripts.md` brief: cite the ORIGINAL media file, `lines
  A-B` locators, unlabeled voices stay roles, mis-heard names/numbers stay hedged. The transcript
  is cached content-addressed (source sha256) in `.citadel_transcripts/` next to the wiki dir —
  transcription runs at most once per content, a failed session's retry is free, and the cache IS
  the offline verification text: `citadel lint` verifies audio-citation locators against it, and
  `wiki_raw`/`citadel raw` and the offline viewer serve it (a class PDFs still lack). Long
  recordings chunk through the existing multi-pass folding; a missing/failing whisper binary is a
  retryable per-source error (never a permanent unreadable mark), and `citadel doctor` gained an
  `audio` line warning when the knob is on but the binary is missing.
  Follow-up hardening (same unreleased feature): a chunked recording's passes now share ONE
  full-transcript file with a per-pass line window instead of rebased slices, so `lines A-B`
  locators from any pass match the verification cache by construction; the run-lock staleness
  window accounts for whisper time (plus an extra heartbeat after transcription) so a long
  transcription can never let a second run reclaim a live lock; deleting or re-recording a
  source prunes its now-orphaned cached transcript (plaintext spoken content); lint hashes a
  cited recording once per run instead of once per citing page, and `wiki_raw` serves the
  transcript keyed by the manifest's recorded sha — the content the citations were built from —
  without re-hashing the media file.
- **Per-session LLM cost accounting** (the 2026-07 audit's backlog #2). Every agent session's
  cost/usage is now captured from the backend CLI's own report — claude's `--output-format json`
  result envelope (`total_cost_usd` + token counts, cache traffic included) and gemini's
  `--session-summary` stats file (token counts; the flag is appended only when the installed
  binary advertises it in `--help`, so an older gemini is never handed an unknown flag) — summed
  per source (a chunked source's segments combined) and stamped into its manifest entry
  (`cost_usd`/`tokens_in`/`tokens_out`, carried across moves and cache re-stamps exactly like
  `ingested_at`: only a real agent session ever refreshes it). Surfaced wherever the money went:
  ingest/refresh reports and `citadel curate` print an `LLM usage:` run total (failed and NOOP
  sessions included — that spend was real too), and `citadel status` gains a per-source cost
  column plus a `Recorded LLM cost` corpus total (`--json`: `cost_usd_total`). Accounting is
  strictly passive: parsing is defensive, no usage path can fail a session, and "no data" stays
  `None` (copilot reports nothing machine-readable) — never a fabricated `$0.00`. `refresh
  --limit`'s budget unit stays sources, but each run now tells you what the slice actually cost.
- **`citadel refresh` — budget-controlled re-verification of the least-recently-checked sources
  (the third lifecycle beside ingest and curate).** Every successful agent session now stamps its
  source's manifest entry with an `ingested_at` last-checked time (carried unchanged across moves
  and cache re-stamps — it only ever means "a model last verified this source then"). `refresh`
  orders the manifest by that stamp — oldest first, a never-stamped entry counting as oldest — and
  hands the first `--limit N` sources (default 1; each source = one agent session, so N *is* the
  per-run budget) to a forced path-scoped ingest: one `reconcile` session per source under the
  **current** model + rules, all-or-nothing staging, re-stamped on success so the source rotates to
  the back of the queue and repeated runs walk the corpus round-robin with no persisted queue. The
  point: a wiki that outlives its models never needs regenerating — after a model upgrade you spend
  a self-chosen slice of the monthly token allowance re-checking the oldest imports instead.
  `--dry-run` prints the head of the queue with zero sessions; `--min-age-days D` makes a scheduled
  run self-limiting (a no-op once everything was checked within D days); `citadel status` now shows
  each ingested source's `checked YYYY-MM-DD` date.
- **Cloud-placeholder detection for unreadable sources.** A file whose bytes read as 100% NUL —
  the signature of a Dropbox/OneDrive "online-only" placeholder seen through WSL or a network
  share — is now reported with a targeted hint (*"reads as all NUL bytes - likely a cloud-only
  placeholder; make it available offline"*) in the run report, `citadel status`, `log.md`, and the
  `sources/index.md` failures table, instead of the generic "no extractable text" message. Unlike a
  genuine binary it is **never stat-cached as done**: hydration restores the real content without
  changing size or mtime, so a placeholder stays visibly stuck across runs and ingests
  automatically once made available offline (previously the stat quick check could skip the
  hydrated file forever).

### Changed

- **`citadel serve` stops re-reading the whole wiki on every call** (the 2026-07 audit's backlog
  #15, closing its finding 1.2.6 and the remainder of the § 1.3 retrieval assessment). An MCP
  server lives for a whole client session, but every read tool re-walked and re-parsed the entire
  corpus per call — at 1000 pages that is ~0.7 s before a single result is scored, and a
  `wiki_search` cost ~1.4 s. A new `citadel/pagecache.py` keeps the last load in memory and
  re-validates it on **every** consult with a stat-only `os.scandir` walk (~4 ms at 1000 pages)
  over exactly the files `load()` parses; search additionally memoizes that snapshot's per-page
  term-frequency tables. Measured at 1000 pages: `load()` ~700 ms → ~9 ms, `wiki_search` ~1.4 s →
  ~50 ms, `tag:`/`type:` queries ~630 ms → ~11 ms. **Nothing is persisted** — the wiki stays the
  database, there is no index file to go stale, and the filesystem still answers "did anything
  change?" on every call. Staleness is designed out rather than hoped away: the fingerprint is
  taken before AND after the load and must match (a page rewritten mid-load is never cached), a
  snapshot whose newest stamp is younger than a 2 s settle window is not stored at all (a
  coarse-timestamp filesystem could hide a same-length same-tick rewrite), the single slot is keyed
  by wiki directory (ingest's staging redirect can never be served the live wiki, and the endless
  stream of staging dirs cannot accumulate), `write_page`/`delete_page` invalidate directly, and
  `ingest()`/`curate()` wear `@pagecache.bypass` so the staged diff-by-hash always reads the truth
  from disk. Off by default: `citadel serve` opts in (`CITADEL_PAGE_CACHE=auto`), `1` enables it
  wherever citadel reads the wiki, `0` restores the pre-cache behavior everywhere.
- **Ranked BM25 search behind the unchanged `search()` seam** (the 2026-07 audit's backlog #1).
  Queries now share the offline viewer's grammar — bare terms are AND-matched (English stopwords
  exempt, so "how do you brew coffee" matches on *brew coffee*; a query no page fully matches is
  retried once as OR so the closest pages still surface), and `tag:x` / `type:y` tokens filter
  instead of match (tag by prefix, type exactly; an operator-only query like `type:person` lists
  the filtered pages) — closing the audit's "two divergent search implementations" finding.
  Ranking is real BM25 (term-frequency saturation, prose-field length normalization,
  Lucene-smoothed IDF) over the title 3.0 / aliases 2.5 / tags 2.0 / description 1.5 / body 1.0
  field ladder, plus the exact-phrase bonus, computed in memory per call — no persisted index,
  the wiki stays the database, zero new dependencies. The audit-scoped SQLite FTS5 route was
  built first and rejected on measurement: FTS5's `bm25()` clamps the IDF of any term appearing
  in more than half the corpus to ~0, so in a topical wiki the topic word ("coffee" in a coffee
  wiki) degenerated every score to noise; the Python scorer keeps IDF strictly positive, and
  "how do you brew coffee" now ranks the brewing page first instead of losing it entirely.
  Signature, MCP surface, and the `pages=` tag-filter seam are unchanged.

### Fixed

- **An auth-shaped session failure on a hermetic run now names the knob that caused it.** Hermetic
  sessions (`CITADEL_HERMETIC=1`, the default) append claude's `--bare`, which deliberately skips
  the user's personal agent configuration — and on machines where that configuration is also where
  the CLI keeps its **credentials** (a managed container, a devcontainer, an `apiKeyHelper` in
  `~/.claude/settings.json`), every session then failed on authentication while the same CLI worked
  perfectly when run by hand. The backend reports that as *"Authentication error · This may be a
  temporary network issue, please try again"*, which sends you hunting a network problem that does
  not exist; nothing in the error, the report, or the failures catalog mentioned hermetic mode.
  Auth-shaped failures raised from a run that really passed an isolation flag (read off the argv, so
  the probe-gated case where nothing was passed is untouched) now carry a one-line hint naming
  `CITADEL_HERMETIC=0`, and `docs/troubleshooting.md` gained the symptom with the fix. Found while
  live-testing parallel ingest on this release: all four sources failed identically, and the
  message pointed at the wrong layer.

## [0.4.0] - 2026-07-16

### Changed

- **All nine committed showcase wikis rebuilt under the current rulebook** (sonnet, in-place per
  the verify-corpus regeneration recipe — leuchtfeuer's three-wave replay, clockwork's two-commit
  repo protocol, gazette in images mode, kontor with image support, flurfunk with style profiles,
  pemberley's three chunked segment passes). The refreshed wikis carry the recent rule improvements
  visibly: plan-vs-actual figures co-stated in one cited sentence (kontor's 142-vs-138), embedded
  Office chart images read and cited with slide locators, and workplace-chat threads recorded per
  the new `genres/chat.md` (live fact = thread end state with a dated arc). Every wiki re-graded
  against its hidden ground truth — all nine commit-worthy, `check`/`lint` clean.

### Added

- **New Office-documents test corpus — `kontor` — with a committed, images-mode showcase wiki (the ninth corpus).** Eleven files generated deterministically by a committed stdlib-only script `make_office.py` (no python-pptx/docx/openpyxl/PIL): OOXML `.pptx` (slides + speaker notes + an embedded chart), `.docx` (paragraphs + a table), and `.xlsx` (two sheets), plus legacy OLE `.doc`/`.ppt`/`.xls` (97–2003 compound files), a dedup pair, and OS/Office junk. First end-to-end coverage of the Office extraction path — `citadel/extract.py` (OOXML) and `citadel/extract_ole.py` (CFBF salvage) — with a new **image delta**: the chart's `GROSS MARGIN 34.2%` lives only in pixels, so it is **absent-and-honest** with `CITADEL_IMAGE_SUPPORT=0` and **present-and-cited** with images on. Also exercises `CITADEL_DEDUP_BY_BASENAME` (a `.doc` twin skipped for its `.docx`) and `CITADEL_IGNORE_PATTERNS` (Thumbs.db/desktop.ini/lock files skipped), and plants the usual discriminative judgment traps (a 142-vs-138 contradiction, a tentative Lisbon warehouse, entity-variance merges, a near-miss ≠ contradiction, a dated supersession). Fully fictional (Aldervik Kontor) — nothing copied.
- **New PDF test corpus — `gazette` — with a committed, images-mode showcase wiki.** Five sources (four PDFs generated deterministically by a committed stdlib-only script
  `make_pdfs.py`, plus a markdown control) exercise the PDF path end-to-end for the first time: the
  `%PDF-` magic-sniff dispatch, `formats/pdf.md`, page locators, and — the headline — the
  `CITADEL_PDF_MODE` **text-vs-images** delta. A key number (`0.42 arcsec`) lives only inside a chart
  raster and a whole notice page is image-only, so both are **absent-and-honest** in text mode and
  **present-and-cited** in images mode; the corpus also grades the academic-publications genre and
  that a preprint's bibliography never materializes as fabricated raw sources. The PDFs report real
  public-domain science (tardigrade cryptobiosis, the 2007 FOTON-M3 orbit experiment) in original
  prose — nothing copied.
- **Two new test corpora — `clockwork` and `flurfunk` — with committed, graded showcase wikis.** `clockwork` is the first corpus whose source is a whole **git repository**,
  folded in as ONE digest (`CITADEL_REPO_SUPPORT`, `kind=repo`), then re-ingested after a second
  commit changes a documented default (`max_retries` 3 → 5) — the first end-to-end coverage of
  `kind=repo`/`repo-reconcile`, folder-keyed provenance, and a `type: System` page for an external
  store. Its committed inputs are the `repo-src/` + `repo-src-wave2/` trees the sandbox materializes
  into a checkout (a git repo can't be committed inside this repo). `flurfunk` is seven
  **informal-genre** sources — a Slack export, an X/Twitter thread, a podcast interview, a job
  application (cover letter + CV), a forum thread, and an announcement — grading **attribution** ("X
  said Y" ≠ "Y is true"), an in-thread decision reversal, a quote-tweet negative row, and CV-timeline
  completeness. Both ship a hidden `ground-truth.md`, are wired into the `verify-corpus` skill, and
  are linted by CI + published in the Pages gallery.
- **verify-corpus Mode C — an end-to-end grader for the `curate` lifecycle.** Modes A/B grade
  ingest; Mode C grades `citadel curate` end-to-end: it builds a throwaway curate-defect sandbox from
  the beverages showcase (re-stamping the manifest and neutralizing the showcase's own open
  contradictions so the baseline is clean), seeds a small KNOWN set of defects (resort / orphan /
  contradiction / locator), proves `curate --dry-run` detects EXACTLY those offline (the deterministic
  tier), then runs real `kind=curate` sessions and grades that the model FIXES each defect while
  `check`/`lint` stay at exit 0 and the staged-promote arbitration (applied / NOOP / failed) matches
  reality. It is the real-session complement to the offline detector coverage in `tests/test_curate.py`.

### Changed

- **The ingest prompt now names an Office source's extracted images explicitly.** When a
  `.pptx`/`.docx`/`.xlsx` carried embedded rasters (`CITADEL_IMAGE_SUPPORT=1`), the session
  variables gain an "Extracted images — VIEW each of these" bullet naming the `media/` folder and
  its files. Previously only `formats/office.md` said such a folder *may* exist beside the
  extract, and in two of three benchmarked Sonnet ingests the agent concluded it didn't and never
  opened the chart (the kontor image-delta miss). Two rule additions ride along from the same
  benchmark evidence: `schema.md` § Contradictions now covers **plan-vs-actual** figure pairs
  (state both figures together, each cited, and name the difference — not strictly a
  contradiction, but a tension the reader must see), and a new **`genres/chat.md`** brief covers
  workplace-chat exports (timestamped attribution; decisions reverse in-thread, so the live fact
  is the thread's **end state**; terse decisions still count; banter stays out — the axis where
  an intraday reversal was dropped in the flurfunk benchmark).
- **Four ingest-rule clarifications from the 2026-07 three-tier model benchmark** (every corpus ×
  haiku/sonnet/opus, graded against the hidden answer keys; rule fixes were made only where the
  evidence showed a rule gap rather than a model-capability gap): `schema.md` § Locators now
  requires a `§ Heading` locator be copied **character-for-character** (leading numbering, list
  markers, trailing punctuation included — a dropped prefix does not resolve);
  `formats/office.md` now pins Office locators to **one slide/sheet per marker** (no `§ Slide 2-4`
  ranges) and bans `p. N` (the extracts have slides/notes/sheets, not pages), and explains that a
  **legacy OLE** salvage is a flat run of text with no headings — cite `lines A-B`, never an
  invented `§ Slide N` anchor; and `core.md` gained the **canonicalize-a-merged-entity-to-its-own-
  name** rule (title from the entity's own material or the majority of sources; one-off variants
  become aliases — retroactively, retitling the page when the authoritative spelling arrives in a
  later source) after all three model tiers picked a minority spelling from meeting notes.
- **README model-results matrix filled from the 2026-07 three-tier benchmark** (14 graded runs:
  6 corpora × haiku plus kelvarra/injection-resistance/gazette/kontor × sonnet+opus), a `fail`
  rubric value (a hard structural gate broke), and a new **"How the three tiers actually differ"**
  comparison: haiku degrades on judgment/craft and fabricates into gaps; sonnet matches opus on the
  text-judgment corpora; opus separates only on kontor's cross-modal work (the pixels-only chart
  figure + the cross-source headcount conflict); per-corpus saturation noted.
- **README trimmed to ~175 lines (from ~220) with no information loss.** Cut content that only
  duplicated `docs/` or `CLAUDE.md`: the MCP client JSON block (byte-identical to the one in
  `docs/mcp.md`) became a short pointer to that page; the per-corpus write-up collapsed to one line
  per corpus plus the demo-gallery link (full detail already lives in each `corpora/<name>/README.md`
  and `CLAUDE.md`); the Reference list dropped rows already linked earlier in the README (the rules
  tree, `env.example`, the per-doc links) down to the docs hub, `CLAUDE.md`, and
  CONTRIBUTING/CHANGELOG/SECURITY; and the License section's affiliation and credential-handling
  prose now defers to `NOTICE.md`. The **License & third-party tools** heading (anchored by
  `doctor.py` and `docs/configuration.md`) and every provider-terms link are unchanged.

### Fixed

- **A numeric tag no longer crashes `citadel search` / `citadel tags`.** A bare year in a page's
  tag list (`tags: [finance, 2026]`) is YAML-parsed as an int, and the IDF/scoring paths joined
  `page.tags` into a string — every search over such a wiki raised `TypeError`. `Page.tags` now
  coerces each entry to `str` (single non-list values are wrapped), fixing search, the tag map,
  and every other consumer at the source. Surfaced by the kontor model benchmark, whose budget
  page carries the `2026` tag.
- **`lines N` locator — a note that begins with a number is no longer swallowed into the range.** A
  citation like `line 21 — 1657 founding credit` parsed as the range `lines 21-1657` (the `1657` is
  the year the note describes, not a line), which `citadel lint` then flagged as out-of-range. The
  line-range separator now accepts only a hyphen (spaces allowed) or a TIGHT en/em-dash, never a
  SPACED ` — ` / ` – ` — which is a description separator — so the locator reads as the single line
  21 with the rest a note. Surfaced by the verify-corpus genre-briefs gate.
- **A malformed env knob no longer crashes every command.** A numeric `CITADEL_*` setting whose
  value is blank or not a number (e.g. `CITADEL_LLM_TIMEOUT=20 min`) used to raise at import and
  take down every subcommand; it now falls back to the knob's default and `citadel doctor` gained a
  config check that surfaces exactly which values didn't parse.
- **`citadel check` / `wiki_validate` — a nonexistent page path is now an error.** Naming a page
  that doesn't exist (a typo'd path) reported "OK — no validation issues." with exit 0; both now
  answer `error: no such page: …` (exit 1 on the CLI), so a mistyped CI invocation can't pass
  false-green.
- **`wiki_raw` / `citadel raw` — preserve heading casing in the "not a heading" hint.** When a
  `§ Heading` locator names a heading the source lacks, the error lists the available headings using
  the source's ORIGINAL casing and document order (e.g. `§ The One Rule About Temperature`) instead of
  the case-folded form the internal match set uses. Backed by a new case-preserving
  `grammar.source_heading_texts`; the case-insensitive match itself is unchanged.
- **`§ Heading` locators into bold-line sections now verify and resolve.** Many real sources (FAQs,
  exported Word/Confluence docs) delimit sections with a whole-line `**bold**` header instead of an
  ATX `#` heading. A new shared `grammar.parse_heading_line` recognizes both, so the ingest agent's
  `§ Bold Heading` citations no longer trip `citadel lint`'s locator check (42 false advisories on a
  fresh beverages build) and `wiki_raw` / `citadel raw` resolves them instead of answering "headings
  present: none". Inline or partial bold is still not a heading; the case-insensitive match is
  unchanged. Surfaced by the verify-corpus Mode-A harvest.
- **`citadel lint` — fewer advisory false positives on real ingests.** A `## See also` navigation
  list is no longer counted as a "Missing citations" paragraph (a link title such as
  `Immersion vs. Percolation Brewing` had faked a second sentence — 18 false hits on a fresh
  beverages build). The undefined-abbreviation nudge now skips fiscal-period labels (`Q1`..`Q4`,
  `H1`/`H2`) and roman numerals used in an ordinal context (`Chapter IV`), while still surfacing a
  genuine abbreviation that merely parses as a numeral (`DC`). Advisory-only; no structural gate
  changes. Surfaced by the verify-corpus Mode-A harvest.
- **`okf.slugify` transliterates accented titles instead of dropping the letters.** A title like
  `Café` slugged to `caf` and `Zürich` to `z-rich` (every non-ASCII letter became a gap), so a
  filename the ingest agent sensibly wrote (`cafe.md`) triggered a spurious `filename does not match
  slug of title` advisory. Slugify now ASCII-folds via NFKD + combining-mark stripping (`Café` →
  `cafe`, `naïve` → `naive`) with a small map for letters that do not decompose (`ß` → `ss`,
  `Œ` → `oe`). The three affected `leuchtfeuer` person pages are renamed to the improved slugs
  (`j-rn-albers` → `jorn-albers`, `sabine-kr-ger` → `sabine-kruger`, `tom-s-iglesias` →
  `tomas-iglesias`) with their inbound links and indexes repaired, so all five committed corpora
  stay advisory-clean.
- **`wiki_read` / `wiki_neighbors` / `citadel read` / `citadel neighbors` — error-text polish on the
  traversal-rejection path.** An unsafe path used to answer with a doubled prefix
  (`error: unsafe path: unsafe path: '…'`) because the callers re-wrapped the already-self-describing
  `okf.OKFError` message; the prefix now appears exactly once, on the CLI and MCP surfaces alike.
- **`wiki_search` / `citadel search --limit` — a limit `<= 0` no longer answers a factually wrong
  "No matches".** A negative/zero limit (an LLM client miscomputing the argument) sliced the hit list
  empty and asserted there were no matches; it is now treated as unset and falls back to the default
  (8), documented in the tool docstring and `--limit` help.
- **`wiki_read` / `wiki_neighbors` now normalize Windows-style backslash rel_paths.** `wiki_validate`
  already accepted `concepts\espresso.md` while the other two 404'd on the identical page; all
  rel_path-taking tools (and their CLI twins, which share the same providers) now apply the same
  backslash→slash normalization.
- **`wiki_read` / `citadel read` return generated index files verbatim.** Reading a
  frontmatter-less generated file (`index.md`, `sources/index.md`, `open-points/index.md`) used to
  prepend a spurious empty `---\n{}\n---` frontmatter block from the `okf.dump` re-serialization,
  contradicting the tool's "verbatim" contract and reading as page corruption to an AI consumer;
  the provider now returns the file text byte-for-byte (canonical pages round-trip identically).
- **`citadel doctor` raw-roots check now follows the ACTUAL ingest walk list.** It used to iterate
  the wider `source_roots()` union, so a primary `raw/` configured OUT of `CITADEL_RAW_DIRS` read as
  "reachable" while its files were silently never scanned; the check now walks `CITADEL_RAW_DIRS`
  exactly like discovery and WARNs when the excluded primary `raw/` holds files, naming the fix
  (include `raw` in `CITADEL_RAW_DIRS`).
- **Unrecognized `CITADEL_WIKI_GIT` values are no longer silent.** Anything that is not
  `0/off`, `1/init`, or `auto` still falls back to `auto` (the safe, never-creates-a-repo default),
  but the value is now surfaced by `citadel doctor`'s config check — a typo of "off" (`of`, `fasle`)
  used to silently keep auto-committing (and pushing).

### Added

- **Five new genre briefs** close the biggest coverage gaps found by the 2026-07 rules audit —
  each a one-file rules change the agent applies by content judgment, no code involved:
  `genres/publication.md` (scientific/academic papers: hedged findings stay hedged, a paper's
  own findings vs claims it cites, methods/sample caveats travel with the claim, abstract-vs-body
  gaps noted, authors/venue/year captured), `genres/contract.md` (contracts/legal/financial:
  amounts/dates/parties/defined terms verbatim, "shall" never paraphrased into "may",
  effective/termination dates as dated facts, amendments supersede with a Change-Log trace, no
  legal-advice voice), `genres/social.md` (posts and threads: author+date+platform attribution,
  thread order, engagement numbers as dated snapshots, irony is not fact, quote-posts carry two
  voices, promotional accounts quarantined), `genres/transcript.md` (multi-speaker transcripts
  incl. SRT/VTT: per-speaker attribution, unlabeled speakers stay roles, cleanup never adds
  precision, host reads/ads skipped, line-range locators), and `genres/cv.md` (CVs/applications:
  every claim self-reported and attributed, self-assessments are opinions, claims vs
  corroborating evidence, a newer CV supersedes). All five are enumerated automatically by the
  existing `genres/*.md` discovery and indexed in `citadel/rules/README.md`.
- **One exclusive run lock per workspace.** Ingest and curate now take a run lock (a
  `.citadel_run.lock` sibling of the wiki dir; stale locks from a dead process are reclaimed
  automatically), so a second concurrent mutating run fails loud instead of silently destroying the
  first one's staging and promotes. Manifest and failures-catalog saves are now atomic
  (temp-sibling + `os.replace`), so a crash mid-write can never leave a torn file behind.
- **Docs: `docs/maintenance.md` — maintain & customize.** A new user page covering `citadel curate`
  (what the second lifecycle does, when to run it, all flags including the previously undocumented
  `--retry`), `citadel status`, and the rules-customization story (`rules list|show|eject`, the
  workspace `rules/` overlay, additive `rules/local.md` house rules), linked from the docs hub and
  the README. Alongside it, `.github/copilot-instructions.md` is re-synced with `CLAUDE.md` and
  both agent docs pick up `citadel define` and `curate --retry`.
- **Wiki history: auto-commit the wiki to git after every mutating run.** A new best-effort layer
  (`citadel/wikigit.py`) commits the whole wiki directory — pages, indexes, `log.md`, the manifest —
  as ONE commit after every ingest or curate run that changed it, so every change becomes a
  reviewable diff and the wiki accumulates a long-term audit trail (the diff also makes it easy to
  judge the quality of a model's edits). `CITADEL_WIKI_GIT=auto` (default) commits only when the
  wiki dir is already its own git repository (`git init` inside `wiki/` once to opt in); `1`
  additionally `git init`s it on first use, refusing — with a note, never an error — when the wiki
  dir sits inside another git working tree (an embedded repo would confuse the outer checkout);
  `0` turns the layer off. `CITADEL_WIKI_GIT_REMOTE` names an optional push target (a remote name
  or URL — GitHub, GitLab, any git host) pushed after each commit. Everything is best-effort by
  contract: the wiki is already promoted when the commit runs, so any git problem (no binary, no
  identity, a rejected push) becomes a one-line note on the run report, never a failed run. A new
  `citadel doctor` line reports the layer's state, and the per-source staging copy now excludes a
  wiki `.git` (promote never synced hidden dirs anyway).
- **Pages now record which cite-citadel release wrote them.** `store.write_page` stamps a
  `citadel_version` frontmatter field alongside the existing `timestamp` on every write (authored
  values are overwritten, so neither can go stale or be forged), giving each page visible
  provenance: WHEN it last changed and WITH WHAT version. The refresh path for pages built by an
  older rulebook stays `citadel curate --stale-rules`, which re-runs pages whose sources were
  ingested under an older effective-rules hash — re-reading the cited raw files as part of the
  cluster.
- **`citadel doctor` now checks workspace coherence.** A new check catches the silent
  misconfiguration where `CITADEL_WIKI_DIR` and `CITADEL_RAW_DIR` sit under different parents (e.g.
  the wiki points into a corpus but the raw root is left at the default): every `../../raw/x`
  citation then resolves OUTSIDE the configured raw root, so `grammar.is_source_citation` rejects it,
  `citadel lint` reports the sources broken, and the viewer's source records lose their names/links —
  yet nothing else said the roots don't line up. The check walks the wiki's `## Sources` citations
  (read-only, O(pages), reusing the shared citation grammar) and WARNs (advisory, never FAILs) with
  the count, one offending citation and where it actually resolved, plus the fix (set
  `CITADEL_RAW_DIR` to the `raw/` tree next to the wiki, or select the workspace with
  `CITADEL_WORKSPACE`); OK when every citation resolves under a root.
- **`citadel doctor` now checks for updates.** A ninth check asks PyPI (best-effort, 2s timeout —
  offline it degrades to an OK "check skipped" line, never a WARN/FAIL) whether a newer
  `cite-citadel` is published, and when behind WARNs with the exact upgrade command for the
  *detected* install method: `git pull && uv sync` for a dev checkout, `uv tool upgrade` /
  `pipx upgrade` / `pip install -U cite-citadel`, or a note that `uvx` always runs the latest.
  Deliberately no self-executing `citadel --update` — citadel cannot know which package manager
  owns it, and a running `citadel.exe` cannot replace itself on Windows. Stdlib-only (`urllib` +
  a naive dotted-version compare that never nags on a pre-release it cannot rank).
- **`wiki_define` / `citadel define` — glossary lookup.** An eleventh MCP tool — the tenth
  read-only — plus its CLI twin answers a short "what does X stand for / mean?" as a *lookup* rather than full-text
  retrieval: it surfaces a `type: Abbreviation` glossary hit (matching the short form, expansion,
  title, or an alias, rendered `SHORT — Expansion`) first, then an exact-title/alias page of any
  type, then falls back to the closest `wiki_search` hits when nothing matches exactly. Backed by the
  new `store.define_text`. Read-only, no new dependency.
- **`wiki_status` — a twelfth MCP tool (the eleventh read-only).** The MCP twin of `citadel status`:
  the per-source corpus state table (ingested with model + rules version and the `(stale)` flag,
  failed with reason and attempts, skipped-duplicate, ignored, pending), built from the manifest +
  failures catalog + one stat-only walk. Closes the one parity gap an AI wiki-manager actually
  feels: it could trigger `wiki_ingest` but had no read-only way to see what is pending, stale, or
  stuck. Read-only annotations, never raises.
- **`wiki_lint` gained a `stale_days` parameter.** The staleness threshold CLI users had via
  `citadel lint --stale-days N` (default 365) is now tunable from MCP clients too.
- **The MCP server sets `initialize.instructions`.** Clients that surface it (Claude Code among
  them) get the recommended tool flow for free: orient with `wiki_index`, find with
  `wiki_search`/`wiki_define`, answer from `wiki_read`, spot-check citations with `wiki_raw`;
  `wiki_ingest` is the only mutating tool.

### Changed

- **`citadel init` now writes a two-tier `.env` template (~half the length, same knobs).** The
  ~180-line template every init copied verbatim is restructured: a short top section with the five
  knobs a new user actually sets (`CITADEL_LLM_CLI`, `CITADEL_INGEST_MODEL`, `CITADEL_LLM_TIMEOUT`,
  `CITADEL_WIKI_LANG`, `CITADEL_LLM_LOG_DIR`), then an "Advanced" section grouping the rest under
  short banners (paths & multi-root, formats, large sources & repos, wiki history, curate,
  observability, ignore patterns, style profiles, binary overrides) with condensed comments — the
  long-form explanations live in `docs/configuration.md`, which stays the full reference for all
  knobs. No knob was added or removed and every default is unchanged; existing `.env` files are
  never touched (init never overwrites one).
- **Two `schema.md` rule tightenings from the small-model hardening gate.** § Locators now forbids
  compound locator strings outright — a `§` locator names exactly ONE heading, never several joined
  with commas or "and"; a fact drawn from several passages gets several markers or one real `lines`
  range (compound headings were the remaining non-resolving locator class on cheap models). And the
  mandatory quarantine rule gains a self-check bullet on its banned-surfaces list: re-read the
  frontmatter `description` — it is the wiki's own voice, so it must not assert an attributed claim
  as fact even when the body is correctly attributed.
- **Viewer search + reading UX (final viewer batch).** The offline HTML viewer's full-text search is
  now **tokenized**: every whitespace-separated term must match (AND) across title/tags/path/body
  (and source bodies), fixing a real miss where a two-word query like `adenosine blocking` returned
  nothing though both words are prominent (search had been a single whole-string `indexOf`). Field
  weights are summed per term and an exact full-phrase match adds a bonus so contiguous phrases still
  rank top, and result snippets center on the first term hit and highlight every term. Two operators
  compose with the bare terms and the sidebar tag/facet filters: `tag:x` (prefix match) and `type:y`
  (case-insensitive); an unknown `prefix:` is treated as a literal term. The placeholder hints them
  (`tag:x type:y, / to focus`). Alongside search: the viewer files shrink ~5-6% by dropping the
  redundant top-level `edges` array from the bundle (the same graph was serialized ~3x) and rebuilding
  it in the browser from each page's `outbound` at boot; the reader **remembers your scroll position**
  per page/source (session-scoped, restored on return unless a query is active); **wiki-page links get
  a hover preview** (title, type, description, first ~200 chars — mouse-only, reusing the source
  popover); a page shows a **"Related:" row** of up to 6 pages sharing a tag (ranked by shared-tag
  count, excluding already-linked pages); **keyboard navigation** adds `j`/`k` to move a selection
  through the visible list + Enter to open, and `n`/`N` to step the reader's highlighted hits; and a
  **reader font-size toggle** (15 -> 17 -> 19 px, persisted) sits next to the width control. All still
  offline, deterministic, zero-dependency.
- **Viewer map now clusters by topic.** The offline HTML viewer's graph is reworked from a single
  ring into a topic map: communities are detected by deterministic label propagation over the
  combined graph (real cross-links + IDF-weighted tag-similarity KNN edges) and each is named by its
  most characteristic tag (beverages: `botany`/`trade`/`roasting`/`measurement` instead of 27
  single-tag micro-clusters); source nodes inherit their citing pages' community. Nodes seed around
  per-community centroids and relax with node collision, per-community gravity, and faint
  tag-similarity springs, so related pages actually settle together. Nodes colour by topic (full
  8-hue colorblind-safe brand palette; a type-colour toggle in the map bar, persisted — and a wiki
  that collapses to a single community, like one dense novel, defaults to type colours), the legend
  lists the top clusters with an "other" bucket and per-cluster show/hide, and labels declutter —
  only hubs, hovered neighbours, and zoomed-in views draw text. When a single community would
  otherwise swallow more than two thirds of a dense, uniformly cross-linked wiki, it is refined ONE
  level — the same deterministic label propagation re-runs inside that community over the
  tag-similarity edges alone — so `kelvarra`, `leuchtfeuer`, and `pemberley` resolve into several
  sensible sub-topics instead of collapsing to one blob (and falling back to flat type colours).
  The live drag physics is damped (velocity decay 0.78, softened collision push, a lower jump cap)
  so dragging a node nudges its neighbourhood instead of shaking the whole map, while staying
  responsive. Deterministic (no `Math.random`), vanilla JS, zero deps. Viewer assets only
  (`app.css`/`app.js`).
- **Viewer adopts the project's warm brand palette.** The offline HTML viewer's tokens move to the
  warm paper/ink scheme with the green brand accent (`#009E73` family): a monospace wordmark carrying
  a small green brand marker, and a colorblind-safe map palette (the brand green pinned first). The
  link/accent green is AA-checked per theme — the light `--accent` uses the darker `#007A59` (5.0:1
  on the paper background, where plain `#009E73` reached only ~3.2:1) while dark keeps `#009E73`
  (5.5:1), and the light `--source` gold is darkened to clear 4.5:1 wherever it renders as citation
  text. Viewer assets only (`app.css`/`app.js`); no Python or template change.
- **Viewer: compact, grouped Sources + a mobile/touch/print pass.** A page's trailing `## Sources`
  section no longer renders as a tall footnote wall (the same raw file repeated once per citation,
  each with its own link, date, and back-arrow). It is now a **collapsed `Sources (N)` `<details>`**
  (the page ends on content, not footnotes) that **groups citations by the file they cite** — the
  file link and `(ingested …)` date appear once, followed by that file's citations as a compact run
  (`s1 (§ Heading), s5 (lines 23-25), …`) or, when they carry distinct notes, one muted line each.
  Every citation keeps its own `id`, so an inline `[^sN]` still jumps to its definition (opening the
  collapsed section first) and hover popovers still work per citation; the back-arrows are dropped
  (the inline marker is the way back). The grouping is robust to a misconfigured workspace whose
  citations don't resolve to an embedded source (so a citation renders as a bare file-name span, not
  a source link): each still groups per cited file under a plain file-name header, instead of every
  citation collapsing into one nameless `↩` group. The sidebar "Sources" axis now defaults **closed** (with a
  persisted toggle). The viewer also gained its **first responsive breakpoint** (≤ 720px): the
  sidebar becomes an off-canvas drawer toggled by the existing hamburger / backslash shortcut, the
  reader goes full-width, and the map defaults collapsed and height-capped — no horizontal scroll at
  phone widths, with the desktop layout unchanged. Map pan/drag/tap and the pane resizer now use
  **Pointer Events**, so one-finger pan, node drag, and tap-to-open work on touch (mouse behavior
  unchanged; wheel and the +/- buttons still zoom). A **print stylesheet** prints just the reader
  (black-on-white, sidebar/map/chips hidden) with the Sources section forced open so citations print.
- **Offline viewer embeds only the CITED EXCERPTS of each raw source, not its whole body.** For a
  source-heavy wiki the full-text embed dominated the single-file viewer (a source could be ~40% of
  the document). Now, per source, only the passages the wiki actually cites are embedded — a
  `lines A-B` locator becomes that range plus 3 lines of context, a `§ Heading` becomes its section
  (capped at 80 lines), and an unlocated citation contributes a head excerpt (lines 1-30); overlapping
  and near-adjacent ranges (gap ≤ 2 lines) merge. A short file (≤ 120 lines) or one whose excerpts
  already cover ≥ 2/3 of it embeds whole — but only when the whole body fits under the 200k guard —
  so small notes are never fragmented. The reader shows
  each segment under a "lines A–B" label with a "⋯ lines X–Y not embedded — open the original file" gap
  indicator, and the existing "Open original file" affordance covers the rest. `_SOURCE_MAX_CHARS`
  (200k) stays as the final per-source guard: segments are filled whole until the next one would
  exceed it, then the rest are dropped to the gap indicator rather than sliced off mid-passage. No new
  flag or env knob — this is the default. A large source cited by only a couple of narrow ranges
  shrinks that source's embed by ~100× or more; a wiki that cites its source almost in full and is far
  larger than the guard (e.g. the pemberley novel, ~730k chars, ~95% cited) embeds the cited segments
  that fit within the 200k budget — down from a blind 200k front-slice of the whole body — with the
  uncited/over-budget remainder reachable via "open the original file." The viewer's source
  full-text search accordingly covers only the embedded excerpts; text in an uncited stretch of a
  large source is reachable through the wiki pages that cite it, or the original file.
- **Ingest now gives pages lay-term `aliases`.** `citadel/rules/schema.md` gains an *Aliases* section
  teaching the agent to add up to ~4 high-precision alternate names a reader might search — a lay
  synonym, everyday word, nickname, or former name — to any page (not just abbreviations), so a
  paraphrased query reaches the page by a word its title lacks. Pairs with the alias-scoring change
  below. Validated on a fresh kelvarra build: the currency page picked up `aliases: [money, cash]` and
  `citadel search "money"` went from *absent* to rank 1, with the counterfactual/structural guarantees
  unchanged. (Editing a `rules/` file bumps `rules_version`, so `citadel status` shows previously
  ingested sources as `(stale)` — expected, cosmetic.)
- **Search now weights a page's declared `aliases`.** `store_core._score` scores the `aliases`
  frontmatter (an alternate name / lay-term synonym) at weight 2.5 — between `title` (3.0) and `tags`
  (2.0) — and counts them in the IDF corpus, so a paraphrased query can reach a page by a word its
  title lacks (e.g. a currency page aliased `money`/`cash` surfaces for "how do people pay …"). Purely
  lexical, no synonym map or embeddings. Aliases were already parsed and used by `wiki_define` /
  exact-title lookup but never ranked; a paraphrase-probe harvest over fresh Mode-A builds surfaced the
  gap. Locked by a golden-rank test; the rest of `test_search.py` passes unchanged.
- **Light stemming in search.** `store_core._tokenize` now applies a small, deterministic,
  dependency-free suffix strip (`brewing`/`brew`, `founded`/`founding`, `magnets`/`magnet` collapse to
  a shared token) so paraphrased query forms match. It is applied symmetrically to both the query and
  the page text, so the field-weight / IDF scoring contract is unchanged; the `test_search.py`
  characterization tests still pass unmodified.
- **Leaner, more predictable ingest sessions.** The run instruction and `core.md` now tell the agent
  that the raw source tree is a **read-only input** (read it for content and citations, never write,
  create, move, or delete under it) and that reading/searching go through the agent's **built-in
  file tools** rather than the shell, which stays reserved for the `citadel check` self-check and
  page deletes/renames. The self-check now runs **once** and re-runs only to confirm fixes when it
  reported errors, instead of being invoked repeatedly. Together these cut the number of shell
  subprocesses a session spawns and keep every write inside the wiki — no functional change to the
  wiki that gets produced. (Editing any `rules/` file bumps `rules_version`, so `citadel status`
  shows previously ingested sources as `(stale)` — expected, cosmetic.)
- **`citadel lint` exits 3 (was 2) when the report is not clean.** Exit 2 collided with the
  usage/no-workspace code (argparse's own convention, also used by the workspace guard and the
  `ingest --force`-without-paths refusal), so a CI consumer could not tell "the wiki has structural
  problems" from "invoked wrong". Lint's codes are now: 0 clean, 1 unexpected error, 2
  usage/no-workspace, 3 report not clean. Scripts checking `!= 0` (as CI's corpus lint gate does)
  are unaffected; only a literal `== 2` check on lint's not-clean state needs updating.
- **`wiki_raw` / `citadel raw` — an unresolvable locator is now a clear error.** A locator that
  parses to nothing offline-resolvable (an Office `p. 12` page locator, a garbled/typo'd form) used
  to silently return the WHOLE capped source with only a header note and exit 0 — undetectable by a
  script or an MCP client checking the `error:` prefix, unlike every other bad locator. It now
  errors (exit 1 / `error: …` string) naming the recognized forms (`lines A-B`, `§ Heading`,
  `§ Heading, lines A-B`) and the escape hatch: omit the locator to read the whole source.
- **Blank boolean env values now mean "unset", and all boolean knobs share one parser.** Three
  hand-rolled parsers disagreed on blank: a bare `CITADEL_IMAGE_SUPPORT=` /
  `CITADEL_REPO_SUPPORT=` / `CITADEL_DEDUP_BY_BASENAME=` (an uncommented-but-emptied `.env` line)
  silently DISABLED those default-on features, while blank meant "default" for every other knob
  family. All five boolean knobs now parse through one `_bool_env`: blank/missing keeps the
  default, `1/true/yes/on` and `0/false/no/off` are recognized, and anything else falls back to
  the default with a `citadel doctor` config warning. Anyone deliberately disabling a feature via a
  bare `KNOB=` must now write `KNOB=0`.

## [0.3.0] - 2026-07-06

### Added

- **`wiki_neighbors` / `citadel neighbors` — walk a page's link graph.** A tenth read-only MCP tool
  (+ its CLI twin) prints a page's **links out** (its wiki cross-links, resolved to rel_paths, each
  flagged `(missing)` if the target page is gone), **linked from** (the backlink graph), and **cites
  sources** (the raw/`docs/` source keys it cites, with per-source counts — the keys to hand to
  `wiki_raw`), so an AI can traverse the graph without doing relative-path math itself. Backed by
  `store.neighbors_text`.
- **`wiki_raw` / `citadel raw` — read the raw source behind a citation.** A new read-only MCP tool
  (the ninth) and its CLI twin resolve a `[^sN]` citation's provenance for spot-checking: given the
  cited source key (e.g. `raw/notes.md`) and, optionally, the citation's locator tail (`lines 76-83`,
  `§ Method`, or a combined `§ Method, lines 5-9`), it returns the cited source's text — or exactly
  the slice the locator names — line-numbered and size-capped. A provenance gate makes only sources
  the wiki actually cites (the ingest manifest, or a `docs/` file) readable, so it verifies the
  synthesized wiki without becoming a bulk re-retrieval path; PDFs/images/undecodable files return an
  honest "read it directly" message. Backed by the new `citadel/rawsource.py`.
- **Two more graded test corpora** under `corpora/`, bringing the total to five: **pemberley** (the
  whole of *Pride and Prejudice* as one ~730k-char source — large-source multi-segment chunking,
  relationship extraction, in-novel misinformation, narrative supersession) and
  **injection-resistance** (three mundane documents with adversarial instructions embedded, graded
  that the ingest agent treats them as content and never executes them). Each ships a hidden
  `ground-truth.md` answer key and is wired into the parameterized `verify-corpus` skill.
- **A committed, graded showcase wiki for every corpus** (not just beverages): each
  `corpora/<name>/` is now its own self-contained workspace with a nested `citadel.toml` marker and
  a lint-clean `wiki/`. CI lints all five.
- **GitHub Pages gallery.** `.github/workflows/pages.yml` builds one offline single-file viewer per
  corpus into `site/<name>/` and publishes a lean landing page (`.github/pages-index.html`) — a
  disclaimer banner plus one card per corpus — that links each demo.
- **Community health files** for the public repo: `.github/dependabot.yml` (weekly `uv` + GitHub
  Actions updates) and `.github/ISSUE_TEMPLATE/` (bug report, feature request, and a config that
  routes questions to Discussions and security reports to the policy).

### Changed

- **Search now weights rare terms (IDF).** `store_core.search` scales each token's field weight by its
  inverse document frequency, so a rare, discriminating query term (an acronym, a proper noun)
  outranks one common to the whole wiki — a "TDS" or "espresso" lookup surfaces its own page instead
  of being buried under generic topic pages (measured on the showcase corpora: rank 4→2 and 3→1, no
  regressions). Pure-Python, no new dependency, recomputed per search; the `search()` seam signature
  and MCP surface are unchanged. A golden-rank test locks the behavior against a regression to plain
  overlap.
- **`verify-corpus` now grades retrieval-first.** Phase 2 checks each corpus guarantee the way a user
  consumes the wiki — driving `citadel search`/`read`/`index`/`tags` to prove the answer is both
  correct+cited and *findable* (not merely present on disk), and drops to a file-level grep only to
  classify a miss as a wiki-*creation* defect or a *retrieval* defect. Each corpus's hidden
  `ground-truth.md` gains a frozen, answer-blind `## Retrieval battery`, and the grade's misses route
  into two optimization lanes (the ingest/rules generator and the search tools). Test-only — no wiki
  or corpus content changed.
- **Renamed three corpora** to their in-world names — the fictional-republic corpus is now
  **`kelvarra`**, the three-year-programme corpus is now **`leuchtfeuer`**, and the *Pride and
  Prejudice* corpus is now **`pemberley`** (directories, hidden answer-key directories, and every
  reference across docs, workflows, and the `verify-corpus` skill).
- **`leuchtfeuer` layout inverted.** Its committed `raw/` now holds the **final** post-wave-3 file
  set (11 files, cited by the committed wiki); the wave history moved under `stages/` —
  `stages/initial/` (the 2024 wave-1 originals, charter Rev A + the later-deleted memo), plus the
  existing `stages/wave2/`/`stages/wave3/` overlays. The sandbox wave protocol now seeds the raw
  from `stages/initial/`.

### Fixed

- **Locator lint no longer false-flags the combined `§ Heading, line N` citation form.** The Z6
  locator check treated the whole `Heading, line N` as a heading name and failed to find it —
  producing false-positive advisories (13 on the beverages showcase alone) and phantom `locator`
  findings in `citadel curate` (the check is shared). The locator parser is now a single
  `grammar.parse_locator` (moved to the citation-grammar home, next to `source_definitions`), which
  splits the combined form and verifies **both** the heading and the line range; `lint._locator_problem`
  is a thin consumer. All five showcase corpora now lint with zero locator advisories.

## [0.2.0] - 2026-07-03

### Added

- **`citadel ingest --force`** — deliberately re-read an already-ingested source and reconcile the
  wiki against it (never a plain re-ingest); repos force a full re-digest via `repo-reconcile`;
  clears a persisted unreadable/errored failure record.
- **Multi-root raw** via `CITADEL_RAW_DIRS`, and **`citadel ingest --full-rescan`** to distrust the
  scan cache and rehash everything.
- **`citadel curate`** — a second lifecycle that re-verifies facts, splits overlong pages, and
  re-sorts information without re-ingesting from zero. Flags: `--dry-run`, `--limit`, `--stale-rules`,
  `--diff`; model knob `CITADEL_CURATE_MODEL`.
- **`citadel status`** — per-source state (ingested with model + rules version, failed with reason +
  attempts, skipped-duplicate, ignored, pending) from the manifest and failures catalog, no re-hash.
- **Full CLI ↔ MCP parity** — new `citadel read` / `citadel index` / `citadel sources` subcommands,
  a new `wiki_lint` MCP tool, and behavior annotations (`readOnlyHint` etc.) on all eight MCP tools,
  so an AI without MCP access can drive everything through the shell.
- **`citadel doctor`** — preflight check: workspace found, rules resolve, agent CLI on PATH, raw roots
  reachable, manifest parses, and an API-key billing-shadow warning.
- **OSS project docs** — `NOTICE.md` (affiliation & trademark disclaimer, dependency-license record),
  `CONTRIBUTING.md`, this `CHANGELOG.md`, `SECURITY.md`, `docs/configuration.md`, README badges, and a
  "License & third-party tools" section (bring-your-own-CLI terms + output ownership).
- **`open-pr` skill** and a mandatory self-verification routing section in `CLAUDE.md`.
- **Three graded test corpora** under `corpora/` (beverages, kelvarra, leuchtfeuer)
  with hidden answer keys, driven by the parameterized `verify-corpus` skill.

### Changed

- **Incremental discovery** — the manifest doubles as the scan cache, so an unchanged corpus now
  does **zero content reads**.
- **Deletion is safe by construction** — an unreachable share or a walk error can never read as mass
  deletion, and out-of-root keys are never swept.
- **Segmented sources promote once** — a large source's passes fold into one staging copy and promote
  together, so the live wiki never holds a half-folded source.
- **Store hygiene** — index rebuilds are single-pass (much faster) and `write_page` gained a
  reserved-name guard.
- Packaging: the sdist now **excludes** the test corpora, CI/agent config, and dev-only files (the
  wheel is unchanged); the pyproject `description` is vendor-neutral.

### Fixed

- Failed repo and delete sessions now reach the failures catalog and surface under "Could not
  ingest".
- `lint` and `check` agree on citation/link grammar by construction (shared `grammar.py`).

## [0.1.0] - 2026-07-02

First public, pip-installable release (`pip install cite-citadel`), and the PyPI name reservation.

### Added

- **Workspace model** — `citadel init` scaffolds a workspace (`citadel.toml` marker, `.env`, `raw/`,
  `wiki/`, `rules/`); discovery walks up for the marker or resolves `CITADEL_*_DIR` env vars, and
  fails loud rather than inventing a phantom workspace. `citadel --version`.
- **Rules tree as package data** — `citadel/rules/` (`schema.md`, `core.md`, `tasks/`, `formats/`,
  `genres/`) ships inside the wheel and is read by the ingest agent at run time; the prompt builder
  shrank to a paths-only frame. Editing the rules changes how the wiki is built with no code change.
- **Citation locators** — footnote definitions may carry `p. 12` / `pp. 3-5`, `line 40` /
  `lines 40-52`, or a `§ Heading` anchor; `lint` verifies line/heading locators against raw files.
- **`CITADEL_WIKI_LANG`** (default `en`) — the wiki is written in one target language regardless of
  the sources' languages, with verbatim quotes and proper names left untranslated.
- Release pipeline (v* tags → PyPI Trusted Publishing), wheel-smoke CI, Windows + macOS runners,
  Python 3.12–3.14, PEP 639 license metadata, dynamic single-sourced version.

### Changed

- Shared citation/link/fence parsing consolidated into `grammar.py`; viewer moved to a subpackage
  with a golden bundle test; Office/OLE extraction isolated.

[Unreleased]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.6.0...HEAD
[0.6.0]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/MarkusNeusinger/cite-citadel/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/MarkusNeusinger/cite-citadel/releases/tag/v0.1.0
