# Changelog

All notable changes to this project are documented in this file.

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

Pre-`1.0.0` releases are atomic milestones building toward the first
installable release; see the roadmap in [README.md](README.md).

## [Unreleased]

## [2.0.1] - 2026-05-09

Patch release. Ships the `/aelf:upgrade` banner-persistence fix from [#513](https://github.com/robotrocketscience/aelfrice/pull/513) so users on v2.0.0 stop seeing the upgrade nag after upgrading.

### Fixed

- **`/aelf:upgrade` banner persistence after upgrade** ([#513](https://github.com/robotrocketscience/aelfrice/pull/513)). Three changes make the upgrade UX imperative and clear the post-upgrade banner correctly: (1) `/aelf:upgrade-cmd` is renamed to `/aelf:upgrade` and rewritten as an imperative orchestrator that calls `aelf upgrade-cmd --check`, runs the install-aware command in Bash, then `aelf setup` (refresh slash bundle), then `aelf upgrade-cmd` (clear stale banner cache); (2) `detect_reachable_installs()` no longer false-positives the running interpreter's venv as a separate install under `uv run`, removing the spurious "multiple installs detected" warning; (3) the default update banner (`format_update_banner(latest)` with no `command` kwarg) now points at `/aelf:upgrade`. CLI surface unchanged: `aelf upgrade-cmd` (canonical advisory print) and `aelf upgrade` (deprecated alias) both still work.

## [2.0.0] - 2026-05-07

Reproducibility cut. v2.0 closes the v1.5–v1.7 feature-parity wave with a single tagged release. No additional product surface beyond v1.7.0 — same wheel content, same retrieval defaults, same CLI surface. v2.0.0 is the canonical reproducibility tag for the v2.0 corpus + bench evidence cut: operators who want a stable pin for the BM25F default-on flip (#154, +0.6650 NDCG@k on the v0.1 retrieve_uplift fixture) plus the v1.7.0 detector wave (#264 worker refactor, #421/#387 edge-type rerank + stale writer, #422 value-comparison, #441 confirm, #197/#201 dedup + contradiction, #228 wonder-consolidation, #383-388 edge-type ship gates) should pin `aelfrice==2.0.0` rather than `aelfrice==1.7.0`. README roadmap rows for v1.7 and v2.0 both flip to `shipped` with this tag.

## [1.7.0] - 2026-05-07

Two structural changes drive v1.7.0: BM25F anchor-text retrieval flips default-on, and every ingest entry point now routes through a single `derivation_worker` that treats the append-only `ingest_log` as the source of truth. On top of that lands the v2.0 detector wave (dedup, semantic contradiction, value-comparison, sentiment-from-prose, wonder-consolidation), four new CLI verbs (`reason`, `wonder`, `confirm`, `delete`), the `unlock`/`promote` lock-management surface, and a wave of edge-type ship gates.

The README roadmap row for v1.7 also lists signed-Laplacian (#149), heat-kernel authority (#150), Plate FFT HRR primitives (#216), and HRR bind/probe (#152). Those remain placeholder lanes in `[retrieval]` config — wiring did not land in this cut and they are not user-visible. The roadmap row is accurate that they "remain opt-in"; v1.7.0 in practice is the BM25F flip, the #264 worker refactor, and the items below.

### Added

- **BM25F default-on; Porter stemming on the BM25F lane** ([#148](https://github.com/robotrocketscience/aelfrice/issues/148), [#154](https://github.com/robotrocketscience/aelfrice/issues/154)). BM25F anchor-text retrieval flips to default-on per the v1.7 retrieve_uplift bench evidence (`+0.6650 NDCG@k` uplift on the v0.1 fixture under Porter stemming). Lands the per-flag NDCG@k harness (`tests/retrieve_uplift_runner.py`) plus the no-regression bench-gate test that the operator can run lab-side against `AELFRICE_CORPUS_ROOT`. Porter stemming is added to the BM25F lane with an LRU memoised stem function for performance. Reversible via `[retrieval] use_bm25f_anchors = false` in `.aelfrice.toml` for users who want the v1.6 ranking back. Other v1.7 components (`use_signed_laplacian` #149, `use_heat_kernel` #150, `use_hrr_structural` #152) stay opt-in pending lane wiring.

- **`aelf reason` + `aelf wonder` — graph-walk CLI surfaces** ([#389](https://github.com/robotrocketscience/aelfrice/issues/389), [#390](https://github.com/robotrocketscience/aelfrice/issues/390)). Two new walking surfaces over the belief graph. `aelf reason <query>` returns a chain of beliefs along graph edges that explains why the retrieval surfaced what it did; `aelf wonder` walks the speculative-belief frontier to surface phantom beliefs the consolidation pipeline has not yet promoted. Phantom scaffolding lands with `ORIGIN_SPECULATIVE` and a `Phantom` dataclass in `aelfrice.models`. Smoke tests, slash commands (`/aelf:reason`, `/aelf:wonder`), and a bench-gate harness with skip-on-no-corpus all ship. The MCP `aelf_confirm` tool (#390) provides the affirm-without-locking primitive that the wonder-consolidation pipeline uses to upgrade phantoms.

- **`aelf confirm` — affirm a belief without locking it** ([#441](https://github.com/robotrocketscience/aelfrice/issues/441)). New CLI verb that bumps the Beta-Bernoulli posterior of an existing belief toward truth without freezing it. `aelf lock` is the right tool when you want a permanent rule; `aelf confirm` is the right tool when you want to say "yes, this is correct" without short-circuiting the decay machinery. Slash command `/aelf:confirm` and reference doc updates ship with it. Contributes to the posterior-ranking eval surface (#151).

- **`aelf delete` — hard-delete with audit row** ([#440](https://github.com/robotrocketscience/aelfrice/issues/440)). New CLI verb that hard-deletes a belief from the store after writing an audit row. Confirmation prompt by default; `--yes` to skip; locked beliefs require `--force` (and still write the audit row). Slash command `/aelf:delete` and reference doc updates ship with it. Closes a long-standing surface gap: previously the only delete path was through `aelf doctor` cleanup flags, which made one-off lock cleanup awkward.

- **`aelf unlock` + `aelf promote` — explicit lock-management surface** ([#391](https://github.com/robotrocketscience/aelfrice/issues/391)). `aelf unlock <id>` drops the user-lock on a belief without changing its origin, writing a `lock:unlock` audit row. `aelf promote <id>` upgrades an `agent_inferred` belief to `user_validated` origin. Both ship as CLI verbs, MCP tools (`aelf_unlock`, `aelf_promote`), and slash commands (`/aelf:unlock`, `/aelf:promote`). The existing `aelf demote` path is rerouted through `unlock()` for audit parity — every lock-drop now writes the same audit row regardless of which surface dropped it.

- **`aelf health` — per-edge-type breakdown + `--json`** ([#452](https://github.com/robotrocketscience/aelfrice/issues/452)). `aelf health` gains an `edges_by_type` field on `HealthFeatures` and a per-edge-type breakdown block in the human-readable output. New `--json` flag emits the full feature struct as machine-readable JSON for scripting. Tests cover the populated case, the empty-graph path, and the JSON shape contract.

- **`session_id` propagation — T3 close-out** ([#192](https://github.com/robotrocketscience/aelfrice/issues/192)). v1.6.0 wired `session_id` through onboard / scanner / MCP. T3 closes the remaining propagation gaps: a shared helper picks up `AELFRICE_SESSION_ID` from the environment as a fallback when no explicit session is passed; `ingest_turn`, `aelf lock --id`, and the MCP `tool_lock` entry point all honor it and stamp the column on inserted beliefs and on `ingest_log` / corroboration rows. Beliefs without a discernible session continue to land with `session_id = NULL`. The corroboration recorder (#190) and the deferred-feedback sweeper (#191) now have full session attribution across every ingest path.

- **Selective L0 injection on `UserPromptSubmit`** ([#373](https://github.com/robotrocketscience/aelfrice/issues/373), [#199](https://github.com/robotrocketscience/aelfrice/issues/199) H3). New `top_k_locks` retrieval helper plus a `locked_max_k` knob on `retrieve()` enables top-k-of-locks selection on the per-prompt path. The `UserPromptSubmit` hook uses it to inject only the top-relevance locks (rather than all locks) when selective mode is active; `SessionStart` complements by suppressing the always-injected locked-baseline under selective mode so the two paths don't double-count. Covers the H3 ship gate from the v2.0 enforcement reading. _Subsumed in this same release by #379 below: the `locked_max_k` knob is removed and locks return to always-inject. The two issues land together because the H3 gate measurement is what drove the always-inject decision._

- **Edge-type-keyed rerank consumer + `POTENTIALLY_STALE` BFS skip** ([#421](https://github.com/robotrocketscience/aelfrice/issues/421)). Two halves: (1) `EDGE_POTENTIALLY_STALE` marker edge type ships in `aelfrice.models` and is pinned to `weight = 0.0` in the BFS multi-hop walk so stale-edged beliefs are skipped during expansion; (2) the new `aelfrice.edge_rerank.apply_edge_type_rerank` post-rerranks retrieval candidates by their incoming edge types via `MemoryStore.edges_to(dst)`. Multi-edge composition is multiplicative; same edge type firing multiple times collapses to one factor. Together with #387 below the contradiction surface now writes evidence and the retrieval surface respects it.

- **`POTENTIALLY_STALE` edge writer (`aelf doctor --detect-stale`)** ([#387](https://github.com/robotrocketscience/aelfrice/issues/387)). Producer side of the rerank consumer (#421). New `aelf doctor --detect-stale` flag walks the relationship-detector output and writes `POTENTIALLY_STALE` edges from sub-confidence contradicts. Direction, idempotency, and report counts covered by `tests/test_relationship_detector_stale_writer.py`.

- **Semantic contradiction detector v3 — typed-slot value-comparison gate** ([#422](https://github.com/robotrocketscience/aelfrice/issues/422)). v3 successor for the regex-based R2 detector closed `wontfix` at adversarial recall 0.033. Adds a typed-slot extractor (`aelfrice.value_compare`) that pulls numeric values, enumerated keys, and bounded vocabularies (e.g. `0.5` vs `1.0`, `indexed` vs `full table scan`, `synchronous` vs `async`) into a typed slot, plus a mutual-exclusion comparator that produces `contradicts` directly without needing token overlap. Wired into `relationship_detector` behind the `use_value_comparison` feature flag. Lab adversarial corpus bench-gate (`tests/bench_gate/test_contradiction_v3.py`) targets recall ≥ 0.5 with precision ≥ 0.7. Stdlib-only path; embedding option left to a follow-up issue.

- **Semantic contradiction detector + `aelf doctor` relationships audit** ([#201](https://github.com/robotrocketscience/aelfrice/issues/201)). New `relationship_detector` module with a semantic-contradiction pass; CLI integration as `aelf doctor relationships`. Acceptance tests cover detector behavior and CLI dispatch. Synthetic corpus carve-out documented; contradiction labels reconciled with the v2_0 corpus convention.

- **Dedup audit pass + `aelf doctor dedup` CLI** ([#197](https://github.com/robotrocketscience/aelfrice/issues/197) R1). Round-1 dedup ships: core algorithm with a configurable `[dedup]` config block, audit pass that surfaces near-duplicates without acting, and `aelf doctor dedup` CLI for one-shot operator runs. User surface and `LIMITATIONS.md` shrink documented at [`docs/dedup.md`](docs/dedup.md).

- **Wonder-consolidation bake-off — three phantom-generation strategies** ([#228](https://github.com/robotrocketscience/aelfrice/issues/228)). Three candidate strategies for phantom-belief generation evaluated head-to-head on a synthetic corpus: bake-off runner, evaluator, and ship-decision memo recorded at [`docs/wonder_consolidation_R_final.md`](docs/wonder_consolidation_R_final.md).

- **Sentiment-from-prose feedback detector** ([#193](https://github.com/robotrocketscience/aelfrice/issues/193)). Regex-based sentiment-from-prose detector emits implicit posterior-feedback signal from inbound prose. `aelf health` surfaces the enabled/disabled state. Privacy doc updated to call out the inbound prose inspection scope ([`docs/PRIVACY.md`](docs/PRIVACY.md)).

- **Five new triple-extractor edge types — `IMPLEMENTS`, `TESTS`, `TEMPORAL_NEXT`, `RELATES_TO`, `DERIVED_FROM`** ([#383](https://github.com/robotrocketscience/aelfrice/issues/383), [#384](https://github.com/robotrocketscience/aelfrice/issues/384), [#385](https://github.com/robotrocketscience/aelfrice/issues/385), [#386](https://github.com/robotrocketscience/aelfrice/issues/386), [#388](https://github.com/robotrocketscience/aelfrice/issues/388)). Each lands on the same template: a new edge type in `aelfrice.models` with a tuned BFS weight (`IMPLEMENTS` 0.65, `TESTS` 0.55, `TEMPORAL_NEXT` 0.25, `RELATES_TO` per #383 corpus, `DERIVED_FROM` per #388), a triple-extractor verb pass that emits the edge from canonical verbs, and a per-type ship gate at +5pp BFS uplift on the `bfs_multihop` corpus. Edge-weight notes live in [`docs/bfs_multihop.md`](docs/bfs_multihop.md).

- **Query-strategy router — R1 entity-expand + R3 IDF-clip** ([#291](https://github.com/robotrocketscience/aelfrice/issues/291)). New `query_understanding` modules implement R1 (entity-expand) and R3 (IDF-clip) query rewrites. The rebuild stage gains a `query_strategy` setting that routes between the legacy stack and the R1+R3 stack. Default remains legacy; opt-in via `[rebuild] query_strategy = "r1_r3"` for the new path.

- **Rebuild-log instrumentation on `UserPromptSubmit`** ([#288](https://github.com/robotrocketscience/aelfrice/issues/288) phase-1a). The rebuild diagnostic log added in v1.6.0 gains coverage of the `UserPromptSubmit` hook path. Each per-prompt rebuild now appends a JSONL record; the audit script in `scripts/audit_rebuild_log.py` (also from v1.6.0) handles UPS records uniformly with the existing PreCompact records.

- **Directive-detection — Path A intent-prefix filter** ([#374](https://github.com/robotrocketscience/aelfrice/issues/374), [#199](https://github.com/robotrocketscience/aelfrice/issues/199) H1). H1 ship of the v2.0 enforcement reading: a regex-based intent-prefix filter that detects directive-shaped input on `UserPromptSubmit`. Bench gate + candidate detector land first, then the prefix-filter behavior. Spec memo at [`docs/directive_detection.md`](docs/directive_detection.md).

- **`uri_baki` post-rank score adjuster — N=10k/50k retest** ([#153](https://github.com/robotrocketscience/aelfrice/issues/153)). Post-rank score adjusters (URI-aware boost/dampen) reland with a retest harness at N=10k and N=50k. Experimental — opt-in via `[retrieval] use_uri_baki = true`.

- **Replay-soak gate — consecutive-green streak computer + PR-time gate** ([#262](https://github.com/robotrocketscience/aelfrice/issues/262), [#403](https://github.com/robotrocketscience/aelfrice/issues/403)). Operationalises the v1.6.0 `replay_full_equality` probe as an actual CI signal: daily cron appends a status JSONL on a dedicated branch, `replay_soak_streak` computes the consecutive-green streak from that log, and a PR-time gate workflow (`replay-soak-gate.yml`) blocks merges that don't have a ≥7d green streak. Path-scoped to the derivation contract surface (`derivation.py`, `derivation_worker.py`, `replay.py`, `ingest.py`, `scanner.py`, soak fixture, soak workflows) so out-of-scope PRs aren't soak-gated. Hypothesis-based replay-equality probe (deliverable B) walks the `ingest_log` and asserts byte-equal store state across permuted inputs. `hypothesis` added to the dev dependency group. The check is currently advisory — adding it to `required_status_checks` is deferred until the streak is operationally satisfiable.

- **End-to-end install/upgrade test matrix** ([#334](https://github.com/robotrocketscience/aelfrice/issues/334)). New `tests/e2e/` skeleton with five scenarios: install → onboard → search; hook inject roundtrip via the rebuild seam; source-kind discrimination across ingest paths; v1.4 → current migration roundtrip from a captured v1.4 snapshot fixture. Install scripts cover `uv tool install` / `pipx install` / `venv + pip` matrix. PR-label-opt-in workflow (`e2e` label) gates the e2e job in CI.

- **Testing-strategy unit/integration/E2E split + `attn:e2e-failure` workflow** ([#370](https://github.com/robotrocketscience/aelfrice/issues/370)). Documents the unit / integration / E2E split at [`docs/testing-strategy.md`](docs/testing-strategy.md). E2E workflow triggers on `push:main` and opens an `attn:e2e-failure` issue if the suite fails — main-branch-only, so PRs aren't on the hook.

- **Close-the-loop measurement harness — spec memo** ([#317](https://github.com/robotrocketscience/aelfrice/issues/317)). Spec memo at [`docs/close-the-loop.md`](docs/close-the-loop.md) for the close-the-loop measurement pipeline; implementation in a future cut.

- **PR-title conventional-commit prefix gate** ([#413](https://github.com/robotrocketscience/aelfrice/issues/413)). New `pr-title-prefix` job in `staging-gate.yml` mirrors the existing `commit-msg-prefix` job but validates the PR title. Fails the PR if the title does not start with `feat:`, `fix:`, `docs:`, or another approved prefix. Prefix list is delegated to `scripts/check-commit-msg.py` so it stays in sync with commits and CLAUDE.md.

- **Auto-`docs` label for docs-only PRs** ([#413](https://github.com/robotrocketscience/aelfrice/issues/413)). New `label-docs.yml` workflow inspects changed files via the API and applies (or removes) the `docs` label whenever the path set matches the docs-only globs. Mirrors the path filters in the heavy gates so the label is a faithful indicator of which PRs skipped pytest / deptry / CodeQL.

### Changed

- **Write-log-as-truth derivation worker — every ingest entry point routed through `derivation_worker`** ([#264](https://github.com/robotrocketscience/aelfrice/issues/264)). The largest internal refactor of v1.7.0 (19 commits across the cycle). The append-only `ingest_log` (introduced in v1.5) becomes the source of truth: every ingest entry point — `ingest_turn`, the regex scanner, the LLM-classifier `accept_classifications` path, the MCP `tool_lock`, the `triple_extractor` worker, and the `aelf lock` CLI verb — is rerouted through a single `aelfrice.derivation_worker` module that derives store state from the log. Each route ships with an integration test plus a per-route replay-equality gate that asserts a freshly-derived store from the log is byte-equal to the live store after the same operations. New `aelf doctor --derive-pending` flag sweeps any unstamped log rows from before the worker landed. `replay_full_equality` (introduced in v1.6) holds across every route. Behavior is unchanged at the public API; the refactor is the v2.0 prereq that lets the log replace the legacy mutation-on-write path. `LIMITATIONS.md` notes the worker-route re-derivation escape hatch for operators who need to rebuild from the log.

- **Locks always inject — `locked_max_k` knob removed; locks treated as always-injected pool** ([#379](https://github.com/robotrocketscience/aelfrice/issues/379)). Reframes locks as the always-injected pool: every lock ships on every prompt, in full, regardless of relevance score. The `locked_max_k` knob added in #373 (selective L0) is removed from `retrieve()`, `search_for_prompt`, and the `UserPromptSubmit` hook; `SessionStart` always injects the full locked set. Lock count is now the user's baseline-context budget knob — if you've locked 200 things, every session opens with all 200, by your design. Mechanically simpler, easier to reason about, and removes the H3-gate decision path from the per-prompt critical path. Documented across [README.md](README.md), [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), and [`docs/v2_enforcement.md`](docs/v2_enforcement.md). #373 and #379 land in the same release because the H3 measurement is what drove the always-inject decision.

- **Update banner reword + `/aelf:upgrade` advisory rename** ([#427](https://github.com/robotrocketscience/aelfrice/issues/427)). The previous shape `"⬆ /aelf:upgrade to v1.6.0"` read as an imperative pointing at a slash command that is itself advisory. New shape embeds the install-method-aware command from `upgrade_advice()` directly: `"⬆ aelfrice 1.7.0 — run: uv tool upgrade aelfrice"`. Slash command renames `/aelf:upgrade` → `/aelf:upgrade-cmd`; `aelf upgrade` CLI subparser renames to `aelf upgrade-cmd` with `aelf upgrade` kept as a deprecated alias for one minor (emits a stderr deprecation notice on invocation). Existing user installs pick up the new slash file via the orphan-prune path in `aelf setup`. Reverses the v1.5.1 self-replacing upgrade introduced in #242: in practice users on `uv tool` and `pipx` setups preferred copy-paste authority over implicit self-replacement, especially after #345 surfaced multi-install machines.

- **Per-lane relevance floor + silent-when-empty in rebuild** ([#364](https://github.com/robotrocketscience/aelfrice/issues/364)). The rebuild stage applies a per-lane relevance floor (locked / l25 / l1 / bfs each get their own threshold) and emits an empty block instead of a degraded one when no lane clears its floor. Lifts the noise floor on the rebuilt context — fewer borderline-relevance beliefs leak into the continuation when the retrieval signal is weak.

- **CI workflow paths-filter migration** ([#446](https://github.com/robotrocketscience/aelfrice/issues/446)). Replaced workflow-level `paths-ignore` in `.github/workflows/ci.yml` with an in-job `dorny/paths-filter@v3` step. Pytest jobs now always run (so the `pytest (3.12)` / `pytest (3.13)` required status checks always report) but install + test steps short-circuit on docs-only PRs. Unblocks docs-only PRs under the `required_status_checks` ruleset on main. Migrates the TODO note from #413 to actual implementation.

- **Differential CI gates for docs-only PRs** ([#413](https://github.com/robotrocketscience/aelfrice/issues/413)). `ci.yml` (pytest matrix), `deadcode.yml` (deptry), and `codeql.yml` (analyze) now skip on PRs whose changed files are entirely under `docs/`, `**/*.md`, `LICENSE`, `.github/ISSUE_TEMPLATE/**`, or `.github/PULL_REQUEST_TEMPLATE.md`. Mixed PRs still run full CI. The `staging-gate` security checks (gitleaks, PII pattern-scan, history-scan, release-docs-check, commit-msg-prefix, pr-body-issue-link) and `typos` always run regardless of paths. Net effect: a single-file docs PR drops from ~3 min of CI to ~30s. CodeQL still runs full-tree weekly via the existing schedule. *(Subsumed for the pytest workflow by the #446 paths-filter migration above.)*

- **Replay-soak status writes to dedicated branch; PR gate reads from it** ([#461](https://github.com/robotrocketscience/aelfrice/issues/461)). Refactors the #403 replay-soak status pipeline so the daily cron writes status JSONL to a dedicated branch and the PR gate reads from that branch. Removes the race where a PR could land on top of an in-flight cron status push. The 0-byte stub of `.replay-soak-status.json` left on `main` from the original #403 implementation is removed.

- **README/ROADMAP/COMMANDS realignment for v1.5.1/v1.6.0/v1.7.0** ([#360](https://github.com/robotrocketscience/aelfrice/issues/360)). Sweep across [README.md](README.md), [`docs/ROADMAP.md`](docs/ROADMAP.md), [`docs/LIMITATIONS.md`](docs/LIMITATIONS.md), and [`docs/COMMANDS.md`](docs/COMMANDS.md) to reflect the post-v1.6.0 ship state — surface count, onboard default, budget defaults, posterior-ranking section. [`docs/CLI_SURFACE_AUDIT.md`](docs/CLI_SURFACE_AUDIT.md) marked as a v1.3-era historical memo.

- **README quickstart no longer reads as a 4-step install**. The original quickstart bundled `aelf lock "never push directly to main; use scripts/publish.sh"` as the third of four lines, indistinguishable from a required step. Users ran the example verbatim and ended up with a permanent locked belief about a publish script that did not exist in their repo. The quickstart is split: install + setup + onboard are the install. The `aelf lock` example is now in its own block under "add your first rule", explicitly called out as an example to substitute or skip. `pip install` switched to `pipx install` (with `uv tool install` as alternative) — `aelf` is a global CLI, and `pip install` failed outright on PEP 668 systems and on macOS where no `pip` shim is on PATH.

### Fixed

- **`relationship_detector` id-collision corroboration guard in `insert_or_corroborate`** ([#264](https://github.com/robotrocketscience/aelfrice/issues/264)). Found during the worker-routing refactor: when two beliefs hash-collided on `content_hash` (rare but possible after heavy abbreviation), `insert_or_corroborate` could route the second insert to the wrong belief id and silently corroborate the wrong belief. New guard cross-checks the belief id before recording a corroboration; on collision, both inserts proceed independently.

- **`override_belief_type` reconstructed from `raw_meta` on full-equality probe** ([#264](https://github.com/robotrocketscience/aelfrice/issues/264)). The replay-equality probe was failing on classification-route entries because `override_belief_type` was stored on the live row but not reconstructed from `raw_meta` when re-deriving. Fix reconstructs the field from the log payload so the probe holds across the classification route.

- **lychee link-check — drop removed `--exclude-mail` flag** ([#414](https://github.com/robotrocketscience/aelfrice/issues/414)). Upstream lychee dropped the flag; CI workflow updated.

- **`flag-stale-open-prs` — refresh `origin/main` before `is-ancestor` check**. Workflow was running `git merge-base --is-ancestor` against a stale `origin/main` ref and falsely flagging fresh PRs as stale.

## [1.6.0] - 2026-05-02

### Added

- **Hook-hardening Phase 1 — framing-tag contract + content escape for memory blocks** ([#280](https://github.com/robotrocketscience/aelfrice/issues/280), [#297](https://github.com/robotrocketscience/aelfrice/pull/297)). Closes the prompt-injection surface where ingested belief content could forge or close the `<aelfrice-memory>` framing tag and inject downstream instructions into the host's prompt. Memory-block rendering now wraps each retrieved belief in a per-belief framing tag, escapes `<` / `>` / `&` in belief content at render time, and asserts the outer block tags match expected start/end on emit. Hosts that consume the framing tag can rely on it as a trust boundary.

- **Hook-hardening mitigation 3 — per-turn audit log** ([#280](https://github.com/robotrocketscience/aelfrice/issues/280), [#314](https://github.com/robotrocketscience/aelfrice/pull/314)). New `<git-common-dir>/aelfrice/hook_audit.jsonl` records the exact rendered hook block on every fire of `user_prompt_submit` / `session_start` — telemetry records counts, audit records what was actually injected. Default-on, opt-out via `AELFRICE_HOOK_AUDIT=0` env or `[hook_audit] enabled = false` in `.aelfrice.toml`. 10 MB cap (configurable: `[hook_audit] max_bytes`); single-slot rotation to `hook_audit.jsonl.1` on rollover; fail-soft on I/O error. Read API: `aelfrice.hooks.audit.read_audit(path)`. 21 new tests in `tests/test_hook_audit.py`.

- **`aelf tail` — live observability for hook injections** ([#321](https://github.com/robotrocketscience/aelfrice/issues/321), [#322](https://github.com/robotrocketscience/aelfrice/pull/322)). `tail -f`-style pretty-printer over the per-turn hook audit log. Streams each fire as a one-line summary (timestamp, session, n_locked, latency, prompt prefix); `--full` switches to the full rendered block. Reader half of the #321 observability story; complements the audit-log writer above. The audit record itself is extended with `beliefs[]` (per-belief id + tier), `latency_ms`, and `tokens` so the tail reader can surface what was injected at what cost without re-running retrieval.

- **Belief retention class + per-source aging policy** ([#290](https://github.com/robotrocketscience/aelfrice/issues/290)). Phase-1 adds the `retention_class` schema column on `beliefs` (`unknown` | `transient` | `durable` | `permanent`) and the matching python validator. Phase-2 wires per-ingest-source defaults into `derive()` and the orphan-classifier scanner so new beliefs land with a non-`unknown` retention class based on which entry point ingested them. Phase-3 lands the promotion path: `MemoryStore.find_promotable_snapshots()` and `set_retention_class()` store APIs, surfaced through `aelf doctor --promote-retention` so an operator can run a one-shot pass that reclassifies low-prior beliefs based on retrieval / corroboration evidence accumulated since they were ingested. Foundational layer for the v2.0 aging / pruning policy spec'd in `docs/belief_retention_class.md`; no automated retention-driven eviction yet.

- **Rebuild diagnostic log — phase-1a + phase-1c audit script** ([#288](https://github.com/robotrocketscience/aelfrice/issues/288), [#302](https://github.com/robotrocketscience/aelfrice/pull/302)). Phase-1a write path: every context rebuild emits a JSONL record under `<git-common-dir>/aelfrice/rebuild_logs/<session>.jsonl` capturing the prompt, retrieval candidates per lane, dedupe stats, and the final pack-rate. Phase-1c reader: `scripts/audit_rebuild_log.py` summarises one or more session logs — pack-rate distribution (mean / p50 / p90), drop-reason histogram, per-lane contribution. Together they unblock the operator-week of in-tree evidence collection that gates the rebuild-redesign calibration work in #289 / #290 / #291.

- **Posterior-ranking eval harness + heat-kernel composition** ([#151](https://github.com/robotrocketscience/aelfrice/issues/151), [#306](https://github.com/robotrocketscience/aelfrice/pull/306), [#310](https://github.com/robotrocketscience/aelfrice/pull/310)). Slice 1 (#306): new `benchmarks/posterior_ranking.py` harness measures MRR uplift and expected-calibration-error (ECE) of the posterior-ranked retrieval against the BM25 baseline on labeled fixtures. Slice 2 (#310): heat-kernel composition lands as a log-additive term in the ranking score; `use_heat_kernel` (default-OFF placeholder added in v1.5.0) is now wired through `retrieve_v2`. Default flip is gated on the harness clearing the MRR-uplift / ECE thresholds across the v2.0 corpus; see issue #151 for the gate criteria.

- **Deferred-feedback sweeper — implicit retrieval-driven posterior signal** ([#191](https://github.com/robotrocketscience/aelfrice/issues/191), [#256](https://github.com/robotrocketscience/aelfrice/pull/256)). Background sweeper observes which retrieved beliefs are *referenced* by the host's subsequent turn (string-overlap signal against the next user prompt + assistant response) and emits implicit posterior-feedback events — `helped` when a retrieved belief shows up in the continuation, `noise` when retrieved-but-unreferenced beliefs accumulate. Feeds the `feedback_history` table that `aelf doctor` and the posterior-ranking harness consume. Replaces the v1.5 explicit-only feedback path with a default-on implicit signal; the explicit `aelf feedback` CLI remains for high-confidence overrides.

- **v2.0 corpus public scaffold + bench-gate harness** ([#307](https://github.com/robotrocketscience/aelfrice/issues/307), [#311](https://github.com/robotrocketscience/aelfrice/pull/311), [#319](https://github.com/robotrocketscience/aelfrice/issues/319), [#320](https://github.com/robotrocketscience/aelfrice/pull/320)). Public-side scaffold for the v2.0 detector evaluation corpus: empty per-module directories under `tests/corpus/v2_0/` (dedup #197, enforcement #199, contradiction #201, wonder-consolidation #228, promotion-trigger #229, sentiment #193) plus the bench-gate harness in `tests/bench_gate/` that loads JSONL via the `corpus_root` fixture. The autouse `bench_gated` pytest marker skips when `AELFRICE_CORPUS_ROOT` is unset or the per-module dir is empty, so public CI stays green while the labeled corpus lives in the lab repo. Lab-side runner: `scripts/run_bench_gate.sh` exports the env var and runs the suite.

- **`replay_full_equality` probe — flip-readiness gate for #262** ([#262](https://github.com/robotrocketscience/aelfrice/issues/262), [#304](https://github.com/robotrocketscience/aelfrice/pull/304)). New deterministic probe walks the append-only `ingest_log` (#205), replays every row through `derive()` into a fresh in-memory store, and asserts byte-equal equality against the live store on every public-API readout. Sentinel for the v2.0 view-flip: while the probe holds, the log-as-source-of-truth derivation can replace the legacy mutation-on-write path with no behaviour change. Fails loudly on any divergence; surfaces the first differing belief id + field for triage.

- **Onboard / scanner / MCP `session_id` propagation to inserted beliefs (phantom-prereqs T3)** ([#192](https://github.com/robotrocketscience/aelfrice/issues/192)). Closes the third phantom-prereq from #190's session-tracking story: every ingest entry point now stamps `beliefs.session_id` so the corroboration recorder (#190) and the deferred-feedback sweeper (#191) can correlate retrieval signal with the originating session. `MemoryStore.scan_repo()` derives a synthetic per-invocation session id; the onboard-classifier path threads the active `onboard_sessions.session_id` through to insert; `tool_lock` (the MCP host-write entry point) accepts an optional `session_id` kwarg and tags the inserted belief with it. Beliefs without a discernible session continue to land with `session_id = NULL`; the column was already nullable.

- **Reachable-install detection + multi-install upgrade warning** ([#345](https://github.com/robotrocketscience/aelfrice/issues/345)). New `lifecycle.detect_reachable_installs()` walks `PATH` and the standard `uv tool` / `pipx` venv roots to enumerate every aelfrice install reachable from the current shell. `aelf upgrade` now prints a warning block listing all reachable installs (with the active one marked) before upgrading, so users on multi-install machines see what they're about to update — and which siblings will silently keep running the old version. Eight new cases in `tests/test_upgrade_ux.py` cover the empty / single / multi cases.

### Changed

- **Dev-only benchmark targets moved out of the `aelf` CLI** ([#342](https://github.com/robotrocketscience/aelfrice/issues/342)). `aelf bench verify-clean | longmemeval-score | posterior-residual` were always-failing in shipped wheels (the `benchmarks/` tree is dev-only and not packaged) and put dev-script knowledge in the runtime CLI surface. Replaced with module-level entry points runnable from a source checkout: `python -m benchmarks.verify_clean PATH ...`, `python -m benchmarks.longmemeval_score PREDS GT JUDGE`, `python -m benchmarks.posterior_ranking [--fixtures ... --seeds N --mrr-threshold X --ece-threshold Y --json --heat-kernel]`. The unknown-target error in `aelf bench` now points at the new entry points. The default `aelf bench` (synthetic harness in `src/aelfrice/benchmark.py`) and the `_BENCH_INERT_TARGETS` placeholders are unchanged. The deptry `DEP001 = ["benchmarks"]` ignore is removed — `src/aelfrice` no longer imports the dev-only package.

### Fixed

- **Corroborations preserved across unique-applying migration** (refs [#336](https://github.com/robotrocketscience/aelfrice/issues/336)). The one-shot migration adding `UNIQUE(content_hash)` to `beliefs` did `DROP TABLE beliefs` with `PRAGMA foreign_keys=ON`, firing the `ON DELETE CASCADE` on `belief_corroborations` and wiping every corroboration row — including the synthetic `consolidation_migration` markers the dedup pass had just inserted. Wrap the swap in `PRAGMA foreign_keys=OFF/=ON` per the SQLite recommended pattern for table-mutation migrations.
- **`aelf doctor --gc-orphan-feedback` for dangling `feedback_history` rows** ([#223](https://github.com/robotrocketscience/aelfrice/issues/223), [#300](https://github.com/robotrocketscience/aelfrice/pull/300)). 100% of `feedback_history` rows had orphaned `belief_id`s after deletes; new doctor flag does a SQL delete-where-not-exists pass with a count printout.
- **Re-ingest row inflation — dedup by `content_hash`** ([#219](https://github.com/robotrocketscience/aelfrice/issues/219), [#283](https://github.com/robotrocketscience/aelfrice/pull/283)). Re-ingesting the same content under a different belief id created near-duplicates and inflated FTS5 retrieval ~5.3×; insert path now dedupes against `content_hash` and routes the second ingest to `record_corroboration` instead.
- **Rebuild block dedupes by `content_hash`** ([#281](https://github.com/robotrocketscience/aelfrice/issues/281), [#293](https://github.com/robotrocketscience/aelfrice/pull/293)). Symptom-level partial fix for the duplicate-output complaint in #281 — output stage drops same-content beliefs at render time so the host doesn't see two copies of the same fact. Full fix lands with the rebuild-redesign work in #289.
- **`step-security/harden-runner` SHA-pinned to v2.19.0 across workflows.** Replaces tag-pin with the corresponding 40-char commit SHA per repo convention; resolves Sourcery / CodeRabbit warnings on subsequent PRs.
- **Renamed `merge-conflict` label refs to `attn:merge-conflict`** in the `flag-stale-open-prs` workflow. The bare label didn't exist in this repo; four sites updated.

## [1.5.1] - 2026-04-28

### Added

- **Belief corroboration tracking — sibling table + ingest recorder (phantom-prereqs T1)** ([#190](https://github.com/robotrocketscience/aelfrice/issues/190)). New `belief_corroborations` table records each re-ingest of identical content without disturbing the existing dedup contract. When `ingest_turn` or `ingest_triples` encounters an already-existing belief (by id), it calls `MemoryStore.record_corroboration(belief_id, source_type=...)` instead of silently discarding the duplicate. Four `source_type` constants distinguish the ingest paths: `CORROBORATION_SOURCE_COMMIT_INGEST`, `CORROBORATION_SOURCE_TRANSCRIPT_INGEST`, `CORROBORATION_SOURCE_MCP_REMEMBER`, `CORROBORATION_SOURCE_HOOK_INGEST` (all in `aelfrice.models`); `record_corroboration` validates against `CORROBORATION_SOURCE_TYPES` and raises `ValueError` on unknown values. `Belief.corroboration_count` (default 0) is populated at retrieval time via a per-belief subquery in `get_belief`, `search_beliefs`, `search_beliefs_scored`, and `list_locked_beliefs`. `session_id` and `source_path_hash` columns are nullable; T3 (#192) will wire session propagation. `ON DELETE CASCADE` removes corroboration rows when a belief is deleted. 16 new deterministic tests in `tests/test_corroborations.py` cover schema creation, idempotency, re-ingest recording, corroboration count on retrieval, cascade deletion, nullable fields, source_type enum coverage, and ValueError on unknown source_type.

- **Default-on LLM-Haiku onboard via host-driven classification — no API key required** ([#238](https://github.com/robotrocketscience/aelfrice/issues/238), [docs/llm_classifier.md](docs/llm_classifier.md), [docs/PRIVACY.md](docs/PRIVACY.md)). The typical aelfrice user already runs an LLM host (Claude Code, etc.); demanding a separate `ANTHROPIC_API_KEY` to opt into the v1.3.0 LLM-Haiku classifier pushed the higher-quality path out of reach for that population. v1.5.0 makes `/aelf:onboard` drive the four-class classifier through the host model's own Task tool against the smallest model in its stack — no API key, no separate billing, no MCP roundtrip. Quality matches the v1.3.0 LLM-Haiku ceiling (~12pp macro-F1 over the regex classifier on real prose); cost is sub-percent of a typical weekly host-plan allowance for one-time per-project onboard. Two new low-level CLI flags make the polymorphic handshake shell-callable: `aelf onboard <path> --emit-candidates` persists a PENDING `onboard_sessions` row and prints `{session_id, n_already_present, sentences[]}` as JSON to stdout (no network); `aelf onboard --accept-classifications --session-id <id> --classifications-file <file|->` reads `[{index, belief_type, persist}, ...]` from a file or stdin, applies the host verdicts, and prints an `AcceptOnboardResult` JSON summary (no network). The slash command body at `src/aelfrice/slash_commands/onboard.md` is rewritten to (1) emit candidates via Bash, (2) batch up to 50 sentences per Task dispatch (`model=haiku`) with a four-class classification template, (3) aggregate the per-batch JSON arrays, and (4) feed the result into `--accept-classifications` via stdin. `--no-subagents` (or absence of the host's Task tool) falls through to the deterministic regex classifier. `[onboard.llm].enabled` flips default `False → True` with a soft-fallback in `check_gates`: users without the `[onboard-llm]` extra or without `ANTHROPIC_API_KEY` who haven't passed `--llm-classify` get the v1.0/v1.2 regex behaviour silently — explicit opt-in (`--llm-classify` or `--dry-run`) keeps the fail-fast install-hint path. Consent prompt copy expanded with cost ballpark, one-time scope, explicit data-sent / data-not-sent lists, the audit command, and the opt-out snippet. The direct-API CLI path (`aelf onboard --llm-classify`) remains the API-key-user fallback and is unchanged. 10 new in-process CLI tests in `tests/test_cli_onboard_handshake.py` cover the flag round-trip end-to-end (emit → accept via file and stdin → session marked COMPLETED, plus usage / unknown-session / bad-JSON failure modes). `tests/test_llm_classifier.py` updated for the new soft-fallback semantics (the no-SDK default-test-env case soft-falls instead of hard-failing).

### Fixed

- **`/aelf:upgrade` is now one-command-and-done ([#242](https://github.com/robotrocketscience/aelfrice/issues/242)).** Three independent bugs combined into a permanent "update available" banner that no upgrade could clear. (1) `lifecycle.installed_version()` read `aelfrice.__version__` from `src/aelfrice/__init__.py`, where the constant had been hardcoded to `"1.2.0"` since v1.2 and was never bumped on subsequent releases — so even after a successful upgrade to 1.5.0 the lifecycle module reported 1.2.0, the cache stayed `update_available=true`, and the orange banner kept firing forever. Fixed by reading `importlib.metadata.version("aelfrice")` instead, sourced from `pyproject.toml` at build time and tracking the published wheel automatically. `__init__.py:__version__` now also reads from package metadata; one source of truth. (2) `/aelf:upgrade` (and the underlying `aelf upgrade` CLI verb) hardcoded `pip install --upgrade aelfrice` for non-pipx, non-venv installs. For users on `uv tool install aelfrice` (the recommended install path) this was both wrong and unrunnable: pip is not on PATH in a uv-managed environment, so the suggestion failed outright. New `_is_uv_tool_install()` detection and a `uv tool upgrade aelfrice` branch in `upgrade_advice()`. (3) `/aelf:upgrade` only printed the command; the user had to copy-paste it into a terminal. Now it executes the upgrade in-process via `subprocess.run` for install methods where the upgrade is run by an external orchestrator (`uv tool` and `pipx`, both of which atomically swap the package and are safe to invoke from inside the running aelf binary). On success it wipes `~/.cache/aelfrice/update_check.json` so the orange banner clears immediately on the user's next prompt. Direct-pip installs still print the command (self-replacement of the running interpreter is unreliable on Windows); pass `--print-only` to force the pre-1.5.1 print-only behaviour on any install method. New flag: `--print-only`. `UpgradeAdvice` gains `argv: tuple[str, ...]` and `self_replacing: bool` fields. Slash command `aelf:upgrade` body updated to match the new behaviour.

## [1.5.0] - 2026-04-28

### Added

- **Composition plumbing — flag policy + placeholder flags + per-lane telemetry (v1.5.0)** ([#232](https://github.com/robotrocketscience/aelfrice/issues/232)). Split off from #154 (which retargets to v1.7 as the default-on-flip tracker). Standardises the retrieval flag-name surface on `use_<lane>_<modifier>`: renames `bm25f_enabled` to `use_bm25f_anchors` across kwarg / TOML key / helper / `retrieve_v2`. Adds four placeholder flags for the v1.6 / v1.7 component waves (`use_signed_laplacian`, `use_heat_kernel`, `use_posterior_ranking`, `use_hrr_structural`); each is default-OFF and no-ops at v1.5.0, but setting any true in `.aelfrice.toml` emits exactly one stderr warning per process per flag, naming the flag and the tracker issue. Adds `LaneTelemetry` dataclass (locked / l25 / l1 / bfs counts post-dedupe, `bm25f_used`, resolved `posterior_weight`) plus `last_lane_telemetry()` accessor; populated in `retrieve()` and `retrieve_with_tiers()` with one assignment per call. Consumers: the v1.6+ benchmark gates and `aelf doctor`. 8 deterministic acceptance tests in `tests/test_composition_tracker.py`. No default flips, no new retrieval algorithms, no benchmark gate — that ships under #154 in v1.7.

- **Version-vector schema for v3 federation forward-compat (v1.5.0)** ([#204](https://github.com/robotrocketscience/aelfrice/issues/204)). New `belief_versions(belief_id, scope_id, counter)` and `edge_versions(src, dst, type, scope_id, counter)` sidecar tables in `aelfrice.store` carry a per-row CRDT version vector keyed by scope id. A stable per-DB `local_scope_id` (32-char hex) is generated on first v1.5+ open and persisted in `schema_meta`; the local-write rule `vv[local_scope] += 1` runs on every belief / edge insert and update via `ON CONFLICT DO UPDATE`. New read API: `MemoryStore.local_scope_id`, `get_belief_version_vector(belief_id)`, `get_edge_version_vector(src, dst, type)` — all returning `{scope_id: counter}` dicts. Backfill: opening a pre-#204 DB stamps `{local_scope: 1}` on every existing row via `INSERT OR IGNORE`; the `version_vector_backfill_complete` schema-meta marker short-circuits subsequent opens. No comparison logic ships at v1.5; the schema accumulates the metadata so v3 federation reconcile can detect concurrent SUPERSEDES across scopes instead of silently losing one to a wall-clock tiebreak. 15 deterministic acceptance tests in `tests/test_version_vectors.py`. The Belief / Edge dataclasses and the public `retrieve()` surface are unchanged; `delete_belief` / `delete_edge` deliberately leave version rows in place for v3 reconcile-time GC.

- **Bash matcher opt-in surface, telemetry, and doctor integration (v1.5.0)** ([#155](https://github.com/robotrocketscience/aelfrice/issues/155)). Completes the v1.5.0 Bash extension split (PR 2/2; parser/allowlist in #213). Three surfaces added. (1) **Telemetry ring buffer**: `hook_search_tool.py` appends one JSONL record per Bash-matcher fire to `<git-common-dir>/aelfrice/telemetry/search_tool_hook.jsonl` (schema: `timestamp`, `session_id`, `command`, `query`, `latency_ms`, `injected_l1`, `injected_l0`). Ring cap: 1000 entries; oldest evicted via atomic tempfile+`os.replace`. Write is fail-soft (one stderr trace on any OS error; hook continues). Public `read_telemetry(path) -> list[dict]` raises `ValueError` on corrupt JSON, returns `[]` for missing file. (2) **`aelf setup --search-tool-bash`** installs a separate `PreToolUse:Bash` hook entry (same `aelf-search-tool-hook` entry point; `matcher="Bash"` field distinguishes it from the v1.2.x `Grep|Glob` entry). **`aelf setup --no-search-tool-bash`** removes it. Both are idempotent and independent of `--search-tool`. `aelf unsetup --search-tool-bash` also removes the entry. (3) **`aelf doctor` telemetry section**: new `diagnose_search_tool_telemetry()` computes rolling p50/p95 latency and injection-noise rate from the ring buffer; `format_report()` renders a `search_tool_hook telemetry:` block. Missing/empty file → "no fires recorded". Corrupt file → CORRUPT sentinel in output. 44 new deterministic tests across `tests/test_search_tool_hook_bash_telemetry.py`, `tests/test_aelf_setup_search_tool_bash.py`, and `tests/test_aelf_doctor_search_tool_telemetry.py`.

- **BM25F sparse-matvec retrieval lane (v1.5.0)** ([#148](https://github.com/robotrocketscience/aelfrice/issues/148)). New module `aelfrice.bm25` carries a BM25F-augmented inverted index (Robertson 2004 stream-replication form) with a single sparse matvec query path. Each belief's indexed document is its own tokens plus the concatenation of incoming-edge `anchor_text` tokens replicated by `anchor_weight` (default `W=3`); standard BM25 length-normalisation absorbs the inflated token count correctly. The `(n_docs × n_terms)` scipy.sparse CSR `tf` matrix, per-doc length vector `dl`, and per-term `idf` vector are precomputed at index time; per-query cost is one sparse matvec plus a length-normalisation broadcast. `BM25Index.serialize` / `deserialize` produce deterministic byte payloads. `BM25IndexCache` subscribes to the store's invalidation callback for rebuild-on-mutation. New `MemoryStore.iter_incoming_anchor_text()` and `MemoryStore.list_beliefs_for_indexing()` helpers expose the data the index needs. The `retrieve()`, `retrieve_with_tiers()`, `retrieve_v2()`, and `RetrievalCache.retrieve` surfaces gain a `use_bm25f_anchors` flag (renamed from the transitional `bm25f_enabled` per #232's flag-name policy), plus the `AELFRICE_BM25F` env var and `[retrieval] use_bm25f_anchors` TOML key, all default-OFF. The FTS5 BM25 path stays the L1 default at v1.5.0 so the v1.4 byte-identical contract holds; the composition default flip is gated on the v1.5.0 composition tracker (#154). Test suite covers the seven acceptance criteria in `tests/test_bm25_index.py` (build dimensions, augmented-doc length invariant, W=0 FTS5 equivalence, vocab-shift recovery with W=3, perf gate via `--run-perf`, rebuild ≤ 1s at N=10k, deterministic serialise round-trip).

### Changed

- **numpy and scipy promoted to runtime dependencies** ([#148](https://github.com/robotrocketscience/aelfrice/issues/148)). The zero-runtime-deps invariant carried by v1.0–v1.4 is broken deliberately to support the BM25F sparse-matvec retrieval lane. The v1.x "no GPU, no network" promise still holds, but the install footprint is no longer stdlib-only — scipy pulls native BLAS / libgfortran. Existing optional extras (`mcp`, `onboard-llm`, `archive`, `benchmarks`) are unchanged.

### Fixed

- **Dynamic-trigger revisit — outcome 2 re-park (v1.5.0)** ([#188](https://github.com/robotrocketscience/aelfrice/issues/188)). The v1.5 captured-corpus retest of the v1.4-parked dynamic-mode trigger candidates lands as **outcome 2 (re-park)**. `benchmarks/context_rebuilder/dynamic_probe.py` gains a `--corpus DIR` mode (mutually exclusive with the existing positional fixture arg) that runs every `*.jsonl` under the directory and emits an aggregate verdict. Lab-side captured corpus was empty at the v1.5 ship-gate (only `.gitkeep`), so the probe ran against the existing synthetic fixture; neither candidate cleared the spec gate (≥ 5% absolute fidelity over threshold at same-or-lower token cost). `rate_of_growth` returned ratio 1.461 (fails the same-or-lower cost requirement; threshold reference is 1.017). `entity_density_delta` returned fidelity delta +0.0147 (below the +0.05 bar). `benchmarks/context-rebuilder/calibration_v1_5_0_dynamic.json` records the result with `aggregate_verdict = "park"` and `corpus = "<lab-side>"`; `synthetic_reference` embedded for traceability. `parked v1.5` log line and hook no-op are unchanged. `docs/context_rebuilder.md § Dynamic mode` footnoted with the v1.5.x re-eval prerequisite (populate captured corpus first).

## [1.4.0] - 2026-04-28

### Added

- **Conventional-commit prefix enforcement in CI + PR-body issue-link advisory** ([#169](https://github.com/robotrocketscience/aelfrice/issues/169), [#170](https://github.com/robotrocketscience/aelfrice/issues/170)). Two new jobs added to `.github/workflows/staging-gate.yml`. `commit-msg-prefix` iterates every commit in the PR range (`base..head`) and calls `scripts/check-commit-msg.py --subject <subject>` for each; the job fails if any commit has an invalid prefix — `Merge` and `Revert` auto-subjects are exempt. `scripts/check-commit-msg.py` is also wired as a local `commit-msg` hook via `.githooks/commit-msg`; run `sh scripts/setup-hooks.sh` once after cloning to install it. `pr-body-issue-link` checks whether the PR body contains a GitHub auto-close keyword (`Closes #N`, `Fixes #N`, `Resolves #N`, etc.) or an opt-out marker (`<!-- no-issue -->`); the job always exits 0 — it is advisory only, emitting a `::warning::` annotation when neither pattern is found. 29 deterministic unit tests in `tests/test_check_commit_msg.py` cover the prefix validator.

- **Rebuilder trigger modes — manual + threshold (+ dynamic parked v1.5)** ([#141](https://github.com/robotrocketscience/aelfrice/issues/141), [docs/context_rebuilder.md](docs/context_rebuilder.md)). v1.4.0 milestone: three trigger-mode surfaces for the v1.4 context rebuilder, sequenced per spec. **Manual** ships first as the explicit testing surface — `/aelf:rebuild` (new slash-command file at `src/aelfrice/slash_commands/rebuild.md`) and the now-visible `aelf rebuild` CLI subcommand both drive the same `rebuild_v14()` codepath. **Threshold** mode auto-fires on PreCompact: new `[rebuilder] trigger_mode` and `[rebuilder] threshold_fraction` keys in `.aelfrice.toml` (defaults `"manual"` and `0.6`); the threshold default is **sourced from eval-harness calibration**, not hand-picked — `benchmarks/context_rebuilder/calibrate.py` sweeps thresholds 0.5/0.6/0.7/0.8/0.9 against the bundled synthetic fixture, scores each post-clear assistant turn by content-overlap on the `<retrieved-beliefs>` sub-block, and picks the threshold maximizing fidelity-per-token-cost (rounded to 3dp; lowest threshold wins ties). The committed `benchmarks/context-rebuilder/calibration_v1_4_0.json` is the reproducible source-of-truth; `tests/test_rebuilder_triggers.py::test_tm3_calibration_is_reproducible_byte_for_byte` regression-tests the JSON against a re-run. **Dynamic** mode is **parked for v1.5** — `benchmarks/context_rebuilder/dynamic_probe.py` measured two heuristic candidates (rate-of-context-growth, entity-density-delta) against the same fixture and neither cleared the spec ship-gate (≥ 5% absolute fidelity over threshold at same-or-lower token cost). Setting `trigger_mode = "dynamic"` at v1.4 logs a `parked v1.5` trace to stderr and no-ops the hook. The `aelf rebuild` CLI subcommand was promoted from hidden (`help=argparse.SUPPRESS`) to visible because it is now the user-facing manual-trigger surface; the `tests/test_slash_commands.py` slash-surface contract gains `rebuild` in the EXPECTED_COMMANDS tuple. 16 new deterministic acceptance tests in `tests/test_rebuilder_triggers.py` cover the full trigger-mode matrix. Pyright strict-clean on every touched file.

- **PreCompact hook + rebuild logic, augment mode** ([#139](https://github.com/robotrocketscience/aelfrice/issues/139), [docs/context_rebuilder.md](docs/context_rebuilder.md)). v1.4.0 milestone: replaces the v1.2.0a0 alpha's per-token union retrieval workaround with the v1.3 `retrieve()` codepath (L0 + L1 + L2.5 in one call). New `aelfrice.context_rebuilder.rebuild_v14()` pure function packs L0 locked beliefs first (full, never trimmed), then session-scoped beliefs whose `session_id` matches the latest transcript turn's session, then the L2.5/L1 tail from `retrieve()` — within a configurable token budget. Query string is built from entity + triple extraction over the recent-turn window (no LLM). `aelfrice.context_rebuilder.main()` is the new module-level Claude Code PreCompact hook entry point; `aelfrice.hook.pre_compact()` continues to dispatch through the same logic and now wraps output in the harness's `hookSpecificOutput.additionalContext` JSON envelope. New `[rebuilder] turn_window_n` and `[rebuilder] token_budget` keys in `.aelfrice.toml` (defaults 50 and 4000); CLI flags on `aelf rebuild --n` / `--budget` override per-call. `aelf rebuild` now drives the same `rebuild_v14()` codepath the hook uses. `aelf setup --rebuilder` (already shipped at v1.2.0a0) installs the PreCompact hook idempotently. Augment-mode only: both the harness's compaction summary and the rebuild block land in the new context. Suppress mode is parked for v2.x. Empty-transcript / missing-store edge cases exit 0 with no `additionalContext` written. Reproducible: same transcript tail + same store state → byte-identical envelope. Latency budget: median ≤ 200 ms on a 10k-belief store; measured ~2 ms on a workstation. 15 new deterministic tests in `tests/test_context_rebuilder_hook.py` cover ordering, edge cases, reproducibility, latency, the `[rebuilder]` config parser, the JSON envelope shape, and session-scoping invariants.

- **Partial Bayesian-weighted ranking (v1.3.0)** ([#146](https://github.com/robotrocketscience/aelfrice/issues/146), [docs/bayesian_ranking.md](docs/bayesian_ranking.md)). L1 BM25 ranking now consumes the Beta-Bernoulli posterior log-additively per the spec's adopted Path B contract: `score = log(-bm25_raw) + posterior_weight * log(posterior_mean(α, β))`. `posterior_weight` defaults to `0.5` (the synthetic-graph optimum from the v1.3 calibration); `0.0` reproduces v1.0.x BM25-only ordering byte-for-byte (regression-tested). Locked beliefs (L0) bypass scoring entirely; L2.5 entity-index hits and L3 BFS expansions are unaffected — the weight only reranks the L1 candidate set. New `scoring.partial_bayesian_score(bm25_raw, alpha, beta, posterior_weight)` reuses `scoring.posterior_mean` (Jeffreys prior `α / (α+β)`); the Laplace `(α+1) / (α+β+2)` form sketched in #151 is explicitly rejected at this layer per spec rationale. New `MemoryStore.search_beliefs_scored(query, limit) -> list[tuple[Belief, float]]` exposes the FTS5 BM25 score; `MemoryStore.search_beliefs` is unchanged. `retrieve()`, `retrieve_with_tiers()`, and `retrieve_v2()` gain a `posterior_weight: float | None` kwarg. New `aelfrice.retrieval.resolve_posterior_weight()` resolves precedence env > kwarg > TOML > default; `AELFRICE_POSTERIOR_WEIGHT=<float>` env override and `[retrieval] posterior_weight = <float>` in `.aelfrice.toml`. Negative values clamp to `0.0`. `bm25 == 0` (FTS5 non-match) is floored at `PARTIAL_BAYESIAN_BM25_FLOOR = 1e-12` so `log(0)` cannot raise. `RetrievalCache` key tuple gains `posterior_weight` (rounded to four decimals via `POSTERIOR_WEIGHT_KEY_PRECISION`) so two callers passing different weights against the same store do not collide; cache invalidation is unchanged — `apply_feedback`'s `store.update_belief()` already triggers `_fire_invalidation()` and wipes the cache, no new hook in `apply_feedback`. 22 deterministic acceptance tests in `tests/test_bayesian_ranking.py` cover the 14-criterion spec (byte-identical v1.0.x at weight 0.0; equal-BM25 reranked by posterior DESC; high-BM25/low-posterior dethroned by low-BM25/high-posterior; one `apply_feedback(+1)` round promotes a rank-3 belief to ≤ 2 at default weight; lock bypass invariant across weights; cold-belief neutrality at all-prior corpus; cache hit/miss matrix; cache wiped through store callback without direct `cache.invalidate()`; bm25=0 edge case finite). Full feedback-into-ranking eval (10-round MRR uplift, ECE calibration, BM25F + heat-kernel composition, real-feedback retest) lands at v2.0.0.

### Fixed

- **`project-warm`: sentinel debounce keyed off git-common-dir, not worktree path** ([#161](https://github.com/robotrocketscience/aelfrice/issues/161)). Previously `_project_id` was derived from `git rev-parse --show-toplevel`, giving each worktree of the same repo a distinct sentinel under `~/.aelfrice/projects/<id>/.last_warm`. Two worktrees of one repo share a single DB (via `git-common-dir`), so they should share one sentinel. `resolve_project_root` now calls `git rev-parse --path-format=absolute --show-toplevel --git-common-dir` in a single subprocess and keys `ProjectRef.id` off the git-common-dir while keeping `ProjectRef.root` as the worktree working directory (for `os.chdir` in `_warm_store`). New test `test_resolve_project_root_worktrees_share_id` verifies that two worktrees of one repo produce identical `ProjectRef.id` values.
- **`aelf --advanced` / `aelf --help --advanced`** ([#159](https://github.com/robotrocketscience/aelfrice/issues/159)). The README promised `aelf --help --advanced` would reveal hidden subcommands, but the flag was never wired. `main()` now pre-scans `argv` for `--advanced` before building the parser; when present, it builds a second parser with `show_advanced=True` (plain `HelpFormatter` instead of `_SuppressSubparsersFormatter`) and prints the full subcommand list — including `project-warm`, `session-delta`, `rebuild`, `bench`, `migrate`, and all other `help=argparse.SUPPRESS` verbs — then exits 0. `aelf --advanced` alone and `aelf --help --advanced` behave identically. The existing `_SuppressSubparsersFormatter` hiding mechanism is unchanged. 16 new deterministic tests in `tests/test_cli_advanced_help.py`.

### Added

- **BFS multi-hop graph traversal (L3)** ([#144](https://github.com/robotrocketscience/aelfrice/issues/144), [docs/bfs_multihop.md](docs/bfs_multihop.md)). New L3 retrieval tier layered on top of L0+L2.5+L1. From each tier-0 belief that survives the prior tiers' budget cuts, walk outbound edges up to a bounded depth and a bounded node-expansion budget, scoring each visited belief by the product of edge weights along the path that reached it. New stdlib-only module `aelfrice.bfs_multihop` exposes `expand_bfs(seeds, store, *, max_depth=2, nodes_per_hop=16, total_budget=32, min_path_score=0.10) -> list[ScoredHop]` plus a module-level `BFS_EDGE_WEIGHTS` dict (SUPERSEDES 0.90, CONTRADICTS 0.85, DERIVED_FROM 0.70, SUPPORTS 0.60, CITES 0.40, RELATES_TO 0.30) — the table biases the frontier toward decisional edges and is monkey-patchable in tests. Determinism: cycle detection via per-call visited-set initialised from seed ids; per-hop edge ranking and final result ordering both tie-break on belief id ASC. `retrieve()` and `retrieve_v2()` gain `bfs_enabled` (kwarg) plus four BFS tuning knobs; default-OFF at v1.3.0 via `[retrieval] bfs_enabled = false` in `.aelfrice.toml`, opt in via `AELFRICE_BFS=1` env var or explicit kwarg. `RetrievalResult.bfs_chains` (placeholder since v1.0.x) is now populated with the edge-type path that reached each expansion belief. `RetrievalCache` key extended with the BFS flag so the two-flag matrix doesn't collide. Cache invalidation is zero-effort: the v1.0.1 wipe-on-write policy on `insert_edge` / `update_edge` / `delete_edge` already covers BFS correctness — no cache-key change needed. Default-OFF contract is regression-tested: with `bfs_enabled=False` the output is byte-identical to the v1.3.0 L0+L2.5+L1 baseline. 30 deterministic acceptance tests in `tests/test_bfs_multihop.py` covering decisional / pruned-informational / contradiction fixtures, edge-weight ordering, cycle detection, budget enforcement, max-depth cap, min-path-score pruning, determinism, default-off byte-identical retrieve, cache invalidation, latency-band guard on a 1k-belief / 5k-edge synthetic store, and the v1.3 latest-serial-per-hop temporal-coherence contract from [LIMITATIONS.md § BFS multi-hop temporal coherence](docs/LIMITATIONS.md#bfs-multi-hop-temporal-coherence) (carried forward to v2.0). No new third-party deps; pure Python over the existing `MemoryStore.edges_from()` API.

- **LLM-Haiku onboard classifier (opt-in)** ([#145](https://github.com/robotrocketscience/aelfrice/issues/145), [docs/llm_classifier.md](docs/llm_classifier.md)). New `[onboard-llm]` optional extra adds the `anthropic>=0.40` SDK; default install does NOT pull it. New `aelf onboard --llm-classify` flag opts into the LLM-Haiku classifier as an alternative to the regex `classify_sentence` path; `[onboard.llm].enabled = true` in `.aelfrice.toml` is the sticky equivalent. Flag wins on conflict. Default-OFF in every code path. Four-gate boundary policy guarantees zero outbound calls without explicit opt-in: (1) `[onboard-llm]` extra installed, (2) `ANTHROPIC_API_KEY` set in env, (3) flag or config block, (4) one-time per-machine consent prompt accepted (sentinel at `~/.aelfrice/llm-classify-consented`, invalidated by new model id or new aelfrice MAJOR version). `aelf onboard --llm-classify --dry-run` prints the candidates that would be sent without contacting the network and without writing the sentinel; `aelf onboard --revoke-consent` clears the sentinel. Failure semantics: 401/403 auth errors exit 1 with no fallback; transient errors (connect/timeout/429/5xx/malformed) fall back to the regex classifier with a `feedback_history` audit row tagged `onboard.llm.fallback:<reason>`; token-cap exceedance aborts with exit 1 and partial inserts that an idempotent re-run resumes via the deterministic belief id. Telemetry stdout-only (`onboard.llm: model=... input_tokens=N output_tokens=N total_tokens=N requests=N fallbacks=N`); the "no telemetry" stance in PRIVACY.md is preserved — aelfrice does not phone-home about its own LLM usage. New `aelfrice.llm_classifier` module exposes `LLMConfig`, `ScannerRouter`, `check_gates`, `prompt_for_consent`, `read/write/revoke_sentinel`, `classify_batch`, `format_telemetry_line`. `scan_repo` gains an optional `llm_router` keyword argument; default `None` keeps the v1.0/v1.2 regex behaviour. 28 acceptance tests in `tests/test_llm_classifier.py` cover all 14 spec criteria including a tripwire test asserting "default install + no flag → zero outbound calls." Privacy: [docs/PRIVACY.md § Optional outbound calls](docs/PRIVACY.md). Config: [docs/CONFIG.md § `[onboard.llm]`](docs/CONFIG.md).

- **Entity-index retrieval (L2.5)** ([#143](https://github.com/robotrocketscience/aelfrice/issues/143), [docs/entity_index.md](docs/entity_index.md)). New retrieval tier between L0 (locked beliefs) and L1 (FTS5 BM25). On every `insert_belief` / `update_belief` the new `aelfrice.entity_extractor.extract_entities` runs nine regex patterns over the belief content (file paths, identifiers, branch names, version strings, URLs, error codes, noun phrases) and writes one row per match into a new `belief_entities` table (composite PK `(belief_id, entity_lower, span_start)`; indexes on `entity_lower`, `belief_id`, `kind`). At read time, `retrieve()` extracts entities from the query, looks them up via `MemoryStore.lookup_entities`, ranks by entity-overlap count, and returns matched beliefs above L1 in the output. Default-on at v1.3.0 via `[retrieval] entity_index_enabled = true` in `.aelfrice.toml`; disable via `AELFRICE_ENTITY_INDEX=0` env var or explicit `entity_index_enabled=False` kwarg. Default token budget rises from 2,000 to 2,400 (L2.5 sub-budget 400 + unchanged L1 sub-budget 2,000); when the flag is disabled the default snaps back to 2,000 byte-for-byte. New `schema_meta` key/value table tracks the one-shot `entity_backfill_complete` marker — first v1.3+ open of any pre-v1.3 store re-extracts entities for every existing belief; idempotent on re-run via the composite PK. New `benchmarks/mab_entity_index_adapter.py` reports per-question L0/L1/L2.5 counts and an aggregate L2.5 hit rate. 56 deterministic acceptance tests in `tests/test_entity_index.py` (one section per AC in the spec). No new third-party deps; pure regex + sqlite3.

- **Context-rebuilder continuation-fidelity scorer** ([#138](https://github.com/robotrocketscience/aelfrice/issues/138)). New `benchmarks/context_rebuilder/score.py` module implements the v1.4.0 answer-match metric on top of the #136 scaffolding. `score_continuation_fidelity(replay_result, *, fixture_turn_texts, post_clear_answers=None, method='exact') -> FidelityScore` compares each post-clear assistant turn's answer against the original session's answer at the same turn and aggregates per-turn binary match scores to a single fidelity ∈ [0, 1]. Method shipped at v1.4.0: `exact` -- deterministic, reproducible, no outbound calls, no extra deps. Comparison runs on a normalized form (Unicode NFC + lowercase + whitespace collapse + strip). Known false-positive modes (regression-shaped restatement, quoted-from-rebuild) and false-negative modes (paraphrase, trailing differences, numeric formatting) documented in the module docstring and in `benchmarks/context-rebuilder/README.md`. `embedding` and `llm-judge` are accepted at the type-literal level for forward-compat but raise `NotImplementedError` -- both parked for v1.5.x (LLM-judge introduces an eval-time outbound call; embedding adds a heavyweight optional dep). The harness's JSON output schema gains a top-level `continuation_fidelity` sub-object alongside `token_budget_delta` and `hook_latency_ms` -- the three v1.4 ship-gate metrics now travel together. `replay.run()` accepts new `score_method` and `post_clear_answers` parameters; the CLI exposes `--score-method {exact,embedding,llm-judge}` with `exact` as the default. Vacuous cases (no clear injected, or no post-clear assistant turns) return `score=1.0` with `n_post_clear_assistant_turns=0` by documented convention. Reproducibility contract pinned by `tests/test_continuation_fidelity_scorer.py::test_score_is_reproducible_across_runs`. 20 new deterministic acceptance tests (~0.06s total). Pyright strict-clean. The rebuilder hook itself (which would feed real agent output into `post_clear_answers`) is [#139](https://github.com/robotrocketscience/aelfrice/issues/139), tracked separately.

- **Context-rebuilder eval-harness scaffolding** ([#136](https://github.com/robotrocketscience/aelfrice/issues/136)). New importable Python package `benchmarks.context_rebuilder` (sibling of the v1.2.0-shipped hyphenated `benchmarks/context-rebuilder/` skeleton) carrying the v1.4.0 acceptance-criterion scaffolding: `replay.py` reads a `turns.jsonl` synthetic fixture and walks per-turn agent state, returning a `ReplayResult` with per-turn `token_budget_delta` (signed cumulative-token delta) and `hook_latency_ms` (wall-clock from PreCompact-hook fire to rebuild-block emit). `inject.py` exposes `ClearInjection` and `midpoint_clear` for forcing a synthetic context-clear at a configurable content-turn index. `measure.py` exposes `estimate_tokens` (4 chars/token heuristic, aligned with `aelfrice.context_rebuilder._CHARS_PER_TOKEN`), `token_budget_delta`, and `hook_latency_ms` (monotonic non-negative by construction). `__main__.py` drives `python -m benchmarks.context_rebuilder.replay <fixture> [--clear-at N] [--rebuild-overhead-tokens N] [--out PATH]`; both `python -m benchmarks.context_rebuilder` and `python -m benchmarks.context_rebuilder.replay` route to the same argparse surface. Missing / empty / directory / markers-only fixture inputs raise `FixtureError` (clear error, no crash); malformed JSON lines are skipped silently and counted in `n_skipped_lines`. New `benchmarks/context-rebuilder/fixtures/synthetic/debugging_session_001.jsonl` is a 16-turn deterministic synthetic transcript matching the v1.2.0 transcript-ingest schema; `fixtures/README.md` documents the synthetic-only public-repo policy per `docs/eval_fixture_policy.md`. 26 deterministic unit tests in `tests/test_context_rebuilder_harness.py` (0.05s total). **Scaffolding only** -- continuation-fidelity scoring is [#138](https://github.com/robotrocketscience/aelfrice/issues/138), tracked separately.

- **`aelf session-delta --id <session_id>`** ([#140](https://github.com/robotrocketscience/aelfrice/issues/140)). Hidden SessionEnd-hook entry point. New module `aelfrice.telemetry` exposes `compute_session_delta(store, session_id, *, telemetry_path, now) -> dict` (pure function) and `emit_session_delta(session_id, *, store, path, now) -> None` (append to file). The writer reads beliefs tagged with `session_id` to compute per-session deltas (`beliefs_created`, `corrections_detected`, `feedback_given`, `velocity_items_per_hour`, `velocity_tier`, `duration_seconds`), snapshots the current store state for the `beliefs` and `graph` blocks, and computes 7-day and 30-day rolling-window rollups by walking the existing `telemetry.jsonl` tail. Schema matches the v=1 format already produced by the HOME-repo hook. Missing or empty `--id` is a stderr-warn no-op (exit 0) so hook failures never surface as broken shell sessions. Idle sessions (zero beliefs) still emit a zero-row so `len(telemetry.jsonl)` equals session count. `window_7` / `window_30` rollups now reflect real per-session activity instead of being static. 10 new tests in `tests/test_telemetry_session_delta.py`.

- **`aelf project-warm <path>`** ([#137](https://github.com/robotrocketscience/aelfrice/issues/137)). Hidden CwdChanged-hook entry point. Resolves a path to a project root (git work-tree via `git rev-parse --show-toplevel` — worktrees correctly resolve to the worktree, not the main checkout — or first ancestor with a `.aelfrice/projects/<id>/` provisioned layout) and pre-loads the project's SQLite + OS file-cache pages so the next `aelf` invocation hits warm storage. Idempotent + 60s-debounced via a unix-ts sentinel at `~/.aelfrice/projects/<id>/.last_warm`. Deny-glob defaults catch `/tmp/**`, `/var/folders/**`, `~/Downloads/**`, `~/Desktop/**`; configurable via `~/.aelfrice/config.json` (`project_warm.deny_globs`). The CLI surface is silent on every code path — unknown projects, denied paths, debounced calls, and any unexpected exception all return exit 0 with empty stdout. The companion `~/.claude/hooks/aelfrice-cwd-warm.sh` lives in the HOME repo and is out of scope for this release. New module `aelfrice.project_warm` exposes `resolve_project_root`, `warm_project`, `warm_path`, `WarmResult`, `ProjectRef`, `WarmConfig`. 19 deterministic unit tests cover the full matrix.

### Performance

- **`update-check`: TTL shortened from 24h to 6h** ([#172](https://github.com/robotrocketscience/aelfrice/issues/172)). Default update-check TTL drops from 24 hours to 6 hours so users on a daily cadence notice new releases sooner without overwhelming the version-manifest endpoint. Existing TTL override paths are unchanged.

### CI

- **GitHub Actions burn reduction — concurrency + dedup pytest + uv cache** ([#184](https://github.com/robotrocketscience/aelfrice/pull/184)). Three structural CI tweaks landed together. (1) `concurrency` group + `cancel-in-progress: true` on both `CI` and `Staging Gate` workflows so rapid-push streaks no longer run superseded jobs to completion. (2) Drop the duplicate `pytest 3.12 + 3.13` matrix from `Staging Gate` — pytest now runs only on `CI`; `Staging Gate` keeps its unique jobs (gitleaks, pattern-scan, history-scan, release-docs-check, commit-msg-prefix, pr-body-issue-link). (3) `enable-cache: true` on `setup-uv`, keyed on `uv.lock`, so pytest legs hit a warm uv cache on most pushes. Net effect: ~50% fewer pytest jobs per PR push; sharply lower wall-clock on cache hits. Branch-protection note for the post-public-flip required-checks list: use `CI / pytest`, since `Staging Gate / pytest` no longer exists.

## [1.2.0] - 2026-04-28

Major release. Auto-capture pipeline (transcript-ingest, commit-ingest, SessionStart hooks), the v1.1.0-designed `agent_inferred → user_validated` promotion path, the triple extractor, ingest-enrichment schema, batch-ingest of historical Claude Code JSONLs, the harness-integration guide, the `INEDIBLE` per-file privacy opt-out, and the v1.3-planned CLI consolidation rolled forward into this release. 16 PRs landed since v1.1.0. Folds in everything that shipped under the v1.2.0a0 alpha (#109) — that pre-release is now superseded; users should upgrade directly to 1.2.0.

### Added

- **CLI surface consolidation** ([#127](https://github.com/robotrocketscience/aelfrice/pull/127), [#129](https://github.com/robotrocketscience/aelfrice/pull/129)). The user-facing surface drops from 22 verbs to 14 listed in `--help` without removing capability. `aelf stats` is renamed to `aelf status` (counts snapshot); `aelf health` folds into `aelf doctor graph`; `aelf doctor` grows a positional `[hooks|graph]` scope arg and runs both checks when no scope is given. Hidden via `help=argparse.SUPPRESS`: `rebuild`, `statusline`, `bench`, `regime`, `migrate`, `unsetup`, plus the back-compat aliases `health` and (old) `stats`. Custom `_SuppressSubparsersFormatter` filters argparse's leaked `==SUPPRESS==` literal. Slash commands track only the user-facing surface — `bench.md`, `health.md`, `migrate.md`, `rebuild.md`, `regime.md`, `statusline.md`, `stats.md`, `unsetup.md` deleted at this release. Aliases live one minor and are deleted at v1.3+. Design memo: [`docs/CLI_SURFACE_AUDIT.md`](docs/CLI_SURFACE_AUDIT.md).
- **`INEDIBLE` filename marker** ([#129](https://github.com/robotrocketscience/aelfrice/pull/129)). Per-file privacy opt-out. Any file or directory whose basename contains `INEDIBLE` (case-sensitive, all caps, anywhere in the basename) is unconditionally skipped by every aelfrice ingest path: `scan_repo` filesystem walk, `scan_repo` AST walk, `ingest_jsonl` single-file path, and `ingest_jsonl_dir` batch path. Examples that match: `INEDIBLE.md`, `INEDIBLE_secrets.txt`, `notes_INEDIBLE.txt`, `partINEDIBLEpart.py`. Lowercase variants do not match — case sensitivity is the deliberate discoverability cue. The check is on the basename only; when `is_inedible(path)` returns True, aelfrice does not open, read, or hash the file. New `aelfrice.inedible` module exposes `INEDIBLE_MARKER` + `is_inedible(path)`. `IngestJsonlBatchResult` gains a `files_skipped_inedible` counter. Documented in `docs/PRIVACY.md § Per-file opt-out`.
- **`aelf doctor` now flags hooks that wrap a script in `2>/dev/null || true`** ([#113](https://github.com/robotrocketscience/aelfrice/issues/113), [#114](https://github.com/robotrocketscience/aelfrice/issues/114)). Previously the shell-meta heuristic short-circuited before extracting the script path, so a stale `bash /abs/path.sh 2>/dev/null || true` hook entry — like the one #113 reproduces against the long-deleted `hook-aelf-search.sh` — was reported as `skipped` instead of `broken`. The check now extracts the script path even with a trailing wrapper and reports it as broken when the file is missing. Hooks using the silent-failure pattern are additionally surfaced as a soft warning regardless of whether the underlying script resolves, with a fix-hint pointing at `~/.aelfrice/logs/hook-failures.log`. Doctor also now tails that log when present, so future bash hook wrappers that redirect stderr into it have a discovery path back to the user.
- **Empty-store onboarding signals across `aelf health`, `aelf doctor`, `aelf search`, and `aelf setup`** ([#116](https://github.com/robotrocketscience/aelfrice/issues/116)). `aelf health` now runs a `corpus_volume` audit check that fires `severity='warn'` when belief count is below `AELFRICE_CORPUS_MIN` (default 50) AND the project is at least seven days old (resolved from `git log --reverse --max-parents=0`). The warning is informational — it never affects the exit code, so CI consumers are unaffected. Brand-new projects and non-git directories never warn. `aelf doctor` prints a `store: N belief(s)` line at the end of its report and surfaces the same empty-store hint when the active project's DB is at zero. `aelf search` distinguishes "store is empty" from "no FTS5 match" so the user can tell whether their query missed or whether nothing has been indexed yet. `aelf setup` prints a one-line `next step: ... aelf onboard .` hint when the active store is empty after install. Resolves the dogfooding gap discovered when an aelfrice-lab project had only 5 beliefs after weeks of active use because the onboard step had no discovery path back to the user.
- **`aelf ingest-transcript --batch DIR [--since DATE]`** ([#115](https://github.com/robotrocketscience/aelfrice/issues/115)). Batch ingest of historical JSONL session logs into the active project's belief store. The path argument is now optional (mutually exclusive with `--batch`). `--batch DIR` recurses into DIR for every `*.jsonl` file; `--since YYYY-MM-DD` (or full ISO timestamp) filters by file mtime so incremental backfill is cheap. `ingest_jsonl` now auto-detects two formats per line: aelfrice's own transcript-logger `turns.jsonl` shape AND Claude Code's internal session-log shape at `~/.claude/projects/<encoded-cwd>/<session-id>.jsonl` (`{"type": "user", "message": {"role": ..., "content": ...}, "sessionId": ..., "timestamp": ...}` with `content` either a string or a `[{"type":"text","text":...}]` array). Idempotent on re-run thanks to existing per-`(source, sentence)` dedup. New `IngestJsonlBatchResult` aggregates per-file counts.
- **`aelf setup` historical-JSONL hint** ([#115](https://github.com/robotrocketscience/aelfrice/issues/115)). When `~/.claude/projects/` exists with at least one `*.jsonl`, setup prints a one-line count + the exact `aelf ingest-transcript --batch` command to backfill, plus a pointer at the privacy trade-off note. Counting is capped at 1000 to keep setup fast.
- **`aelf doctor` orphan-slash check** ([#115](https://github.com/robotrocketscience/aelfrice/issues/115)). Reads `~/.claude/commands/aelf/*.md`, compares the basenames against the running CLI's argparse subcommand registry, and surfaces any slash file naming a subcommand the CLI does not implement (the canonical case: a slash command is published in a docs branch but the corresponding CLI feature lives on a different branch). Informational only — never affects exit status. New `--known-cli-subcommands` parameter on `diagnose()` keeps the check off when callers don't ask for it (backwards-compat).
- **`docs/INSTALL.md § Batch ingest of historical sessions`**. New subsection covers `--batch`/`--since`, idempotency, format auto-detection, and the privacy trade-off (Claude Code session JSONLs may contain pasted secrets — there is no PII scrubber on the v1.2 ingest path; review before backfilling).

- **Ingest enrichment schema** ([docs/ingest_enrichment.md](docs/ingest_enrichment.md)). Three coupled additions: `DERIVED_FROM` edge type (valence 0.5, mirrors `CITES`); `anchor_text TEXT` column on `edges` carrying the citing belief's own phrasing of the relationship, capped at 1000 characters at the dataclass boundary; `session_id TEXT` column on `beliefs` plus a real `sessions` table (`MemoryStore.create_session` / `complete_session` go from no-op stubs to persisting). `ingest_turn` now writes `session_id` end-to-end. Forward-compatible with v1.0 stores: `ALTER TABLE` adds the columns on first open and is idempotent on re-open. The `idx_beliefs_session` index is sequenced after the migration so a v1.0 store can open at all. Producers that populate the new fields densely are the v1.2.0 transcript-ingest, commit-ingest, and triple-extraction paths.
- **Commit-ingest `PostToolUse` hook** ([docs/commit_ingest_hook.md](docs/commit_ingest_hook.md)). New `aelfrice.hook_commit_ingest:main` entry point fires after every successful `git commit` Bash call, parses `[branch shorthash]` from the commit's stdout, runs the triple extractor on the full commit message body (fetched via one `git log -1 --format=%B <hash>` subprocess), and inserts beliefs and edges under a deterministic session id `sha256(branch + ":" + commit_hash)[:16]`. The first ingest path that densely populates `Edge.anchor_text`, `Belief.session_id`, and `DERIVED_FROM` edges in production data, closing the v1.0 limitation that the graph only grew on explicit `aelf onboard` / `aelf remember` calls. Lazy imports keep cold-start cost off the latency budget; commit-message bodies are capped at 4 KB before extraction. Non-blocking on every failure path — a hook problem may NEVER cause `git commit` to feel broken. Wired via `aelf setup --commit-ingest` (opt-in at v1.2; default flip pending production-corpus latency telemetry); `aelf unsetup --commit-ingest` strips it. New `aelf-commit-ingest` project script registered in `pyproject.toml`. Setup machinery generalises `_get_event_list(data, event, ...)` so the same builders cover UserPromptSubmit, Stop, and PostToolUse-with-matcher entries; `_build_entry` gains an optional `matcher` field for PreToolUse / PostToolUse shapes. 15 unit tests + 1 latency regression test (in-process p95 < 200 ms across 20 iterations on a fixture commit).
- **Triple extractor** ([docs/triple_extractor.md](docs/triple_extractor.md)). New `aelfrice.triple_extractor` module with `extract_triples(text) -> list[Triple]` (pure regex over a fixed pattern bank — six relation families covering `SUPPORTS`, `CITES`, `CONTRADICTS`, `SUPERSEDES`, `RELATES_TO`, `DERIVED_FROM`, with active and passive forms) and `ingest_triples(store, triples, session_id=None) -> IngestResult` (resolves subject/object beliefs by content-hash lookup, creating with the project default prior when missing; inserts edges with `anchor_text` populated from the citing prose; idempotent on re-run; self-edges dropped). All triple-derived beliefs share a canonical id space via the `triple` source label, so the same noun phrase resolves to the same belief id across every extraction call site. Stdlib-only; no LLM, no POS tagger, no embedding. Reusable by every v1.x prose-ingesting caller — the v1.2.0 commit-ingest hook, transcript-ingest, manual `aelf remember` calls, and the v1.3.0 entity-index path. 38 tests cover the per-pattern templates, negative cases, anchor-text substring guarantee, idempotency, session_id propagation, and self-edge handling.
- **Transcript ingest** ([docs/transcript_ingest.md](docs/transcript_ingest.md)). New `aelfrice.transcript_logger:main` entry point handles four Claude Code hook events through one dispatch: `UserPromptSubmit` → append `{"role": "user", ...}`; `Stop` → read the last assistant message from the transcript path and append `{"role": "assistant", ...}`; `PreCompact` → write a `compaction_start` marker, rotate `turns.jsonl` into `archive/turns-<ts>.jsonl`, spawn `aelf ingest-transcript` detached; `PostCompact` → write a `compaction_complete` marker. JSONL lives at `<git-common-dir>/aelfrice/transcripts/turns.jsonl` (or `~/.aelfrice/transcripts/turns.jsonl` outside a git tree); `$AELFRICE_TRANSCRIPTS_DIR` overrides. Per-turn budget is sub-10ms p99 by skipping `git rev-parse` / `symbolic-ref` on the hot path; `cwd` and Claude Code's own `session_id` from the payload stay on the line. Non-blocking contract: every failure returns exit 0 with a stack trace on stderr.
- **`aelfrice.ingest.ingest_jsonl()`** reads a `turns.jsonl` archive and lowers each line through `ingest_turn`. Within a session, consecutive turns get a `DERIVED_FROM` edge linking turn N+1's last belief to turn N's last belief with `anchor_text` set to turn N's literal text. Idempotent. Returns `IngestJsonlResult(lines_read, turns_ingested, beliefs_inserted, edges_inserted, skipped_lines)`. Compaction markers and malformed lines count under `skipped_lines` and never raise.
- **`aelf ingest-transcript PATH` CLI** prints the `IngestJsonlResult` summary. Used as the detached spawn target of the `PreCompact` hook on rotation; also runnable manually for replay or recovery on archived transcripts.
- **`aelf setup --transcript-ingest`** wires the four logger hook events to the new `aelf-transcript-logger` entry point. Idempotent. `aelf unsetup --transcript-ingest` strips them. The flag is opt-in at v1.2.0.
- **`aelf-transcript-logger` project script** registered in `pyproject.toml`.
- Slash command `aelf:ingest-transcript`.
- **`agent_inferred → user_validated` promotion** ([docs/promotion_path.md](docs/promotion_path.md)). Implements the v1.1.0-designed promotion path. New `Belief.origin` schema column (`TEXT NOT NULL DEFAULT 'unknown'`) tags each belief with one of `user_stated`, `user_corrected`, `user_validated`, `agent_inferred`, `agent_remembered`, `document_recent`, `unknown`. Forward-compatible with v1.0/v1.1 stores: `ALTER TABLE` adds the column on first open, then a one-shot backfill flips locked rows to `user_stated` and correction rows to `user_corrected` (everything else stays `unknown` rather than retroactively claim `agent_inferred`). Producers tag origin explicitly: `scan_repo` writes `agent_inferred` on every onboard belief; `aelf lock` and the MCP `aelf:lock` write `user_stated`. New `aelfrice.promotion` module exposes `promote(store, belief_id)` and `devalidate(store, belief_id)` — provenance flip only, no math change (alpha/beta/lock_level/type unchanged). Each call writes one zero-valence audit row to `feedback_history` tagged `promotion:user_validated` or `promotion:revert_to_agent_inferred`. Idempotent; refuses locked beliefs and `user_stated` rows with a clear "demote first" message. New `aelf validate <belief_id> [--source LABEL]` CLI subcommand and `aelf:validate` MCP tool. `aelf demote` extended one-tier-per-call: drops a lock if locked, else flips `user_validated` → `agent_inferred`. The contradiction tie-breaker grows from three to five precedence classes (`user_stated > user_corrected > user_validated > document_recent > agent_inferred`); `precedence_class()` reads `belief.origin` first, with `lock_level=user` short-circuiting to `user_stated` regardless of origin so the v1.0.1 lock-always-wins invariant holds. `unknown` and `agent_remembered` fall through to `document_recent` (honest unknown bucket). 33 new tests across `tests/test_promotion.py`, `tests/test_contradiction.py`, `tests/test_cli.py`, and `tests/test_mcp_server.py`.
- **`docs/HARNESS_INTEGRATION.md`** ([docs/HARNESS_INTEGRATION.md](docs/HARNESS_INTEGRATION.md)). User-facing operational guide for running aelfrice alongside Claude Code's auto-memory. Documents three coexistence modes (default coexistence after `aelf setup --transcript-ingest`; aelfrice-canonical with a `~/.claude/CLAUDE.md` edit; aelfrice-only after disabling auto-memory) plus a migration recipe (`aelf onboard ~/.claude/projects/<slug>/memory`) for moving accumulated `.md` content into aelfrice as `agent_inferred` beliefs. Rewrites [docs/LIMITATIONS.md § harness conflict](docs/LIMITATIONS.md) to point at the v1.2.0 hook mitigation rather than the v1.0/v1.1 manual `CLAUDE.md` edit.
- **SessionStart hook**. New `aelfrice.hook.session_start()` entry point reads the SessionStart JSON payload (drained for protocol; no fields used at MVP), retrieves L0 locked beliefs via `retrieve()` with empty query, and emits an `<aelfrice-baseline>` block to stdout. Fires once per Claude Code session, before any user message, so the agent enters the session with durable user-asserted ground truth already in context. Distinct tags from UserPromptSubmit's `<aelfrice-memory>` so the model can tell channels apart: baseline = "stuff that's always true," memory = "stuff related to this prompt." Honors the non-blocking hook contract (returns 0 on every failure path; empty store / no locked beliefs / empty stdin / malformed payload all silent). Wires `install_session_start_hook` / `uninstall_session_start_hook` / `resolve_session_start_hook_command` in `aelfrice.setup` mirroring the UserPromptSubmit pair (idempotent, atomic-write, basename-match). Three event channels (UserPromptSubmit, transcript-ingest, SessionStart) coexist in the same `settings.json` without disturbing each other. New `--session-start` flag on `aelf setup` / `aelf unsetup` and new `aelf-session-start-hook` console script.
- **Context rebuilder MVP** ([#109](https://github.com/robotrocketscience/aelfrice/pull/109)). Originally shipped under the v1.2.0a0 alpha pre-release; now folded into v1.2.0 final. New `aelfrice.context_rebuilder.rebuild()` pure function takes a list of recent turns and an open `MemoryStore` and returns the formatted `<aelfrice-rebuild>` XML block with L0 locked beliefs + L1 BM25 hits via per-token union retrieval (works around the public store's AND-only FTS5 semantics). Two transcript adapters (`read_recent_turns_aelfrice` for canonical turns.jsonl, `read_recent_turns_claude_transcript` as fallback). `pre_compact()` hook entry-point in `aelfrice.hook` reads the PreCompact payload, locates a transcript, runs `rebuild()`, writes the block to stdout — non-blocking on every failure path. New `aelf-pre-compact-hook` console script + `install_pre_compact_hook` / `uninstall_pre_compact_hook` / `resolve_pre_compact_hook_command` setup pair. `aelf setup --rebuilder` installs the PreCompact hook alongside the UserPromptSubmit hook. `aelf rebuild` CLI subcommand (with `--transcript`, `--n`, `--budget`) manually emits the rebuild block. Augment-mode only at v1.2.0; suppress-mode coordination, posterior-weighted ranking, triple-extracted queries, session-scoped retrieval, and per-session-state trigger tuning all deferred to v1.3+.

## [1.1.0] - 2026-04-27

Minor release: project identity, structural auditor, onboarding
git-recency, legacy-DB migration tool, design memo for the v1.2.0
promotion path, worktree concurrency tests, and the `edges → threads`
user-facing rename. Eight PRs landed between v1.0.3 and v1.1.0.

### Added

- **Per-project DB resolution** ([#88](https://github.com/robotrocketscience/aelfrice/issues/88), PR [#96](https://github.com/robotrocketscience/aelfrice/pull/96)). v1.0.x stored everything in a single global `~/.aelfrice/memory.db`. v1.1.0 introduces a resolution chain in `cli.db_path()`: `$AELFRICE_DB` (override) → `<git-common-dir>/aelfrice/memory.db` (when `cwd` is in a git work-tree; resolved via `git rev-parse --path-format=absolute --git-common-dir`, so worktrees of one repo share one DB) → `~/.aelfrice/memory.db` (legacy fallback for non-git dirs). `.git/` is not git-tracked — the brain graph never crosses the git boundary. New `_git_common_dir()` helper shells out to `git rev-parse` and returns `None` gracefully when git is unavailable. No new runtime dependencies.
- **`aelfrice.auditor` module + rewritten `aelf health`** ([#90](https://github.com/robotrocketscience/aelfrice/issues/90), PR [#100](https://github.com/robotrocketscience/aelfrice/pull/100)). `audit(store) -> AuditReport` runs three mechanical structural checks: `orphan_threads` (edges whose src/dst no longer exists), `fts_sync` (`beliefs_fts` row count vs `beliefs`), `locked_contradicts` (pairs of locked beliefs joined by CONTRADICTS). `aelf health` exits 1 if any check fails. Informational metrics (counts, average confidence, credal gap, thread counts by type) print alongside but don't affect exit. The v1.0 regime classifier is preserved as a separate `aelf regime` command (always exit 0).
- **`aelf status`** ([#90](https://github.com/robotrocketscience/aelfrice/issues/90)). Alias for `aelf health` per lab `COMMAND_DESIGN.md`. Same handler, same exit codes.
- **`aelf regime`** ([#90](https://github.com/robotrocketscience/aelfrice/issues/90)). New command preserving the v1.0 regime classifier output (`supersede` / `ignore` / `mixed` / `insufficient_data`).
- **Store API additions** for the auditor: `count_orphan_edges()`, `count_fts_rows()`, `list_locked_contradicts_pairs()`, `count_edges_by_type()`. Read-only.
- **`aelf migrate`** ([#93](https://github.com/robotrocketscience/aelfrice/issues/93), PR [#104](https://github.com/robotrocketscience/aelfrice/pull/104)). One-shot copy from the legacy global `~/.aelfrice/memory.db` into the active project's resolved DB. Reads source via SQLite `mode=ro` URI (rejects writes at the SQLite layer). Default project-mention filter (belief content references the absolute project root); `--all` overrides. Edges follow only when both endpoints land. Dry-run by default; `--apply` writes. `--from PATH` overrides the source. Idempotent. Never deletes from the source. New module `aelfrice.migrate` with `default_legacy_db_path()` and `migrate(...) -> MigrateReport`.
- **`PRAGMA busy_timeout=5000`** in `MemoryStore.__init__` ([#89](https://github.com/robotrocketscience/aelfrice/issues/89), PR [#102](https://github.com/robotrocketscience/aelfrice/pull/102)). Required for safe multi-worktree access where two processes share one `.git/aelfrice/memory.db`. Without it, the second writer hits `database is locked` immediately instead of waiting.
- **Worktree concurrency test suite** at `tests/test_worktree_concurrency.py` ([#89](https://github.com/robotrocketscience/aelfrice/issues/89)). Five tests: WAL-on regression, busy_timeout regression, two-worktree DB-path identity, concurrent multi-process write correctness (40 beliefs across 2 spawn processes), and a three-round repeat to catch flakes.
- **Onboard git-recency weighting (Tier 2)** ([#94](https://github.com/robotrocketscience/aelfrice/issues/94), PR [#103](https://github.com/robotrocketscience/aelfrice/pull/103)). Scanner records the most-recent author date of the commit that touched each source file, then `scan_repo` writes that as `belief.created_at` so the existing decay mechanism (already in `scoring.py` at v1.0) penalises stale prose. New `_build_file_recency_map(root)` makes ONE `git log --name-only --pretty=format:%aI` call per scan and returns `{relative-path: most-recent-iso-date}`. `SentenceCandidate` gains an optional `commit_date: str | None = None` field. `extract_filesystem` and `extract_ast` accept the recency map and stamp candidates from recency-known files; `extract_git_log` carries each commit's own author date. Files outside git, untracked files, and the entire fallback when git is unavailable continue to use wall-clock `now`. No new ranking math.
- **`docs/promotion_path.md`** ([#95](https://github.com/robotrocketscience/aelfrice/issues/95), PR [#101](https://github.com/robotrocketscience/aelfrice/pull/101)). Design memo for the v1.2.0 `agent_inferred → user_validated` promotion path. Recommends adding `origin TEXT NOT NULL DEFAULT 'unknown'` to `beliefs` (v1.1.0 schema bump deferred to v1.2.0 implementation alongside the command), conservative backfill, flag-only flip mechanism (preserve `α/β` and `lock_level`), `aelf validate <belief_id>` CLI surface mirroring `aelf demote`, tie-breaker slot between `user_corrected` and `document_recent`, zero-valence audit row tagged `source='promotion:user_validated'`. 10 open TBDs explicitly enumerated.
- **`aelf` CLI grew from 12 to 19 subcommands.** Added: `health` (rewritten), `status` (alias), `regime`, `migrate`. Slash commands: `health.md` rewritten, new `status.md`, `regime.md`, `migrate.md`.
- **MCP `aelf:stats` returns `threads` key.** Same integer value as `edges`. `edges` retained for one minor.
- **MCP `aelf:health.features.thread_per_belief` field.** Same value as `edge_per_belief`. `edge_per_belief` retained for one minor.

### Changed

- **`edges` → `threads` user-facing rename** ([#92](https://github.com/robotrocketscience/aelfrice/issues/92), PR [#105](https://github.com/robotrocketscience/aelfrice/pull/105)). All user-facing surfaces — CLI output labels (`aelf stats`, `aelf health`, `aelf migrate`), slash command descriptions, COMMANDS.md / MCP.md prose, auditor finding strings — now use "threads". The internal SQLite `edges` table, the `Edge` Python dataclass, and the `EDGE_*` type constants are unchanged. The auditor's `metrics` dict keys (`threads`, `threads_supports`, `threads_contradicts`, …) follow the rename. Schema docs (ARCHITECTURE.md, design memos) keep `edge` / `Edge` because they describe internals.
- **Project-identity narrative reconciled** ([#99](https://github.com/robotrocketscience/aelfrice/pull/99)). Re-applies the framing fix dropped from #87's squash and sweeps post-#96 docs: ARCHITECTURE.md / COMMANDS.md / INSTALL.md § Database / PRIVACY.md / LIMITATIONS § Project identity all describe the new resolution chain. PRIVACY.md drops "to share across machines: sync the file" and links to LIMITATIONS § Sharing or sync of brain-graph content. INSTALL.md uninstall section uses "resolved DB path" language and a `cli.db_path()`-discovering verification command.

### Deprecated

- **MCP `aelf:stats` JSON: `edges` key.** v1.1.0 emits both `edges` and `threads` with the same integer value. **`edges` is removed in v1.2.0.** Clients should migrate to `threads` now.
- **MCP `aelf:health.features.edge_per_belief` key.** v1.1.0 emits both `edge_per_belief` and `thread_per_belief` with the same value. `edge_per_belief` is removed in v1.2.0.
- **`aelfrice.auditor.CHECK_ORPHAN_EDGES` constant.** Kept as a deprecated alias of `CHECK_ORPHAN_THREADS` for v1.0 importer compatibility. Removed in v1.2.0.

### Documentation

- **Local-only contract locked** ([#87](https://github.com/robotrocketscience/aelfrice/pull/87)). LIMITATIONS.md gains "Sharing or sync of brain-graph content" under "Out of scope by design" with privacy/determinism/audit rationale. ROADMAP.md "Non-goals" adds "Brain-graph sharing, sync, or export between users / machines / projects." `.aelfrice/seed.md`, `.aelfrice.toml` cross-machine identity, and v2.0 cross-project shared store are removed from the roadmap. Bootstrap recipe for new clones is `aelf onboard .`.
- **`docs/promotion_path.md`** linked from ROADMAP § v1.2.0 and LIMITATIONS § Onboarding.

## [1.0.3] - 2026-04-27

Patch release: contradiction tie-breaker, `aelf resolve` CLI, onboard
performance regression test, and a power-user CONFIG.md for the
`.aelfrice.toml` schema. Three feature PRs and a docs PR landed
between v1.0.2 and v1.0.3 — this release surfaces them to PyPI.

### Added

- `aelfrice.contradiction` module: `resolve_contradiction`,
  `find_unresolved_contradictions`, `auto_resolve_all_contradictions`.
  When the graph holds a CONTRADICTS edge, the tie-breaker picks a
  winner via precedence (`user_stated > user_corrected >
  document_recent`; ties broken by recency, then by id), creates a
  SUPERSEDES edge from winner to loser, and writes a
  `feedback_history` audit row tagged
  `source='contradiction_tiebreaker:<rule>'`. v1.0.x collapses to
  three precedence classes (the fourth `agent_inferred` class needs
  a `Belief.origin` field landing in v1.1.0) (#75).
- `aelf resolve` CLI subcommand (12th) — sweeps unresolved
  CONTRADICTS edges in the store and runs the tie-breaker on each.
  Matching `slash_commands/resolve.md`. The 1:1 CLI ↔ slash
  invariant is preserved at 12/12 (#75).
- `tests/regression/test_onboard_perf_50k_loc.py`: regression benchmark
  asserting `scan_repo` finishes in under 60s on a synthetic ~55k-LOC
  project (250 .py + 60 doc files). Marked `regression`. Held against
  the `:memory:` store. Current measured time ~0.8s on Apple Silicon;
  the 60s budget is a regression alarm, not a target (#76).
- `docs/CONFIG.md`: power-user reference for `.aelfrice.toml` —
  full schema, worked examples, and what each setting affects. Linked
  from the noise-filter LIMITATIONS entry, the onboard CLI help, and
  ARCHITECTURE.md (#74).

## [1.0.2] - 2026-04-27

Patch release: per-project install routing, `aelf doctor` settings
linter, and CI guardrails for release docs. Closes the v1.0.1 gap
where one machine couldn't cleanly route per-project venv hooks
alongside a global `pipx` install, and the README roadmap drifted out
of sync minutes after the wheel was on PyPI.

### Added

- `aelf doctor`: scan user-scope and project-scope Claude Code
  `settings.json` for hook + statusline commands whose program token
  doesn't resolve. Catches dangling absolute paths, bare names not on
  `$PATH`, and missing scripts under `bash /…` interpreter wrappers.
  Exits `1` on any broken finding so it can gate CI (#81).
- `staging-gate.yml` `release-docs-check` job: when a PR bumps
  `pyproject.toml` `version`, enforce `CHANGELOG.md` has a matching
  `## [X.Y.Z]` section + compare-link footnote, and that `README.md`
  has no roadmap row marking the released version as `next` /
  `planned`. No-op on non-release PRs (#80).
- `post-release-docs-issue.yml`: on `release.published`, opens a
  tracking issue `docs sweep for vX.Y.Z` with a per-doc checklist
  for the second-order docs the gate can't verify automatically
  (RELEASING.md test counts, ROADMAP.md narrative, etc.) (#80).

### Changed

- `aelf setup` no longer requires `--scope`. Default is auto-detect:
  `project` (writes `<cwd>/.claude/settings.json`) when `cwd/.venv`
  matches the active interpreter's `sys.prefix`, else `user` (writes
  `~/.claude/settings.json`). Explicit `--scope` still wins (#81).
- `aelf setup --command` defaults to an absolute path: project scope
  -> the active venv's `aelf-hook`; user scope -> the first
  `aelf-hook` on `$PATH` (typically a `pipx`-installed
  `~/.local/bin/aelf-hook`). Lets one machine route per-project venvs
  to their own hook AND fall back to a global pipx install outside
  any project, without bare-name `$PATH` collisions (#81).
- `aelf setup` now silently removes legacy dangling
  `/usr/local/bin/aelf{,-hook}` symlinks if their target no longer
  exists. Real files and live symlinks are never touched (#81).
- `aelf unsetup` defaults to basename-match cleanup (every entry
  whose program basename is `aelf-hook`), so an install written with
  the new auto-resolved absolute path can be torn down by bare
  `aelf unsetup` without specifying the path (#81).

## [1.0.1] - 2026-04-27

Patch release: power-user ergonomics + retrieval-side feedback loop.
Closes the v1.0.0 gap where hook retrievals never wrote audit rows,
adds a no-config noise filter (with a TOML escape hatch), and ships
`aelf --version` as a first-class CLI flag.

### Added

- `aelfrice.hook_search` module: `search_for_prompt(store, prompt, ...)`
  and `record_retrieval(store, beliefs, ...)`. Closes the v1.0.1
  retrieval-side feedback-loop gap: every UserPromptSubmit hook
  retrieval now writes one `feedback_history` row per returned belief,
  tagged `source='hook'` with `HOOK_RETRIEVAL_VALENCE` (0.1) positive
  valence (#70).
- `propagate: bool = True` kwarg on `aelfrice.feedback.apply_feedback`.
  Default preserves the corrective-feedback contract (positive signal
  on a contradictor pressures the contradicted user lock); pass `False`
  for non-corrective signals (hook-driven retrievals) to update the
  posterior without pressure-walking locked beliefs (#70).
- `aelf --version` flag prints `aelf <__version__>` and exits 0.
  Closes the long-standing argparse error users hit when probing the
  installed version (#71).
- `aelfrice.noise_filter` module: pure-stdlib pure-function predicate
  `is_noise(text, config)` that drops candidate paragraphs matching
  one of four well-known non-belief shapes — markdown heading blocks,
  checklist blocks, three-word fragments, or license-header
  boilerplate (seven canonical signatures). Wired into
  `scanner.scan_repo` before classification. New
  `ScanResult.skipped_noise` counter (default 0 for back-compat)
  reports how many candidates were filtered per scan (#72).
- Power-user configuration via a single `.aelfrice.toml` at the
  project root (or any ancestor); discovered automatically by
  `scan_repo` walking up from the scan root. The `[noise]` table
  exposes: `disable` (subset of `headings`, `checklists`,
  `fragments`, `license`), `min_words` (override fragment
  threshold), `exclude_words` (whole-word case-insensitive — "jso"
  drops "jso" but never "json"), and `exclude_phrases` (literal
  substring case-insensitive). No regex in the user-facing schema.
  Library callers can pass an explicit `NoiseConfig` to
  `scan_repo(..., noise_config=...)` to bypass file discovery (#72).
- `aelf upgrade` subcommand: prints the right pip-upgrade command for
  the user's install context (venv / pipx / system), includes the
  published wheel SHA-256 + PyPI URL for hash-pinned installs (#73).
- `aelf uninstall` subcommand with mutually-exclusive `--keep-db` /
  `--purge` / `--archive PATH` disposition flags. `--purge` requires
  three redundant gates (explicit flag, typed `PURGE`, final `[y/N]`).
  `--archive` encrypts the DB with AES-128-CBC + HMAC via Fernet,
  scrypt-derived key from a user password (Salt embedded; password is
  the only secret needed for recovery). Public `decrypt_archive()` API
  for later recovery (#73).
- `aelf statusline` subcommand: emits an orange-coloured one-line
  update-banner prefix snippet for Claude Code's statusbar (and any
  shell-driven status bar). Empty output when no update is pending.
  Truecolor → 256-color → basic-yellow fallback, NO_COLOR honoured (#73).
- Two-component update notifier ported from GSD's pattern: detached
  background PyPI check writes a JSON cache at
  `~/.cache/aelfrice/update_check.json`; statusline + post-command
  banners read that cache only. Cache TTL: 24h. Opt out via
  `AELF_NO_UPDATE_CHECK=1` (#73).
- `aelf setup` auto-wires the statusline alongside the hook by default
  (`--no-statusline` opts out). Composes deterministically with an
  existing `statusLine`: empty slot → install; already ours → no-op;
  simple existing → wrap with `; aelf statusline 2>/dev/null`; complex
  (shell metacharacters) → leave alone with a one-line hint (#73).
- `aelf unsetup` reverses the statusline composition surgically (#73).
- New optional extra: `pip install 'aelfrice[archive]'` adds the
  `cryptography` dep that `aelf uninstall --archive` needs (#73).
- Slash commands: `/aelf:upgrade`, `/aelf:uninstall`, `/aelf:statusline` (#73).

### Changed

- `aelfrice.hook.user_prompt_submit` now routes through
  `aelfrice.hook_search.search_for_prompt` instead of calling
  `aelfrice.retrieval.retrieve()` directly. Same non-blocking contract,
  same OPEN_TAG/CLOSE_TAG output envelope, same payload schema; the
  behavioural difference is the audit-row write per returned belief
  (#70).
- `aelf setup` output now shows two lines (hook + statusline) on a
  fresh install. `aelf unsetup` shows the matching teardown lines (#73).
- README "Install" section rewritten with explicit verification
  commands; new "Upgrade" and "Uninstall" sections (#73).
- `docs/INSTALL.md` rewritten with explicit Statusline composition
  table, full uninstall reference (including archive recovery), and
  Update notifier opt-out instructions (#73).

## [1.0.0] - 2026-04-27

First installable PyPI release. The full v1.0 surface is the cumulative
shipped contents of v0.1.0–v0.9.0rc0; this release tags it, builds the
sdist + wheel, and publishes via the GitHub Actions Trusted Publisher
workflow.

### Added

- PyPI publication: `pip install aelfrice` (`pip install "aelfrice[mcp]"`
  for the MCP server extra).

### Changed

- Trove `Development Status` classifier promoted from `4 - Beta` to
  `5 - Production/Stable`.
- README headline and tagline rewritten for a post-rebuild audience:
  install instruction front-and-centre, "Status: under active rebuild"
  warning removed, "What works today" section retitled to v1.0.0.

## [0.9.0rc0] - 2026-04-26

Benchmark-harness milestone — final gate before `v1.0.0`. The `v0.9.0-rc`
roadmap row is shipped as PEP 440 release candidate `0.9.0rc0`.

### Added

- `aelfrice.benchmark` module — deterministic 16-belief × 16-query
  synthetic harness. Public surface: `BENCHMARK_NAME`,
  `seed_corpus(store)`, `run_benchmark(store, *, aelfrice_version,
  top_k=5)`, frozen `BenchmarkReport` dataclass with `hit_at_1`,
  `hit_at_3`, `hit_at_5`, `mrr`, `p50_latency_ms`, `p99_latency_ms`
  (#50).
- `aelf bench` CLI subcommand — prints the report as a single JSON
  document. Defaults to in-memory store for full reproducibility;
  `--db PATH` for an explicit on-disk SQLite file; `--top-k N` to
  override hit@k accounting depth (#50).
- `slash_commands/bench.md` keeping the CLI ↔ slash 1:1 invariant
  green at 11 commands (#50).
- `tests/regression/test_benchmark_cli_end_to_end.py` —
  `@pytest.mark.regression` end-to-end coverage of `aelf bench`
  default invocation, `--db PATH`, and `--top-k` override (#50).

### Notes

- The harness is the **measurement instrument**, not proof of the
  central feedback claim. Retrieval ranking in the v1.0 line is
  BM25-only (`store.search_beliefs` orders by `bm25(beliefs_fts)`,
  posterior `alpha`/`beta` are not consumed), so `apply_feedback`
  does not currently move benchmark scores. A v1.x retrieval
  upgrade that consumes posterior is the precondition for using
  this harness to claim feedback drives accuracy.

## [0.8.0] - 2026-04-26

Project-metadata milestone — everything required for PyPI publish (gated
until v1.0.0) is now present and verified.

### Added

- `LICENSE` file at repo root (MIT) matching the `pyproject.toml` license
  declaration. `uv build` bundles it into both wheel and sdist (#45).
- `CHANGELOG.md` following the [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
  format with retroactive sections for v0.1.0–v0.7.0 (#46).
- `docs/INSTALL.md` — install, configure, quickstart, Claude Code wiring,
  development workflow, uninstall (#47).
- `docs/ARCHITECTURE.md` — design principles, module map, data model,
  Bayesian update path, retrieval flow with ASCII diagram, Claude Code
  integration diagram, test layers, explicit "out of scope through v1.0.0"
  list (#47).
- README `## docs` section linking to both new files (#47).
- `pyproject.toml` PyPI-ready metadata pass: sharpened `description`,
  `authors` with email, explicit `license-files = ["LICENSE"]`, ten
  `keywords`, thirteen Trove `classifiers` (Beta dev status, Python 3 /
  3.12 / 3.13, MIT, Topic / Audience / `Typing :: Typed`), and
  `[project.urls]` (Homepage, Repository, Documentation, Changelog,
  Issues) (#48).

## [0.7.0] - 2026-04-26

Claude Code wiring milestone — aelfrice retrieval can now be installed
as a `UserPromptSubmit` hook with a single `aelf setup`.

### Added

- `aelfrice.setup` module: idempotent `install_user_prompt_submit_hook` /
  `uninstall_user_prompt_submit_hook` / `default_settings_path`
  functions that mutate a Claude Code `settings.json`. Atomic on-disk
  write via sibling tempfile + `os.replace` (#39).
- `aelfrice.hook` module: `aelfrice.hook:main` reads the
  `UserPromptSubmit` JSON payload from stdin, runs aelfrice retrieval,
  and writes an `<aelfrice-memory>...</aelfrice-memory>` block to stdout.
  Non-blocking by contract — every failure mode (empty stdin, malformed
  JSON, missing/blank/wrong-type prompt field, retrieval exceptions)
  exits 0 with no stdout (#40).
- `aelf setup` and `aelf unsetup` CLI subcommands wrapping the install /
  uninstall functions, with `--scope user|project`, `--project-root`,
  `--settings-path`, `--command`, `--timeout`, `--status-message`. CLI
  surface grows from 8 to 10 commands; matching `setup.md` and
  `unsetup.md` slash commands ship in `src/aelfrice/slash_commands/`
  (#41).
- `aelf-hook = "aelfrice.hook:main"` script in `[project.scripts]`. CLI
  default `--command` switches to `aelf-hook` (#42).
- End-to-end regression test in `tests/regression/` exercising
  `aelf setup` → real subprocess spawn of the recorded hook command →
  verify retrieval output → `aelf unsetup` (#43).

### Changed

- `aelfrice.__version__` and `uv.lock` synced to `0.6.0` after v0.6.0
  shipped (#38).

## [0.6.0] - 2026-04-26

CLI / MCP / slash-commands milestone — the user-facing surface.

### Added

- `aelfrice.cli` with eight subcommands (`onboard`, `search`, `lock`,
  `locked`, `demote`, `feedback`, `stats`, `health`) and the `aelf`
  console script in `[project.scripts]`. Folds `config.py` into the CLI
  and reorganises `scoring.py` / `store.py` (#32).
- `aelfrice.mcp_server` with eight FastMCP tools mirroring the CLI
  surface; `pip install aelfrice[mcp]` extra adds the `fastmcp`
  dependency (#35).
- `src/aelfrice/slash_commands/` directory with eight markdown slash
  commands matched 1:1 to CLI subcommands; an invariant test enforces
  the correspondence (#36).
- `aelfrice.health` module with regime classifier
  (insufficient-data / supersede / ignore / balanced) backed by
  confidence, lock density, and edge density features (#31).
- Polymorphic onboard state machine in `aelfrice.classification` (#34).
- `onboard_sessions` schema + CRUD helpers in `aelfrice.store` (#33).

### Fixed

- FTS5 query special characters are now escaped in `search_beliefs`
  (#30).

## [0.5.0] - 2026-04-26

Scanner milestone — onboarding from a project directory.

### Added

- `aelfrice.scanner` package with `scan_repo` orchestrator combining
  three extractors (filesystem walk, git log, AST) with the
  classification module and the store (#28).
- Filesystem-walk extractor (#25), git-log extractor (#26), and AST
  extractor (#27).
- `aelfrice.classification` with `TYPE_PRIORS` and a regex fallback
  (#24).
- End-to-end regression test for the onboarding flow (#29).

## [0.4.0] - 2026-04-26

Feedback-loop milestone — the central `apply_feedback` endpoint.

### Added

- `apply_feedback` endpoint in `aelfrice.feedback` performing
  Beta-Bernoulli updates and writing to the `feedback_history` table
  (#19).
- `feedback_history` table + `Store` helpers (#18).
- Demotion-pressure-on-contradiction-edge: contradicting feedback
  against a locked belief now increments `demotion_pressure` (#20).
- Auto-demote locked belief when `demotion_pressure` crosses threshold
  (#21).
- No-LLM correction detector in `aelfrice.correction` (#22).
- End-to-end regression test for the feedback loop (#23).

## [0.3.0] - 2026-04-26

Retrieval milestone — two-layer L0 locked + L1 FTS5 BM25.

### Added

- `aelfrice.retrieval` module with L0 locked-belief auto-load and L1
  FTS5 BM25 keyword search. Token-budgeted output. No HRR, no BFS
  multi-hop, no entity-index in the v1.0 line (#14).
- Lock test enforcing L0-before-L1 ordering invariant in retrieval
  output (#15).
- Property test: token-budget invariant holds across budget magnitudes
  (#17).

### Changed

- Pytest configured with `pytest-timeout` (5s default) and registered
  markers; subprocess-driven tests (v0.6.0+) override per-test (#16).

## [0.2.0] - 2026-04-26

Scoring milestone — Beta-Bernoulli posterior + decay.

### Added

- `aelfrice.scoring` with posterior mean, type-specific half-life decay
  (with lock-floor short-circuit), and a basic relevance combiner; plus
  the `test_bayesian_inertia` property test (#8).
- `test_decay_required` property test confirming decay actually moves
  posteriors over time (#9).
- `test_lock_floor_sharp` property test confirming a `user`-locked
  belief does not decay (#10).

### Changed

- Pyright strict mode enabled across `src/aelfrice` and `tests` (#12).

### Fixed

- `requirement` belief half-life corrected to 720h per spec (#11).

### Documentation

- README test-count corrected (#13).

## [0.1.0] - 2026-04-26

Foundation milestone — store, models, config.

### Added

- `aelfrice.models`: `Belief` / `Edge` dataclasses plus enum-style
  module-level constants for belief / edge / lock types; `aelfrice.config`
  with type half-lives and broker-attenuation parameters (#4).
- `aelfrice.store` with SQLite WAL journaling, FTS5 full-text search,
  CRUD for beliefs and edges, broker-confidence-attenuated
  `propagate_valence`, and `demotion_pressure` read/write (#5).
- Property test: `propagate_valence` broker-attenuation invariant (#6).
- Round-trip test for `demotion_pressure` reads + writes (#7).
- Initial repo scaffold: pyproject, README, GitHub Actions workflows,
  scan configs (commit `67b4343`).

[Unreleased]: https://github.com/robotrocketscience/aelfrice/compare/v2.0.1...HEAD
[2.0.1]: https://github.com/robotrocketscience/aelfrice/compare/v2.0.0...v2.0.1
[2.0.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.7.0...v2.0.0
[1.7.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.5.1...v1.6.0
[1.5.1]: https://github.com/robotrocketscience/aelfrice/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.2.0...v1.4.0
[1.2.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/robotrocketscience/aelfrice/compare/v1.0.3...v1.1.0
[1.0.3]: https://github.com/robotrocketscience/aelfrice/compare/v1.0.2...v1.0.3
[1.0.2]: https://github.com/robotrocketscience/aelfrice/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/robotrocketscience/aelfrice/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.9.0rc0...v1.0.0
[0.9.0rc0]: https://github.com/robotrocketscience/aelfrice/compare/v0.8.0...v0.9.0rc0
[0.8.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.5.0...v0.6.0
[0.5.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.4.0...v0.5.0
[0.4.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.3.0...v0.4.0
[0.3.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/robotrocketscience/aelfrice/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/robotrocketscience/aelfrice/releases/tag/v0.1.0
