# Changelog

All notable changes to Axiom are documented in this file.

## [0.22.0] — 2026-05-28 — Durable RAG ingest + provenance gate + corpus audit + availability-aware CLI + multi-forge CI

Adds a durable, resumable document-ingest engine and an export-control-aware
provenance gate to the RAG subsystem (spec-rag-ingest-advanced), an
availability-aware CLI dispatcher (ADR-047), and a forge-agnostic CI-provider
abstraction for RIVET.

### Added
- **`axi rag ingest`** — durable, resumable ingest of large corpora: preflight
  (input scan, destination reachability, capacity estimate that aborts with
  numbers), calibration/ETA measured from the live run, a progress event stream
  (TTY panel + headless JSON), batch-granularity checkpoint/resume, and a SIGINT
  checkpoint-and-exit / double-tap-abort coordinator. Wraps the existing ingest
  engine; no schema change. (Replaces the legacy `ingest` index-alias.)
- **Provenance/artifact router** — per file, decides exclude / quarantine /
  allow(+tier) by source path and artifact type from a `--rules <file.toml>`
  rule set, wired into the ingest path so controlled or proprietary content is
  gated *before it is read*. Rule loader (`load_rules`/`load_rules_file`),
  first-match-wins routing.
- **Safe-by-default guard** — ingest into a shared tier (`rag-org`/
  `rag-community`) refuses without provenance rules unless `--no-rules` (or the
  `AXIOM_RAG_RULES` env var) is set.
- **`axi rag audit`** — audit an existing corpus's source paths against a rule
  set; `--purge` removes EXCLUDE-flagged documents. Finds controlled content
  ingested before the rules existed. (`store.list_document_paths`.)
- **Honest ingest drop reporting** — `IngestStats` distinguishes unchanged /
  unsupported / failed / excluded / quarantined, with per-extension and per-rule
  breakdowns surfaced to the operator. The directory walk now *sees* unsupported
  files instead of pre-filtering them invisibly.
- **Availability-aware CLI dispatcher** (ADR-047) — commands declare capability
  requirements (`requires = ["git"]` in the AEOS manifest); the dispatcher hides
  them from help/completion and refuses to run them with a reason + remedy when a
  dependency is missing, instead of crashing mid-run. New `infra/capabilities`
  (probes for git/gh/glab/gitlab-token), `infra/cli_gating`, and
  `AXI_SHOW_UNAVAILABLE` to reveal hidden commands. `axi release` requires git.
- **git availability helpers + repo-init offer** (`infra/git`, `infra/git_setup`)
  — `git_available`/`is_git_repo`/`is_inside_work_tree`/`init_repo`; `axi release`
  now guards on git + repo presence and offers `git init` (or fails fast in
  automation) instead of crashing on the first git call in a non-repo.
- **Forge-agnostic CI providers for RIVET** (`release/providers`) — pipeline
  status read via a provider detected from the git remote: GitHub (`gh`), GitLab
  (REST, any host, by path or numeric `project_id`), and Gitea/Forgejo. The
  watched-repo list is config-driven (`ci-repos.toml`, `$AXI_CI_REPOS`); a
  missing tool/token degrades to "no signal", never a crash.
- **`axi release --tag-only`** — tag an already-bumped version (e.g. after a
  release PR bumped it) and push, without re-bumping. The push fires the
  publish/mirror sync.

### Changed
- **Embedding-failure durability** — `embed_texts` raises `EmbeddingError` when a
  provider is configured but fails (vs returning `None` only when no provider is
  configured). Ingest no longer commits a document text-only with its checksum
  after a transient embedder/network failure (which would skip it as "unchanged"
  forever) — it retries on re-run. Query-time search degrades to keyword.
- **EC keyword screening** (`ec_screening`) reworked into severity classes
  (controlled/sensitive/none) and demoted to a *secondary* signal behind
  provenance-based routing.

### Notes
- The advanced-ingest live path uses the hardened engine (file-level checksum
  resume). Batch-level checkpoint execution, the federated SSH `--target`, and
  the proof prompt remain on the `spec-rag-ingest-advanced` roadmap.

## [0.21.1] — 2026-05-26 — TIDY remote-delete no longer hangs on credential prompt

### Fixed
- **`safe_git_env` sets `GIT_TERMINAL_PROMPT=0`** — no axiom git subprocess
  can hang waiting on an interactive credential/username prompt; it fails
  fast instead. (A `git push origin --delete` blocked on osxkeychain during
  the 2026-05-26 branch cleanup.)
- **TIDY `branch_prune` deletes GitHub remote refs via `gh api`** (scoped
  token) instead of `git push --delete`, which could block on the HTTPS
  credential prompt. Non-GitHub remotes keep the `git push --delete` path
  (now prompt-disabled, so it fails fast rather than hanging).

## [0.21.0] — 2026-05-26 — RIVET/TIDY boundary + TIDY branch/remote-ref reclamation + RACI D6 graduation-safety

Establishes the RIVET/TIDY role boundary (ADR-046) and closes the
detect-only gap in TIDY's branch hygiene.

### Added
- **TIDY branch/remote-ref prune** (`hygiene/branch_prune.py`): TIDY now
  *executes* merged-branch cleanup — deletes merged local branches and
  merged remote refs, archiving each under `refs/tidy-archive/` first
  (reversible). New `axi tidy branches [--prune] [--remote] [--dry-run]
  [--yes]` + `branch-hygiene` skill.
- **RACI v2 D6 graduation-safety** (ADR-045, now Accepted): the
  `act-then-notify` tier, reversibility-gated graduation, volume/rate
  circuit-breaker, novelty confirmation, digest batching.
- **Action-guard D6 primitives** (`policy/agent_action_guard.py`):
  reversibility gate (refuses irreversible autonomous ops) +
  `volume_mode="confirm"` (over-limit batch downgrades to a prompt).
- **RIVET lifecycle events** (`release/lifecycle_events.py`): emits
  `rivet.pr_merged` / `tag_released` / `ci_recovered` on the EventBus for
  TIDY to consume; RIVET touches no git refs.

### Docs
- ADR-046 (RIVET/TIDY boundary), ADR-045 D6 amendment, and the
  coverage-manifest / action-guard specs + agent roster synced to the new
  capabilities.

## [0.20.0] — 2026-05-22 — axi schedule install (4 backends) + RIVET PR-CI watcher (3 layers) + daemon idempotence

Two feature lines land in this release. The scheduling primitive
grows from render-only to a full multi-backend install pipeline.
RIVET grows a proactive PR-CI watcher with three layers.

### Added — `axi schedule install` apply side, four backends (PR #211 — issue #205)

`axi schedule install --host <host>` lays down host-side schedule
artifacts via four backends:

| Backend | Target | Artifact | Idempotence |
|---|---|---|---|
| `cron` | Linux/Unix with crontab | `<name>.cron` | marker-comment in crontab |
| `launchd` | macOS | `<name>.plist` under `~/Library/LaunchAgents/` | content-equal + already-loaded skip |
| `systemd` | Linux with user-systemd | bundled `<name>.systemd` → `.service` + `.timer` | `_write_if_changed` + daemon-reload |
| `wintasks` | Windows 10/11 via OpenSSH | `<name>.ps1` PowerShell script | `Unregister` before `Register` |

`--backend auto` (default) probes the host (`uname -s`, `command -v
systemctl`, `ver`) and picks. Transports: `SSHRunner` (default) and
`LocalRunner` for `--host localhost`.

Integration smokes against real hosts:
- cron via SSHRunner against a remote Linux host (`RUN_SSH_TESTS=1`)
- launchd against the local macOS box (`RUN_LAUNCHD_TESTS=1`) with
  tmp `LAUNCH_AGENTS_DIR` override
- systemd via SSHRunner against a remote Linux host (`RUN_SSH_TESTS=1`)
- wintasks unit-tests-only (no Windows host in CI)

Plus a full CLI E2E subprocess smoke (`test_cli_e2e_smoke.py`) that
exercises `axi schedule create → install → uninstall` through
`python -m`.

### Added — `axi schedule create` is backend-aware (PR #211 — issue #205)

`axi schedule create` now accepts `--backend
{cron,launchd,systemd,wintasks}` (default `cron`). Routes through
`backend.render()` so create and install share one format. Prior
0.19.0 path had quietly diverged — create wrote cron files without
the `# axi schedule managed:` marker that install required.

`axi schedule list` shows the inferred backend per row.

When `axi schedule install` finds no artifacts for the requested
backend but artifacts exist in OTHER backend formats, it now prints
an actionable error naming the mismatch + suggesting `--backend <X>`
or recreate. `axi schedule uninstall` checks artifact existence
before claiming success.

### Fixed — macOS daemon-registration noise (PR #211 — issue #208)

`LaunchdProvider.install` now compares plist content against the
on-disk file and skips the write when they match. `start()` probes
`launchctl list` and skips load when already loaded. Re-running
`axi install` on an unchanged config fires zero "App Background
Activity" toasts. Same idempotence in the new `LaunchdBackend` for
`axi schedule install`. `RegistrationResult.unchanged` plumbed
through; install CLI suppresses the "Agent services" output block
on fully-idempotent runs.

### Added — chat auto-start provisioned llamafile + free-first fallback (PR #212)

`axi chat` auto-starts a provisioned llamafile when present, and
shows a free-first fallback menu when the user hasn't set up a
provider yet.

### Added — RIVET PR-CI watcher, Layer 1 polling (PR #211 — slice 18)

New `release/pr_check_watcher.py`. RIVET's heartbeat enumerates the
user's open PRs (`gh pr list --author @me`) and fetches per-job
state, classifies failures (`infra` vs `code`), persists last-seen
state at `~/.axi/agents/rivet/pr-checks.json`, emits `StateFlip`
events on terminal-state transitions. Fills the gap that let PR
#211's own Build Wheel billing-block failure fly under the prior
top-level-run-only `ci_monitor._check_github`.

### Added — RIVET responder, Layer 3a notifications + failure reports (PR #214 — slice 19)

`release/pr_check_responder.py` routes `StateFlip` events via
`TerminalNotificationProvider` (stdout + macOS notification center
via pync):

- infra flip → high-urgency "ACTION REQUIRED" notification
- code flip → normal-urgency "FIX NEEDED" + failure-report markdown
  under `~/.axi/agents/rivet/reports/` with actionable next steps
- recovery → low-urgency "recovered" notification

### Added — RIVET auto-close on recovery (PR #215 — slice 20)

`release/pr_check_auto_closer.py` auto-closes stale `🔴 CI failed
on \`refs/pull/<N>/merge\`` issues opened by the github-actions bot
when RIVET sees a recovery flip. Safety defenses: title regex
anchored on the specific PR's merge ref, author must be the
`github-actions` bot, state must be `OPEN`, `RIVET_AUTO_CLOSE_DRY_RUN=1`
for surface-only, `RIVET_AUTO_CLOSE=0` for hard disable. Each close
drops an audit comment.

### Added — `axi rivet close-stale` manual sweep (PRs #215, #216, #217 — slices 21-23)

CLI verb for the existing backlog. Four target modes:

- `--pr <N>` — one PR (only if currently passing)
- `--all-prs` — every PR with stale issues whose state is safe
  (open+passing OR merged+main-passing OR closed-without-merge)
- `--all-main` — main-branch issues (only if main is currently
  passing)
- `--all-tags` — release-tag issues whose tag is reachable from
  main (release was integrated, codebase has moved on)

`--dry-run` previews on all four. Live deployment closed **95**
stale issues across the three target modes this cycle (44 main +
43 PR + 8 tag).

### Fixed — pre-push test flake (PR #211)

`hygiene.git_signals._run` now strips `GIT_*` env vars before
invoking subprocess git. The pre-push hook context was leaking
`GIT_DIR`, breaking 8 hygiene tests across the last three releases
by routing `git ls-files` etc. to the host repo instead of the
test's tmp_path. Plus regression tests that reproduce the leak.

### Documentation / hygiene

Scrubbed UT-specific `rascal` host name from the scheduling
surface (`src/axiom/cli/scheduling/*` + tests). Test smokes now
require explicit `AXIOM_TEST_HOST` env var; default no longer baked
in. Follow-up issue #213 tracks the broader scrub across other
modules.

### Test count

- pre-push: ~7333 passed at release tip (up from 7152 at v0.19.0)
- 130+ new tests across scheduling backends, CLI E2E smoke, RIVET
  watcher/responder/auto-closer/sweep

### Breaking changes

None. NeutronOS floor pin can bump `>=0.19.0` → `>=0.20.0`.

## [0.19.0] — 2026-05-19 — Memory sessions + MCP audit-trail fixes + repo-hygiene signals + AEOS defaults + axi schedule

Three PRs across three feature areas land in this release. Headline:
memory provenance grows to include a session dimension, the MCP
audit-trail surface is correctness-fixed, and Axiom gains a proactive
hygiene-signals surface plus AEOS-default + scheduling primitives that
the 2026-05-18/19 Rascal cleanup motivated.

### Added — session-aware memory (PR #197 — `spec-memory §3.7`)

`Provenance` tuple grows from `(T, U, A, R)` to `(T, U, A, R, S)`.
Episodic fragments stay session-bound by default; core / procedural /
resource fragments cross sessions unconditionally; semantic fragments
cross by relevance (phase-1 = no filter). The principle: **things you
DID are scoped to where you did them; things you KNOW are global.**

- `Provenance.session_id: str = ""` — backwards-compatible default;
  legacy v1+v2 fragments decode to `""` and are interpreted as
  cross-session.
- `CompositionService.write(..., session_id=)` auto-resolves from the
  active session when omitted.
- New module `axiom.memory.session`: per-process current-session
  resolution, `~/.axi/sessions/<uuid>.json` registry, auto-name
  `<cwd-basename>-<YYYY-MM-DD-HHMM>`, 4h auto-resume window per
  `(principal, cwd)`. `PYTEST_CURRENT_TEST` auto-disables resolution
  so the test suite doesn't pollute `~/.axi/`.
- `axiom_memory__compose` MCP tool accepts caller's `session_id`
  (cross-vendor clients attribute writes to their session, not the
  MCP server's process session).
- `axiom_memory__retrieve` accepts `scope` (`default` / `strict` /
  `current` / `all` / `session:<id>`) + `session_id`; surfaces
  `session_id` in the provenance block for cross-session attribution
  audits.
- PRD §5 (item 10) + §6 (new bet) + §9 (open question) updated;
  spec-memory §3.3 (tuple grows) + §3.7 (new — id/lifecycle/scope/
  registry/CLI) + §8 (write signature) updated.

### Added — MCP audit-trail bug fixes (PR #197)

Three stranded fixes that landed alongside the session work — the
`(T,U,A,R)` provenance audit was effectively broken on main without
them:

- `fix(mcp)`: `axiom_memory__compose` honors caller-supplied agents +
  resources. Previously, every MCP-written fragment came back with
  `agents=["mcp_root_server"]` regardless of caller — no way to
  distinguish "Claude wrote this from Claude Code" vs "GPT-5 wrote
  this from Codex".
- `fix(mcp)`: `axiom_memory__retrieve` returns full content +
  provenance by default. Prior shape truncated each fragment to
  `summary + fact_kind`, forcing follow-up per-fragment fetches that
  didn't exist as tools yet.
- `fix(mcp)`: `axiom_memory__retrieve` sorts newest-first. Without
  the sort, the registry returned insertion order ascending so
  `limit=N` returned the OLDEST N — exact opposite of every chat
  client's expectation.

Bonus: `scripts/inspect-mcp-memory.py` for quick `(T,U,A,R)` audit
dumps.

### Added — repo-hygiene signals (PR #204 — issue #201)

Seven pure-function signals over file-tree + git state. Each returns
`Finding` objects suitable for `node_health` aggregation; each
`auto_fixable` flag distinguishes TIDY-can-batch-resolve entries from
human-review-only entries.

| Signal | Surfaces |
|---|---|
| `check_stale_branches` | Local branches whose tip is on `origin/main` and aren't checked out anywhere |
| `check_orphan_worktrees` | Worktrees git marks prunable, on already-merged branches, or whose upstream is gone (filters local-only-WIP false positive) |
| `check_dormant_stashes` | Stashes older than the dormancy threshold (default 60d) |
| `check_dup_basenames` | Same filename tracked at multiple paths (filters `__init__.py` etc.) |
| `check_self_similar_dirs` | Paths containing `X/X/` patterns |
| `check_scripts_with_hardcoded_paths` | `scripts/*.sh` containing `$HOME/Projects/...` etc. |
| `check_non_graduated_scaffolds` | Scaffolds tracked by `axi ext init` that haven't graduated and are older than `dormancy_days` (default 14) |

Each signal's motivating case was a real artifact surfaced during the
2026-05-18/19 cleanup audit (25 stale branches on axiom, 8 on
NeutronOS, the Rascal residue chain, etc.).

### Added — AEOS defaults (PR #204 — issue #202.1/.2/.4/.5/.6)

- `axiom.infra.paths.get_agent_output_dir(agent_name)` returns
  `<project_root>/runtime/agent-output/<agent_name>/`. Convention:
  every extension agent writing operational output (heartbeat JSON,
  health reports, cron logs) resolves its write path via this helper.
  Consumers `.gitignore` the single root once and cover every agent
  forever. Motivating case: NeutronOS `runtime/mo-reports/`
  accumulating 32MB of untracked heartbeat JSON.
- `axi ext init` records each scaffold in
  `<project_root>/.axi/scaffold-graduation.json` (project-local,
  gitignored). Companion `check_non_graduated_scaffolds` flags
  prototypes that sit untouched. Module: `axiom.cli.ext.scaffold_registry`
  with `ScaffoldRecord` / `record_scaffold` / `graduate_scaffold` /
  `list_records` / `list_non_graduated`.
- AEOS scaffold's generated `AGENTS.md` now points new extension
  authors at `get_agent_output_dir`, with a note on the bespoke-path
  failure mode.

### Added — `axi schedule` primitive (PR #204 — issue #203, render side)

- `axi schedule create <name> --host <host> --cron <expr> --command <cmd>`
  writes a portable cron artifact to `deploy/<host>/<name>.cron` with
  `${REPO_DIR}` / `${PROJECT_ROOT}` placeholders. Per-host isolation;
  re-running overwrites. Validation rejects malformed cron and
  unsafe name/host segments.
- `axi schedule list` enumerates all schedule artifacts under
  `deploy/`.
- Apply side (`axi schedule install --host`) tracked as issue #205
  follow-up — SSH + crontab manipulation deserves its own focused PR.

### Fixed — conftest pollution-restoration (PR #200)

`tests/conftest.py`'s session-end guard snapshots `user.name` /
`user.email` at session start and restores them at session end.
Discovered 2026-05-18: if the snapshot was already polluted from a
prior session (e.g. `Test` / `test@example.com` from a hygiene
fixture leak), the guard re-installed the pollution forever — the
conftest itself became the persistence vector.

- `tests/_pollution_guard.is_polluted_snapshot()` recognizes known
  markers: names `Test` / `T` / `tester` / `GLOBAL-LEAK-PROBE` and
  emails `test@example.com` / `t@example.com` / `t@t.test`.
- Guard now **unsets** the local config when the snapshot itself is
  polluted, letting the global identity (`Benjamin Booth`) take over.

### Versions

- axiom-os-lm `0.18.0 → 0.19.0`

### Test counts

- Pre-push: 7097 passed / 220 skipped / 76 deselected
- 115 new tests across PR #197 (37) + PR #200 (19) + PR #204 (59)

### Downstream

- NeutronOS floor pin: bump `axiom-os-lm>=0.18.0` → `>=0.19.0` to
  pick up the session-aware memory + MCP audit-trail fixes (the
  hygiene signals + AEOS defaults are useful in NeutronOS' own
  development; the schedule primitive is the right home for what
  `scripts/mo-heartbeat.sh` was doing).

## [0.18.0] — 2026-05-18 — Chat surface: resilience, multimodal, slash commands, scidisplay Pillar 1, built-in MCP

Twelve PRs across four feature areas land in this release. Headline:
the chat surface gains parity-level capabilities (slash commands,
prompt fragments, multi-modal vision, per-tool permissions, background
tasks, graceful tool-failure recovery), Axiom ships its first scientific
display pillar (math + code rendering), and the built-in MCP server
exposes platform primitives to MCP clients.

### Added — chat surface

- `feat(chat)` — **user-defined slash commands** (Claude-Code-style).
  Authors drop `.md`/`.txt` files into a prompt-library directory;
  the chat surface registers them as `/name` shortcuts with argument
  templating.
- `feat(chat)` — **user-authored prompt-library fragments**. Reusable
  snippets composable into slash commands or pasted inline.
- `feat(chat)` — **multi-modal image input** (Anthropic + OpenAI
  vision). Attach images via paste / drop / `axi chat --image PATH`;
  the chat loop forwards them to the model with vision-capable
  request shapes.
- `feat(chat)` — **per-tool persisted permissions**. Permission
  prompts now offer "Always allow" / "Always deny" toggles that
  persist across sessions per `(principal, tool)` tuple.
- `feat(chat)` — **`/tasks` slash command** wires the background-task
  surface into chat; long-running ops (test suites, benchmarks,
  ingest) report progress as a separate stream.
- `feat(chat, gateway)` — **resilience pass**. Gateway retries 5xx +
  network errors with jittered exponential backoff; chat handles
  tool failures via a typed taxonomy and auto-retries the recoverable
  classes without losing conversation context.

### Added — scidisplay (Pillar 1)

- `feat(scidisplay)` — **math rendering pipeline** (A1-A5). KaTeX-based
  ` ```math ` fence rendering with three Axiom themes; cached
  per-fragment.
- `feat(scidisplay)` — **chat-surface math integration** (A4). Math
  fences in chat output render inline at terminal-display time.
- `feat(scidisplay)` — **code rendering with three Axiom themes**
  (A9-A11). Syntax-highlighted code blocks; theme matches the
  surrounding chat surface.
- `feat(scidisplay)` — **chat-surface code integration** (A12).
  ADR-039 (scientific displays) lands as the architectural anchor.

### Added — built-in MCP server

- `feat(mcp)` — **built-in MCP server core**: aggregation, server,
  drift detection, CLI surface, subscriber. ADR-038 + PRD + spec +
  18 harness adapter docs.
- `feat(aeos)` — **MCPBlock schema** + manifest-schema lint +
  scaffold default for `axi ext init`.
- `feat(hygiene, signals)` — MCP handlers for hygiene + signals
  extensions; `node_health` drift check exposed to MCP clients.

### Added — memory CLI

- `feat(memory)` — color + markdown rendering helpers in
  `axi memory show`. Frontmatter highlighting, semantic coloring of
  fragment fields, inline markdown for fragment bodies.

### Added — design (no code)

- `prd(identity)` — `prd-identity-and-bindings.md`: external-account
  binding layer + persona model + three verification levels.
- `prd(memory)` — `prd-cross-surface-memory.md`: vocabulary lock,
  per-vendor inbound/outbound matrix, OpenCode interop posture.
- `docs(roadmap)` — 2026-05-14 entry; new identity + cross-surface
  design layer (id-1..id-10) spanning Phase 0 through Phase 4
  federation.

### Versions

- axiom-os-lm `0.17.1 → 0.18.0`
- memory extension unchanged at `0.5.0`
- scidisplay extension at `0.1.0` (first feature release)

### Notes for downstream consumers

- NeutronOS floor pin: bump `axiom-os-lm>=0.17.1` → `>=0.18.0` to
  pick up the gateway resilience improvements.
- No breaking changes to public APIs. CompositionService,
  MemoryFragment, federation surfaces unchanged.

## [0.17.1] — 2026-05-13 — bench-1: LongMemEval first run

First public-benchmark wiring through the maturation pipeline.
Drives [LongMemEval](https://huggingface.co/datasets/xiaowu0162/LongMemEval)
(Xiao Wu et al. 2024) — 500 long-conversation questions across 5
capabilities. 0.17.1 ships the **runnable harness** + **reproducible
baseline number** on the first 100 questions.

### Added — `axiom.memory.maturation.bench`

- `SyntheticCorpus.small()` — 5-question in-memory fixture (no network)
  covering all 5 LongMemEval capabilities. Used by unit tests + smoke
  runs.
- `load_corpus_from_huggingface()` — direct `hf_hub_download` of
  `xiaowu0162/LongMemEval` (`longmemeval_s` / `longmemeval_m`). The
  LongMemEval files are bare JSON, not standard HF dataset format, so
  the loader bypasses `datasets.load_dataset` to avoid format-detection
  failure.
- `LongMemEvalRunner(configuration="baseline" | "matured", top_k=3)`
  — per-question isolated ledger; ingests haystack sessions as
  `chat_turn` episodes; optionally runs the full maturation pipeline
  (mat-2 → mat-3 → mat-4) when `configuration="matured"`; keyword-
  overlap retrieval over `chat_turn` + `compacted_chat_turn` +
  `semantic_insight`; recall-based scoring with abstention handling.
- `score_answer()` — token-recall of ground-truth in retrieved text,
  stopword-filtered. Deterministic (LLM-judged scoring lands when the
  gateway integration ships).
- CLI: `python -m axiom.memory.maturation.bench.longmemeval --corpus
  {synthetic|huggingface} [--limit N] [--top-k K] [--configuration
  {baseline|matured|both}] [--json]`

### Results — first 100 LongMemEval-S questions

| Configuration | Accuracy | Mean recall | Δ |
|---|---|---|---|
| baseline | **77.0%** (77/100) | **0.730** | — |
| matured | 77.0% (77/100) | 0.730 | +0.0pp |

**Headline:** the substrate + keyword-overlap retrieval recovers ≥ 50%
of ground-truth tokens on **77 of 100** real LongMemEval-S questions.
This is the first defensible "best memory platform" number for Axiom
(per `prd-memory.md §3` axis 3).

**Honest no-differentiation finding:** with *deterministic everything*
(mat-2 importance heuristic + mat-3 token-recurrence reflection + mat-4
length-reduction compaction), the matured ledger doesn't differentiate
from episodic-only under token-overlap retrieval. The full analysis is
in `docs/working/memory-benchmarks-longmemeval-2026-05-13.md`. Short
version: deterministic reflection re-states tokens that already exist
in the source episodes, so a token-overlap retriever sees no new
vocabulary. LLM-driven reflection (deferred from 0.17.0; lands when
the gateway integration ships) is expected to differentiate by
producing synthesis with new vocabulary.

**What the run *does* prove:**

- Substrate scales: 100 questions × 2 configurations end-to-end in
  ~17min on a laptop, with median 54 sessions × 2 turns per question
  ingested into isolated ledgers.
- No regression: the maturation pipeline never destroys retrievable
  content (audit-chain rule from `spec-memory-compaction.md §6.1`
  holds end-to-end).
- Benchmark harness reproducible: deterministic scoring + extractors
  + scorer; byte-identical replay across runs.

### Added — 9 smoke tests

`test_longmemeval_bench.py` covers module surface, synthetic corpus
shape, `score_answer` recall semantics (recovery, abstention, stopword
handling), runner accuracy on the synthetic corpus, no-regression
check (matured ≥ baseline), result.to_dict shape, invalid-configuration
rejection.

### Added — benchmark report

`docs/working/memory-benchmarks-longmemeval-2026-05-13.md` —
full analysis, per-bucket breakdown, reproduction instructions, and
sequencing into 0.18+.

### Versions

- axiom-os-lm `0.17.0 → 0.17.1`
- memory extension unchanged at `0.5.0`

### Deferred to 0.18.x / 0.19

- Full 500-question run (cost: ~88min; this release runs first 100
  for the substantive number — running the remaining 400 doesn't
  change the headline finding without an LLM-driven extractor).
- LLM-driven extractor variants (gateway integration prerequisite).
- LLM-judged scoring (the published LongMemEval methodology; needed
  for direct comparison with published Mem0/Letta/MemGPT numbers).
- bench-2 LoCoMo, bench-3 MemBench.

## [0.17.0] — 2026-05-13 — Memory matures: dream-cycle lifecycle (mat-1..mat-4)

Axiom Memory now matures the way biological memory does. Episodes get
scored for importance, consolidate into semantic facts via the daily
dreaming pass, and compact into summary fragments once their insight
has been captured. The dream-cycle orchestrator coordinates all of it
per scope, gated by per-stage triggers and per-cycle budgets.

This release ships the **minimum-viable maturation pipeline** of the
seven-stage lifecycle defined in `docs/specs/spec-memory-maturation.md`:

- Stage 2: importance scoring (catch-up sweep + deterministic scorer)
- Stage 3: daily consolidation (deterministic reflection extractor)
- Stage 4: summarize compaction (with audit-chain enforcement)

Stages 5 (archive), 6 (forget), and the LLM-driven extractor variants
land in 0.18+ once the gateway integration is wired and a cold-tier
storage backend is selected. See `docs/working/memory-roadmap.md` for
sequencing.

### Added — `axiom.memory.maturation` package

- **`DreamCycleOrchestrator`** (`maturation.dream_cycle`) — coordinates
  registered :class:`StageHandler` instances in canonical
  :data:`STAGE_ORDER` for a scope. Per-cycle budget enforcement
  (calls, tokens, walltime) stops the cycle cleanly at a stage
  boundary; :class:`BudgetExceededError` raised mid-stage marks the
  cycle interrupted. Per-scope cooldown (default 60s) prevents
  thrashing; ``force=True`` on :meth:`run_cycle` bypasses. Each cycle
  writes a ``fact_kind="dream_cycle_metrics"`` fragment to the ledger
  for SCAN/TRIAGE observability.
- **`ImportanceScoringStageHandler`** (`maturation.importance`) —
  stage-2 catch-up sweep that scores episodic fragments lacking an
  importance score. Side-fragment representation
  (``fact_kind="importance_score"``, ``target_fragment_id``) keeps the
  substrate append-only. Default scorer is
  :class:`DeterministicImportanceScorer` (byte-identical heuristic on
  text-length + question marks + presence of assistant response).
- **`ReflectionStageHandler`** (`maturation.reflection`) — stage-3
  daily consolidation. Builds an :class:`EpisodeBatch` from un-
  consumed episodes (newer than the scope's last
  ``reflection_marker``), invokes the extractor, applies the policy
  gate (citation requirement; classification composition: derived
  = max of sources), writes accepted proposals as semantic fragments
  with ``derived_from`` provenance chains, and emits a new marker so
  the next cycle skips already-consumed episodes. Default extractor
  is :class:`DeterministicReflectionExtractor` (recurring-token
  heuristic; byte-identical). Trigger: accumulated importance ≥
  150.0 (Park et al. threshold).
- **`CompactionSummarizeStageHandler`** (`maturation.compaction`) —
  stage-4 summarize cadence. Audit-chain enforcement: only compacts
  episodes whose ``id`` appears in some semantic fragment's
  ``derived_from``. Default summarizer is :class:`DefaultSummarizer`
  (deterministic length-reduction; must reduce ≥ 50%). Each compaction
  emits two side fragments — the compacted_chat_turn summary and the
  supersession record — keeping the substrate append-only.

### Added — three coordinated normative specs

- `docs/specs/spec-memory-maturation.md` — the umbrella. Two
  operations (consolidate vs compact), six stages, multi-scale timing
  (seconds → year), dream cycle, MIRIX 6-type backbone, storage
  tiering, classification monotonicity, per-extension policy profiles
  (default | aggressive | conservative | regulated | custom).
- `docs/specs/spec-memory-reflection.md` — stage 3 narrowly: three
  cadences (daily / weekly / monthly + custom), LLM and deterministic
  extractor kinds as peers, citation requirement, policy gate. The
  daily pass ships here; weekly themes + monthly identity (`semantic
  → core`) follow.
- `docs/specs/spec-memory-compaction.md` — stages 4-6: summarize /
  archive / forget. Audit-chain enforcement; cryptographic erasure
  per ADR-026; the tombstone-event channel that reflection
  subscribes to; cohort cold-tier federation; TIDY as principal
  operator, WARDEN as auditor.

### Tests

49 new tests across four files cover the maturation MVP:

- `test_dream_cycle_orchestrator.py` (16) — module surface, Stage
  enum, canonical ordering, registration semantics, cycle execution
  (empty / pending / triggered / skipped), budget enforcement, cooldown,
  cycle-metrics fragment.
- `test_importance_scoring.py` (14) — module surface, deterministic
  scoring properties (range, idempotency, ordering), side-fragment
  write, handler pending/idempotency/scope filtering, end-to-end via
  orchestrator.
- `test_reflection.py` (12) — module surface, deterministic extractor
  byte-identical replay, handler trigger at importance threshold,
  semantic fragment write with `derived_from`, reflection-marker,
  citation gate rejection, end-to-end via orchestrator.
- `test_compaction.py` (11) — module surface, default summarizer
  length reduction + determinism, audit-chain enforcement (skip
  without semantic; compact with semantic), supersession side
  fragment, idempotency, end-to-end via orchestrator.

Full memory ext + axiom.memory regression: 165 passed, 4 skipped, 0
failed.

### Versions

- axiom-os-lm `0.16.0 → 0.17.0`
- memory extension `0.4.0 → 0.5.0`

### What's deliberately *not* in this release

- LLM-driven extractors (deterministic only this round; gateway
  integration deferred)
- Stage 5 (archive) — needs cold-tier storage backend selection
- Stage 6 (forget) — needs cryptographic erasure key-management
- Weekly + monthly reflection cadences — daily only
- Cross-scope reflection
- `axi memory dream` CLI surface — orchestrator is library-only this
  round
- bench-1 LongMemEval first run — deferred to 0.17.x (see roadmap)

## [0.16.0] — 2026-05-11 — Codex CLI transcript ingest (cross-tool memory adapter #2)

### Added — codex adapter for `axi memory ingest`

Second canonical per-tool transcript parser, joining claude-code in the
cross-tool memory substrate. Closes the silent gap where Codex CLI
sessions sat unfolded on disk despite the Codex MCP registration shipped
in 0.15.x.

- New `parse_codex_jsonl()` in `axiom.memory.session_capture` — parses
  `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` into turn-pair dicts.
  Drops `role=developer` records (system permissions / instructions);
  collapses *consecutive* same-role records (streamed assistant output;
  auto-injected env context + user prompt) into single segments; emits
  one turn pair per (user-segment → assistant-segment) transition.
- New `ingest_codex_jsonl()` — full ingest path with idempotency on
  stable `source_uuid = codex:<session_id>:turn-<idx>` derived from
  `session_meta.id` + turn index. Re-running on the same transcript is
  a no-op; running on a transcript that grew in place writes only the
  new turn pairs. Mirrors the `ingest_claude_code_jsonl` contract.
- `axi memory ingest <path> --tool codex` dispatches to the new parser
  via `KNOWN_TOOL_PARSERS`. CLI surface unchanged.
- 16 new unit tests covering parse contract, pairing semantics
  (consecutive-record collapse, developer-role drop, function_call
  ignore, malformed-line tolerance), idempotency, dispatch, and
  metadata extraction. Verified end-to-end against six real user-host
  codex transcripts (13 turn pairs total).

### Fixed — `axiom.infra.git.run_git` no longer walks up to parent repos

The shared `run_git` helper passed `cwd=repo_root` but didn't constrain
git's upward search, so callers handing it a path lacking a `.git/`
(e.g. a `tmp_path` in tests, or a project subdirectory under a larger
workspace repo) silently got state from the nearest ancestor repository.
That vector produced the 2026-05-04 and 2026-05-11 tester-pollution
incidents — the worktree's HEAD moved during test runs because production
code called via tests reached up and operated on the real workspace repo.

`run_git` now sets `GIT_CEILING_DIRECTORIES=<repo_root>` in the subprocess
env. If no `.git/` is present at `repo_root` or below, git returns its
normal "not a git repository" error rather than searching upward.

The same fix applies to three duplicate `_run` helpers in the hygiene
extension that bypass `run_git`:

- `axiom.extensions.builtins.hygiene.worktrees._run`
- `axiom.extensions.builtins.hygiene.drift._run`
- `axiom.extensions.builtins.hygiene.ci_watcher._run`

Each now sets `GIT_CEILING_DIRECTORIES=<cwd>` (or `Path.cwd()` when cwd
is None) before invoking subprocess.

### Fixed — GIT_DIR propagation from git hooks bypassed every protection

The pre-push hook's full-suite run still produced 17 failures + 27
errors after the `GIT_CEILING_DIRECTORIES` fixes, even though
standalone pytest runs were clean. Empirical reproduction (running
pytest with ``GIT_DIR=/Users/ben/.../axiom-memory-mcp/.git`` in the
parent env) showed the exact same failure pattern as the hook context.

Root cause: ``git push`` invokes pre-push hooks with ``GIT_DIR`` (and
peer ``GIT_*`` vars) set in the environment. ``GIT_DIR`` short-circuits
all git repo discovery — it overrides ``cwd=``, ``-C <path>``, and
``GIT_CEILING_DIRECTORIES``. Every subprocess we spawned for git
inherited ``GIT_DIR`` from ``os.environ.copy()``, so each test silently
operated on the host repo instead of its ``tmp_path``-scoped one.

Fix: strip every ``GIT_*`` key from the inherited env before adding our
isolation-specific keys. Applied to:

- `axiom.extensions.builtins.hygiene._git_isolation.git_isolated_env`
  (the canonical test helper)
- `axiom.infra.git.run_git`
- `axiom.extensions.builtins.hygiene.{worktrees,drift,ci_watcher}._run`
- `axiom.cli.ext.commands.publish._in_git_repo` and `_git_tags` (new
  shared `_git_env_for` helper)

Verification: pytest run with ``GIT_DIR`` artificially set in the
parent env — 6700 passed, 204 skipped, 0 failed, 0 errors, HEAD
unchanged. This is the test that the standalone runs could not
exercise but the pre-push hook does by virtue of being invoked from
``git push``.

A systemic audit of the remaining production sites that invoke git via
subprocess.run (12+ files outside the helpers above) is queued as
follow-up. Current tests pass with the helpers fixed; remaining sites
are latent rather than active vulnerabilities, but the same
strip-`GIT_*` pattern should apply to each.

### Fixed — consolidated all git-subprocess sites onto `safe_git_env`

The systemic audit landed: every production `subprocess.run(["git", ...])`
call site now sets `env=safe_git_env(<cwd>)`. New canonical helper
`axiom.infra.git.safe_git_env(repo_root=None) -> dict[str, str]`
(public, exported) returns an env dict with every `GIT_*` key stripped
plus, when `repo_root` is supplied, `GIT_CEILING_DIRECTORIES` set to
its resolved path.

Refactored to use the shared helper:

- `axiom.infra.git.run_git` (was duplicating the strip + ceiling logic)
- `axiom.extensions.builtins.hygiene._git_isolation.git_isolated_env`
- `axiom.extensions.builtins.hygiene.{worktrees,drift,ci_watcher}._run`
- `axiom.cli.ext.commands.publish._in_git_repo` + `_git_tags`

Newly fixed sites (22 across 14 production files): the previously
latent vulnerabilities listed in the audit. Includes:

- `axiom.agents.pipeline.repo_state._default_runner`
- `axiom.cli.ext.commands.migrate._is_inside_git_worktree`, `_move`
- `axiom.extensions.builtins.classroom.cli` (git config --global probe)
- `axiom.extensions.builtins.diagnostics.tools._exec_git_commit_fix`
  (7 sites)
- `axiom.extensions.builtins.hygiene.agents.tidy.discover._git_head`
- `axiom.extensions.builtins.hygiene.cli` (3 sites)
- `axiom.extensions.builtins.publishing.scripts.publish.get_commit_sha`,
  `count_commits_since`
- `axiom.extensions.builtins.release.cli._git`
- `axiom.extensions.builtins.review.tools.diff.local_diff`
- `axiom.extensions.builtins.update.cli` (9 sites)
- `axiom.extensions.builtins.update.version_check` (4 sites)
- `axiom.rag.personal._git_log_text`
- `axiom.setup.probe` (2 sites)

Non-git subprocess sites (kubectl, docker, pip, pytest, ruff, pandoc,
grep, k3d) intentionally left alone — `safe_git_env` is specifically
for git's environment-variable surface and would be misleading on
other commands.

Verification: 6700 passed / 0 failed / 0 errors in *both* standalone
and `GIT_DIR`-set-in-parent (simulated pre-push hook) contexts. The
hook-simulated check is the new ground truth — prior fixes passed
standalone but the hook context exposed each missed vector.

### Fixed — `test_concurrent_reads_dont_block_each_other` threshold

Bumped the concurrent-reads timing assertion from `< 1.0s` to
`< 3.0s`. The test asserts that 3 multiprocessing readers complete
concurrently (not serially). Standalone the test runs in ~0.4s, but
late in the 6700-test full sweep system load pushes it to ~1.0–1.5s
even with the locks behaving correctly. 3.0s still catches a serial
regression (which would be 3+ second-scale process-startup overhead)
without false-positive-ing on load. Not caused by the safe_git_env
work — pre-existing flakiness surfaced by running it inside the full
sweep.

### Notes

- Codex MCP-side registration already shipped in 0.15.x (writes
  `~/.codex/config.toml` via the per-tool registrar protocol). 0.16.0
  completes the round-trip: registered Codex sessions can now be folded
  into the per-principal ledger via the ingest path.
- No breaking changes; additive to the dispatch surface. `opencode`,
  `gemini`, and `chatgpt-desktop` remain stubs with contributor-pointer
  `NotImplementedError`.
- Memory extension manifest bumped 0.3.0 → 0.4.0.
- Validates the per-tool adapter pattern that the remaining cross-tool
  Tier-2 work (jetbrains, vscode-copilot, gemini, chatgpt-desktop) will
  replicate per `docs/working/memory-roadmap.md`.

## [0.15.1] — 2026-05-08 — Re-release of 0.15.0 content (PyPI 0.15.0 was a phantom)

PyPI's `axiom-os-lm 0.15.0` (published 2026-05-06) was an accidental release built from an orphan version-bump commit (`dba60dd1`) that never reached `main`. It contained essentially only the version number change. The real 0.15.0 content — cross-tool memory MCP foundation, post-Prague hardening, and the agent rename — landed on `main` in PR #171 (2026-05-08) and ships as **0.15.1**.

The PyPI 0.15.0 release stays in place (immutable, can't be replaced); 0.15.1 is the canonical version users should install. Same surface as the [0.15.0] section below; treat that section as the change-log for 0.15.1's content.

## [0.15.0] — 2026-05-08 (PyPI release built from PR #171 — phantom on PyPI; superseded by 0.15.1) — Cross-tool memory MCP foundation + post-Prague tight-cut hardening

### Added — axiom-memory MCP server (per-extension topology)

The shared cross-tool, cross-session memory substrate. Every LLM tool the user
touches (Claude Code, Codex, Gemini, OpenCode, axi chat, future tools) writes
to and reads from the same per-principal ledger via a common path.

- New `axiom-memory` MCP server at `axiom.extensions.builtins.memory.mcp_server`
  exposing read tools (`axiom_memory_show`, `_recent`, `_search`) and write
  tools (`axiom_memory_append`). Server `instructions` field carries the
  cross-vendor model-discipline prompt for tools without on-disk session logs.
- New `axiom.memory.session_capture.record_session_turn()` — the common write
  path the MCP, CLI, and (eventual) `axi chat` all converge on. Provenance
  encodes originating tool + model in the agents set so cross-tool queries can
  scope by origin.
- `axi memory record [--principal] [--tool] [--user-input] [--assistant-output]`
  for shell-driven writes. `--json-stdin` reads a JSON event for hooks /
  automation.
- `axi memory ingest <path>` folds Claude Code session JSONL transcripts into
  the ledger via the common path. Idempotent on `content.extra.source_uuid`.
  `--watch` polling mode supports incremental ingest as the transcript grows.
- `axi memory ingest --tool <name>` dispatches by parser. claude-code is
  canonical; opencode / gemini / chatgpt-desktop are stubs raising
  NotImplementedError with a contributor pointer.
- Memory extension manifest declares the MCP via `[mcp_servers.axiom-memory]`
  so `axi ext mcp --target claude_code` aggregates it alongside other
  extensions.

### Added — post-Prague memory hardening (tight cut)

Items pulled forward from `project_axi_memory_failure_modes_and_selfheal` to
close the highest-leverage silent-dysfunction classes before Prague:

- **Default principal pin** via `memory.default_principal` setting. CLI and
  MCP fall back to the pin when callers omit `--principal` / `principal_id`,
  closing the cross-identity footgun (e.g. canonical UT email vs harness-
  provided account email).
- **Per-tool registrar protocol** in `axiom.extensions.builtins.memory.register_mcp`.
  `TOOL_REGISTRARS` maps tool name → `ToolRegistrar` (detect + register +
  is_registered). `axi memory register-mcp --all` walks every detected tool;
  `--tool <name>` for a single tool. New Codex registrar (writes
  `~/.codex/config.toml` via tomlkit, idempotent). Gemini and OpenCode are
  stubs.
- **Heartbeat fragment + freshness check.** `axi memory heartbeat` writes a
  periodic `fact_kind=heartbeat` fragment via the common path. `axi memory
  heartbeat-install` lays down a launchd plist
  (`~/Library/LaunchAgents/com.axiom.memory.heartbeat.plist`) for hourly
  cadence; `heartbeat-uninstall` reverses it. `axi dr` flags missing/stale
  heartbeats: ≤60min OK, 60–120min WARN, >120min ERROR.
- **`axi dr` principal-pin reconciliation.** Samples the 25 most-recent
  fragments and WARNs when recent writes used a principal other than the
  pinned default — catches drift before users wonder why memory is "empty."
- **`axi dr` MCP-registration check** (resolves symlinks so `python` ≡
  `python3.14` in the same venv don't false-positive as stale).

### Renamed — agents (IP remediation, was Unreleased before 0.15.0)

Renamed agents to remove Pixar/WALL-E derivation:
- WALL-E → AXI (the chat/orchestrator agent)
- EVE → SCAN
- M-O → TIDY
- BURN-E → RIVET
- D-FIB → TRIAGE
- PR-T → PRESS
- CURI-O → CURIO (dash dropped)
- CHALK-E → CHALKE (dash dropped)
- V-EGA → WARDEN

CLI nouns updated where they matched old agent names. PyPI script entry points
`walle` and `wall-e` removed (agent identity unified with the `axi` binary).
No backwards aliases — pre-public, no external users to deprecate.

### Notes

- Tests: 423 passed / 4 skipped in the targeted memory + cli + classroom sweep.
- 49 new tests across the post-Prague tight-cut items.
- Out of scope (still post-Prague queue): per-tool MCP liveness probe, file-
  watch on tool configs, audit-log → SQLite replay, RACI escalation surface,
  privacy knobs, opt-in install wizard, onboarding seed fragment, Linux
  systemd-timer install.

---

## [Pre-0.15.0 history — previously labeled Unreleased] — 2026-05-04 — Agent rename (IP remediation)

Renamed agents to remove Pixar/WALL-E derivation:
- WALL-E → AXI (the chat/orchestrator agent)
- EVE → SCAN
- M-O → TIDY
- BURN-E → RIVET
- D-FIB → TRIAGE
- PR-T → PRESS
- CURI-O → CURIO (dash dropped)
- CHALK-E → CHALKE (dash dropped)
- V-EGA → WARDEN

CLI nouns updated where they matched old agent names. PyPI script entry points `walle`
and `wall-e` removed (agent identity unified with the `axi` binary). No backwards
aliases — pre-public, no external users to deprecate.
