Generated banner for data-olympus
Generated logo for data-olympus

data-olympus

Memory serverOfficial Knaisoma project

Created by Knaisoma

Published in knaisoma/data-olympus. Publisher page

30 GitHub stars

What it is

Governance-grade, OKF-compatible knowledge-base format with a single-writer MCP server and CLI. Pre-release.
— the description by Knaisoma

Summary from the brain’s metadata

Governance-grade, OKF-compatible knowledge-base format with a single-writer MCP server and CLI. Pre-release. New here? Start with WHY.md. It is the story behind the project: the problem we kept hitting with coding agents, what data-olympus does differently, how it relates to Google's Open Knowledge Format, and where our benchmarks say it is strong and where it is not. The rest of this README is the technical reference.

How it works

Memory model: Markdown vault

AutoGineer overview, from the brain’s own files

Memory model: Markdown vault. Folders: .github (agent config, 20 files); bin (scripts, 7 files); docs (docs, 47 files); scripts (scripts, 29 files); src (source code, 81 files); tests (tests, 286 files). The agent reads .rules/versioning.md, example-bundle.

Folders
FolderRoleFiles
.githubagent-config20
binscripts7
docsdocs47
scriptsscripts29
srcsource-code81
teststests286
.rulesfolder5
benchmarksfolder328
deployfolder19
example-bundlefolder27
releasefolder2

The agent reads

Requirements

Runtimes: Python >=3.13

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Google Antigravity, Kiro, OpenCode, Amp, Roo Code, Cline, JetBrains Junie, Devin (ex-Windsurf)

Set it up

  1. Not scanned by AutoGineerIts dependencies are installed by pip or uv when you run it and were not scanned by AutoGineer (5 declared). https://pypi.org/project/data-olympus/
  2. Not scanned by AutoGineerAutoGineer could not scan everything it runs. https://pypi.org/project/data-olympus/
  3. Created by Knaisoma. Published in knaisoma/data-olympus. Licence: Apache License 2.0.

Usage

From the brain’s own files

  • Requires Python 3.13+ and uv.
  • Run the stable CLI directly from PyPI:
  • Install it persistently when you are ready to create a bundle and run the server:
  • An announced candidate remains opt in through its exact PyPI version.
  • Replace X.Y.ZrcN with the candidate named on the releases page, if any:
  • See release channels for what each channel means, how to verify a candidate before adopting it, and how to roll back.
  • See docs/quickstart.md for bundle initialization, server startup, readiness, agent registration, and the contributor source installation.
  • See docs/adoption.md for the full bundle authoring guide.

Documentation by Knaisoma

data-olympus

Image: knaisoma/data-olympus MCP server Image: MCP Marketplace

<!-- mcp-name: io.github.knaisoma/data-olympus -->

New here? Start with WHY.md. It is the story behind the project: the problem we kept hitting with coding agents, what data-olympus does differently, how it relates to Google's Open Knowledge Format, and where our benchmarks say it is strong and where it is not. The rest of this README is the technical reference.

data-olympus is a governance-grade knowledge-base format and server for agent workforces. It is readable by Open Knowledge Format (OKF) v0.2 consumers: it inherits OKF's directory structure, frontmatter conventions, reserved filenames, and link model, then layers governance extensions on top (stable id, controlled type/status/tier fields, supersedes chains) plus a single-writer MCP server and a CLI. CI proves two concrete directions against official Google OKF v0.2 commit ad30107c31c06aec8a7d5636e0d1058118604e6f: its reference visualization consumer reads every concept in example-bundle, and data-olympus imports, lints, indexes, searches, and retrieves the pinned official Bitcoin sample. This is fixture-scoped interoperability evidence, not a blanket guarantee for every OKF bundle or future upstream revision. The result is a git-native, version-controlled document graph of engineering standards, architectural decisions, and project knowledge that agents and humans can read, search, and extend without any proprietary service.

It governs decisions, not code. When an agent is about to make a choice (a library, a pattern, a migration), data-olympus surfaces the established standard or decision that should govern that choice. It is deliberately not a code-search, reference-finding, or "where is X used" tool: LSP, grep, and Sourcegraph already do that well. The retrieval task it targets is coding-intent to governing-rule, and it helps where current model interaction during vibe-coding is weakest: keeping the model aligned to patterns the team has already established as correct.

Status: pre-1.0 beta. Stable releases are distributed through PyPI and GHCR.

Why

  • Portable, no lock-in. The entire KB is a directory of markdown files in git. No database, no proprietary schema, no vendor.

  • Git-native diffs and review. Every change is a commit. Proposed edits go through a pending queue before commit; history is plain git log.

  • Agent and human readable. Plain markdown with YAML frontmatter. No SDK required to read or author a document.

  • Governed multi-agent writes. The single-writer MCP pipeline (advisory locks, per-session worktrees, durable push queue) prevents concurrent write races without requiring distributed locking infrastructure.

  • Queryable by status, tier, and type. Filter by status: accepted, tier: T1, or type: decision without post-processing. The supersedes chain makes it possible to trace decision history across the graph.

  • Tested with official OKF tooling. CI pins an exact Google OKF revision and proves both consumption directions over committed fixtures. The pin, fixture checksum, and Apache 2.0 license provenance live in tests/okf/reference.json.

Quickstart

Requires Python 3.13+ and uv. Run the stable CLI directly from PyPI:

uvx --from data-olympus data-olympus --help

Install it persistently when you are ready to create a bundle and run the server:

uv tool install data-olympus
data-olympus init my-kb
data-olympus-mcp --help

An announced candidate remains opt in through its exact PyPI version. Replace X.Y.ZrcN with the candidate named on the releases page, if any:

uvx --from 'data-olympus==X.Y.ZrcN' data-olympus --help

See release channels for what each channel means, how to verify a candidate before adopting it, and how to roll back.

See docs/quickstart.md for bundle initialization, server startup, readiness, agent registration, and the contributor source installation.

See docs/adoption.md for the full bundle authoring guide.

Documentation

  • SPEC.md: format specification (bundle layout, frontmatter schema, serving contracts).

  • docs/quickstart.md: verified local-run procedure.

  • docs/adoption.md: bring-your-own-KB guide (author, lint, index, serve, wire an agent).

  • docs/serving.md: single-replica serving model, read-only replicas, git pull loop, health/readiness/liveness split, proxy headers, audit-log rotation.

  • docs/operations.md: production runbook: backup, upgrade, recovery playbooks (degraded/fetch-failed, history rewrite, frozen/demoted push entries, orphaned locks), the health/alerting model, and release channels (stable and candidate, verification, rollback).

  • docs/comparison.md: how data-olympus relates to OKF, enterprise catalogs, markdown KB tools, agent-context conventions, RAG, and ADR tooling.

  • docs/okf-profile.md: field-by-field OKF profile: which governance extensions are stable, which are runtime-only serving fields, and which are experimental candidates.

  • docs/glama.md: Glama registry claim, release, and score-maintenance notes.

  • docs/mcp-registry.md: official MCP Registry notes, what server.json declares, and the checklist for publishing.

  • docs/enforcement.md: turning the KB into a mandatory consultation gate (hooks, kb enforce).

  • benchmarks/README.md: retrieval benchmark methodology and how to reproduce the numbers in docs/comparison.md.

  • SECURITY.md: supported versions and how to report a vulnerability.

License

Apache 2.0. See LICENSE and NOTICE.

CHANGELOG.md

Changelog

All notable changes to this project will be documented in this file.

The format follows Keep a Changelog. This project uses Semantic Versioning.

<!-- CONVENTION: The topmost section should always be an [Unreleased] block for changes merged to main but not yet tagged. When a release is cut, rename that block to the version and date, then open a new empty [Unreleased] block above it. Never hand-edit the release date on the [Unreleased] heading. -->

Unreleased

Removed
  • The old release path is retired now that the new-model pipeline has shipped 0.11.1 and 0.11.2. Removed workflows: rc-publish.yml, tag-release.yml, set-channel.yml, release-image.yml, release-image-reusable.yml, publish-pypi.yml and publish-pypi-reusable.yml. Removed old-path scripts: rc_tag.py, release_upload.py and workflow_validation.py. Candidates are built by rc-build.yml, published by rc-publish-stage.yml and promoted by promote-release.yml; hotfixes start with hotfix-cut.yml. The pull request packaging dry run moves unchanged to package-dry-run.yml. hotfix-cut.yml and recut-release-branch.yml no longer list old-path runs and no longer hold actions: read. The rc image channel is no longer moved by any workflow while SDLC_RC_CHANNEL is unset.

Security
  • Dependencies: the 2026-W44 Pillow remediation (issue #426, items 1 and 3) updates the locked pillow from 12.3.0 to 13.0.0. Pillow 13 needs fastembed 0.9.1, the first release that allows it, so the embeddings extra and the dev extra now require fastembed>=0.9.1,<0.10 (locked 0.8.1 to 0.9.1). data-olympus calls only TextEmbedding(model_name=...).embed(...), which fastembed 0.9 keeps. Upgrade note for the embeddings extra: fastembed 0.9 downloads built-in models from Hugging Face only, without the Google Cloud Storage fallback; a model cached by an older fastembed still loads, with a FutureWarning. The lock is regenerated with the pinned uv 0.12.23, which writes lock revision 5. The container image installs neither extra, so its contents are unchanged.

  • Container image: git refuses the http, https, ftp and ftps transports. The image's system gitconfig sets protocol.<name>.allow=never for each, so git stops with "transport 'https' not allowed" before it starts a libcurl remote helper. SSH and local remotes are unchanged; the server's KB clone, fetch and push run over SSH. Breaking for image users whose KB_REMOTE_URL or KB_GIT_REMOTE_URL is an HTTPS URL: switch to an SSH remote. A server installed from PyPI keeps the host's git configuration. The libcurl3t64-gnutls package stays installed as a git dependency, so its scanner findings remain. A new CI job builds the image and runs tests/image/git_protocol_policy.sh inside it.

  • Container image: uv is pinned by content. The image copies the static uv and uvx 0.12.23 binaries from astral's official ghcr.io/astral-sh/uv:0.12.23 image by its multi-arch index digest (sha256:61d393e4..., which carries astral's signed build provenance from astral-sh/uv's publish-docker-image.yml) into /usr/local/bin, instead of pip install uv==0.12.23, which checked the version but not the bytes. The uv version is unchanged; the binary is now the musl build, and uv is no longer a Python package in the image's system site-packages.

  • Container image: rebuild on the current python:3.13-slim index digest (sha256:70729b46..., Python 3.13.16 on Debian trixie with current security updates), raise the pinned uv from 0.5.4 to 0.12.23, and stop installing curl, which nothing in the image uses (probes are httpGet/tcpSocket; the bin/kb client scripts that call curl run on operator machines). On the release candidate assessment this takes the blocking vulnerability-gate matches from 358 to 106 per platform; the rest are findings without a fix or with a fix only on Python 3.15. git still pulls in libcurl3t64-gnutls for its http helpers. Anyone who ran curl inside the container by hand needs another tool, such as python -c with urllib.request.

Changed
  • The old-path set-channel.yml and tag-release.yml workflows now take the shared data-olympus-promotion concurrency group (unconditional, cancel-in-progress: false), so they cannot run at the same time as promote-release.yml, rc-publish-stage.yml, recut-release-branch.yml or hotfix-cut.yml. tag-release.yml enters the lock only when dispatched from main and drops its per-candidate group, so two different candidates now serialise. Recorded as an R9 deviation (operator follow-up decision of 2026-10-10). GitHub keeps one pending run per group and a newer pending run cancels an older pending one: dispatch again if that happens.

Added
  • The vulnerability gate judges fix availability by release line (operator ruling, SDLC section 13.2). A fix counts when it is on the installed line or on a newer line eligible for adoption. release/adoption-lines.json lists newer lines not yet eligible (Python 3.15: general availability 2026-10-09, eligible from 2026-12-08); a finding whose fixes are only on such a line follows the no-fix rules until that date and is listed with its fix versions and eligible_from in the evidence and job output, then blocks as fixable. Unlisted newer lines count (fail closed). The release record names the file's Git blob at S. See docs/releases/vulnerability-gate.md.

  • Vulnerability gate for promotion (SDLC section 12, rulings of 2026-10-18). A new vulnerability-gate job in promote-release.yml (read-only, no secrets) scans the candidate image by digest on both platforms with grype 0.119.0 (download checked against its published SHA-256) and a fresh database, records the scanner version and database build time as evidence, and blocks promotion on any finding with a fix available, on Critical, High or Unknown severity findings without a fix unless a scoped, approved, unexpired exception in the new release/exceptions.json covers them, and on expired exceptions. Medium, Low and Negligible findings without a fix are recorded. Malformed scanner output, a report naming another database, or a database built more than five days before the run refuses the gate. scripts/vulnerability_gate.py validate checks the exception file. The security_alerts.py gate is unchanged. See docs/releases/vulnerability-gate.md.

  • Per-artifact SBOMs for release candidates (SDLC section 9). rc-build.yml generates one SPDX 2.3 document per wheel, sdist and image platform with syft 1.52.0 (download checked against its published SHA-256); stage two binds each document to the artifact it verified, stores it as a candidate release asset and signs it with actions/attest (predicate https://spdx.dev/Document/v2.3) on the wheel or sdist hash or the platform manifest digest; promote-release.yml refuses a candidate unless every SBOM attestation verifies with the stage-two signer and carries the stored document. release-record.json gains an optional sboms list (schema version unchanged). See docs/releases/sbom.md.

  • Add docs/releases/repository-settings.md, a checklist of the GitHub repository and organization settings the release pipeline requires (immutable releases, variables, environments, Trusted Publishers, rulesets, code scanning, Dependabot, the GitHub App and the Actions policy), each with a read-only gh api read-back.

  • This is the first release cut under the STD-U-821 branch model (release/new, content-derived versions, reviewed promotion). Candidates are built unprivileged and published by a separate trusted stage; releases are drafted, filled with assets and published last, which GitHub immutable releases require. Candidate 0.11.1-rc.2 of the first live run is burned.

  • Add a disabled-by-default trusted RC publication stage (rc-publish-stage.yml) that runs only main scripts, recomputes the candidate version from H's history as data, verifies package hashes and the OCI digest, splits reservation, PyPI OIDC upload and image push into least-privilege jobs, refuses duplicate identities, reuses same-H objects on retry, attests the published image digest and Python files for promotion to verify, and records branch-based staging selection with hotfix priority. Credentials may only be environment secrets on main-only environments. Only admitted runs take the shared promotion lock, signing runs in a separate job that never parses the candidate archives, and the rc channel moves only once SDLC_RC_CHANNEL=enabled is set after the first new-model release.

  • Add disabled-by-default promotion of reviewed release squashes, with exact parent, tree, version and release-note proofs. Compare stable Python files with both candidate payloads and reuse the RC image digest with provenance mapping the reviewed head, squash and stable tag.

  • Build normal and hotfix release candidates with read-only permissions, injected Python versions, an OCI archive, and provenance. Cut builds and dry runs build and verify artifacts without allowing promotion.

  • Wire the ratified STD-U-821 adoption record into candidate builds, the trusted publication stage and promotion. Builds of an adoption cut take the pinned ratification and vendored amendment from main's Git blobs rather than the candidate tree, the publication stage uses its own main checkout, promotion reads them from the candidate's recorded main, and only dispatched dry runs evaluate the record unratified. The engine now refuses a ratification passed without the standard file, and the build parser refuses any binding of the pinned names other than one top-level literal. The cut build stays unpromotable and promotion still refuses while the record exists at the reviewed head. Add the adoption cut runbook (docs/releases/adoption-cut-runbook.md).

  • Plan verified release branch recuts and stable hotfix cuts, preserve pending commits on a backup branch, and refuse stale or unreconciled state, including stray stable tags and active old-path release runs. The new workflows share the promotion lock, apply only from a main-restricted environment with a per-run GitHub App token, and remain disabled until W9 bot activation. See branch recut and hotfix cuts.

Changed
  • Pin the build backend to hatchling==1.32.4 in [build-system] requires, the exact version recorded in the published 0.11.1 wheel (Generator: hatchling 1.32.4), so release artifacts are rebuilt reproducibly. A test fails if the requirement floats again.

  • Record that the pypi and pypi-rc environments have no required reviewers by operator decision of 2026-10-09, with the consequence for promotion dispatches and the read-back commands.

  • Record the lessons of the first adoption cut in the adoption cut runbook, including the main-first ordering, the burned candidates and the minimum commit count on release/new.

  • scripts/sdlc_recut.py --apply now replaces release/new by deleting it and creating it again at the new base, instead of one atomic --force-with-lease update. The protected-branch rulesets require pull requests for updates and no App holds a bypass, while creation and deletion stay permitted. The old head is backed up to its preservation ref first, each single-ref push is guarded by remote reads before and after it, each deletion is a compare-and-swap push leased on the planned old head, creations carry no lease, no push uses a plain force, a + refspec or --atomic, and a verified hotfix/new is deleted only after the recreation. If recreation fails after the deletion, one recovery push restores the old head, also when the remote cannot be read, and the error names the old head, the backup ref and the exact recovery command. docs/releases/branch-recut.md documents the leased deletion, the explicit decision any plain-delete fallback would need, and the controller's ruleset toggle.

Security
  • Promotion now verifies the stage-two GitHub artifact attestations of the candidate wheel, sdist and image digest (gh attestation verify with the rc-publish-stage.yml signer workflow and refs/heads/main source ref) in the prove job, before any publication. The exact-source check gate moves into release_record.py checks: the reviewed head needs test, and the squash, whose tree the proof shows equal to the head's, needs test, CodeQL and every configured analysis, since release/new heads are not guaranteed CodeQL while the squash on main is. Only a completed run with conclusion success counts.

  • Validate manual publication identities and reject collisions, bind Python builds to validated commit SHAs, and move image channels by verified digest. Remove tag-triggered rebuild publication and OIDC grants from PR packaging builds.

Fixed
  • The release record notes command requires --output and no longer prints the rendered notes to standard output, which removes the log sink that code scanning flagged (a false positive carried through the ratification inputs).

  • Release an onboarding bootstrap in-flight marker only for the claim that wrote it. release removed the marker unconditionally, so a bootstrap whose claim outlived its TTL, and whose slot another bootstrap then reclaimed, deleted the new holder's live marker when it finally failed and released, admitting a third bootstrap into the convergence window. Each claim now stores a random token in its marker and returns it, and release removes the marker only when the token matches, reading, comparing and unlinking under the same per-key lock as the claim. A mismatched, missing or repeated release returns False and does not raise. A marker written by an earlier version has no token and is never released; it holds the slot until its recorded expiry and is then reclaimed, the same bound a crashed claimer has.

  • Make the onboarding bootstrap in-flight claim single-winner. The claim took a lock-free O_CREAT|O_EXCL fast path and locked only the reclaim of a stale marker, and the two paths did not exclude each other. A marker created but not yet written read as expired and was reclaimed under its writer, and a reclaimer's unlink let a fast-path contender recreate the marker in the gap. Concurrent claims could therefore both win, reopening the double-bootstrap race the guard exists to close, or fail with FileExistsError (about 1% and 10 to 20% of rounds at 16 threads). This is also why test_concurrent_reclaim_grants_exactly_one_winner failed intermittently in CI. Claim and release now run under a per-key flock, which the kernel drops if the holder dies, so a crash no longer leaves a .reclaim lock file that blocks reclaim forever. The marker is written to a temporary file and renamed into place.

  • Let the STD-U-821 adoption record name the highest stable tag as its base, as authorized by STD-U-821 amendment 1.5 (ratified 2026-10-10, company-knowledge #236, vendored at docs/releases/std-u-821-amendment-1.5.md). The version engine accepted the record only with base 0.11.0, so once v0.11.1 existed and main had advanced past it (#358 to #365) the next cycle could be cut neither at main (missing_tags_in_released_product) nor at the tag (recut_required). The engine now takes the base from the record: a strict X.Y.Z whose vX.Y.Z exists, is the highest stable tag by SemVer precedence, is an ancestor of the cut and is a strict ancestor of the anchor. It refuses the record when any v-digit tag in the repository is not a strict stable or prerelease version, and it requires both ratification lines (amendment 1.3 and amendment 1.5, pinned as RATIFIED and EXTENSION_RATIFIED in scripts/adoption_ratification.py and read from main by every stage); without either it refuses with adoption_unratified. Every other record rule is unchanged. The record is single use per record, only while its base is the highest stable tag. The runbook gains "Reusing the record route after a release", with a check that src/ and pyproject.toml are unchanged between the tag and the anchor. Known out-of-scope commits on main after v0.11.1: #363 and #365 (onboarding product fixes, outside amendment 1.5 scope: product changes must use release/new), and #362 (build backend pin, borderline: build configuration). The next adoption issue records the handling decided by the operator or lead at cut time: (a) revert them on main and re-land through release/new, or (b) a recorded exception in the issue. Nothing is blocked now because there is no release to cut.

  • Continue a promote-release.yml promotion after its stable tag exists. The engine counted the new vX.Y.Z on S as a released version, so the recheck after tagging recomputed the release against a stable tag above its own base, and every promotion of an adoption cut (as well as hotfixes and first releases) failed its post-tag recheck with bad_adoption. The first live promotion stopped there with v0.11.1 pushed and nothing after it. compute_version takes ignore_tags (exact vX.Y.Z names, empty by default). The resume proofs pass only the release's own tag, and only after verifying that it is annotated, names S, has the App bot as tagger and carries the generated notes byte for byte. The resume-pypi phase accepts that tag, prove resolves the App identity before its proof, and resume-state requires GitHub's ref to name the verified tag object. It also accepts the states the later jobs leave behind (GHCR vX.Y.Z absent or on the candidate digest, a channel on the candidate only after vX.Y.Z), and it checks a release visible to its token before any channel moves: generated notes, not a prerelease, only this run's assets, and all four on a published release. create-tag stays idempotent. Any other tag fails closed. The docs no longer claim a ruleset restricts v* tag creation; the control is content equality with the generated tag.

  • Verify the stable PyPI upload of promote-release.yml without the attestation files. The PyPI publish action writes a <file>.publish.attestation next to each uploaded file, and the inline verifier compared every file in the directory with PyPI's two, so the first live promotion uploaded 0.11.1 to PyPI and then failed before the tag, the image channels and the GitHub release. release_record.py verify-pypi now compares exactly the wheel and the sdist named by the stable provenance with PyPI, requires both attestations when the upload ran (and their absence when it did not) and refuses any other file. A new resume_pypi dispatch input completes such a promotion: it proves S is still on main (which may have advanced), that PyPI already holds exactly the stable wheel and sdist, and that the stable tag, the GHCR version tag, a published GitHub release visible to the job's token and the stable/latest move to the candidate are all absent, then skips only the upload while keeping the pypi approval. A draft release is probably invisible to that read-only token; the release job refuses a foreign draft.

  • Make the promotion check gate (release_record.py checks) ask each commit for the evidence GitHub produces there. The aggregate CodeQL check run exists only on pull request heads, so it was never present on the squash S pushed to main and the gate could never pass. H, the release pull request head, now requires test and aggregate CodeQL from the GitHub Advanced Security app, the only observed producer of that check run (github-code-scanning is kept as the alternate slug; a run named CodeQL from GitHub Actions is refused); S requires test and every analysis in SDLC_REQUIRED_CODEQL_CHECKS. The proof that the trees of S and H are equal ties the two results to the same code. All other rules stay fail closed.

  • Accept the subject release: X.Y.Z (STD-U-821) in the PR title lint, only for a same-repository pull request from release/new or hotfix/new into main; every other base or head still rejects the release type, and malformed versions stay rejected. For that subject the squash-impact comparison is skipped, since the engine computes the version from the commits and promotion proves subject and body byte for byte. The workflow passes the branch names and repositories as environment variables. The workflow runs the base-ref copy of the linter, so this change applies to pull requests only after it lands on the base branch they target.

  • Tolerate GitHub release listing lag after creating a draft and after uploading assets, in stage 2 reserve and in the stable release step: the read is repeated with bounded backoff (about 60 seconds) and then fails closed as before.

  • Publish candidate and stable GitHub releases draft first, for immutable releases. Stage 2 reserve now creates a draft prerelease, uploads and reads back every asset, and a new last finalize job publishes the draft only after PyPI, the image and the attestations are complete. Drafts are found by listing releases and matching the exact tag. promote-release.yml creates the stable release as a draft and release_record.py release publishes it after verifying every asset. A published release with a missing or different asset is refused as burned; 0.11.1-rc.2 is the known burned candidate. The candidate tag message now binds every asset's SHA-256 so the read-only jobs check bytes, reserve refuses a created release that is not a draft, and attest and finalize still run when pypi is skipped.

  • Show a sanitized, single-line, redacted stderr excerpt of at most 300 characters when a stage-two gh or skopeo command fails, never stdout or arguments.

  • Accept the documented 0.0.0+unreleased placeholder package version only where the engine-managed cycle puts it. version-free-guard skips it with an explicit message on pull requests into release/new and hotfix/new, and into main only when the pull request head tree equals origin/release/new or origin/hotfix/new (the reviewed release or hotfix squash) or main already declares it unchanged. It fails closed on the placeholder anywhere else, on any other non X.Y.Z version and on an unexpected decision. The server.json tests waive version equality on those branches while still requiring a concrete stable entry. The rule lives in scripts/placeholder_version.py and is described in docs/releases/placeholder-version.md.

  • Accept the ratified Amendment 1.3 heading for adoption verification while retaining the proposed heading form. Include the ratified amendment and pinned verification arguments so CI can verify it without private KB access.

  • Require an adoption cut to add only its record, block promotion until that record is absent from the candidate tree, and validate dated ratification references against UTC. The CLI requires a standard file with a matching ratification line in the Proposed amendment 1.3 section.

  • Preserve the gate server's uncovered signals and suggested consult intent in pre-tool hook denials. Replace controls and characters outside printable ASCII and Unicode letters with spaces for every dialect, then retain workspace/session tokens within a 1200-character cap on the whole message. Empty reasons continue to use the generic consultation guidance.

[0.11.0] - 2026-10-05

Added
  • derived_from: name the dependents of a retired document (#300). A document may declare derived_from, a concept id or a list of ids its guidance was drawn from. When a source leaves force (graph-excluded, deprecated, superseded, rejected or expired), kb_get names the in-force dependents on the source (dependents_to_review) and the retired sources on the dependent (derived_from_retired), and data-olympus lint warns on both ends. rejected counts as retired on purpose; draft and proposed never governed and do not. It is a cue for a person: nothing is filtered, demoted, ranked or invalidated, and kb_search and kb_consult are unchanged. Lint errors on a malformed or unresolved target (downgradable with --unresolved-targets warn), self-reference, derivation cycles, and deriving from a document the same document supersedes; the write path refuses a newly introduced unresolved or malformed target but never a write because a source retired. The format moves to 0.5, a backward-compatible addition; no migration.

  • Capture provenance on memory proposals (first slice of #141). Memory proposals accept an optional capture envelope for a memory distilled from a passive capture such as a hook event or an external transcript: capture_source, capture_event_id, source_event_hash, transformation and raw_retention are required, and capture_session, classification and a server-verified derived_memory_hash are optional. It holds identifiers, hashes and enums only. It is written to the memory's frontmatter, shown in the pending listing and readback, recorded in the committed and pending audit events, and counted by the session recap as capture_derived; an operator edit at resolve keeps it. It is a label, not authority: it never forces review and never makes a memory in force. An invalid envelope is refused as rejected_invalid_capture (HTTP 400) and a credential-shaped value as rejected_secret_detected.

  • Writing rules on the documents agents write (#283). The write pipeline now applies the project's raw writing rules (no em-dash, no en-dash used as one, no agent authorship credit) to the lines a write ADDS, on every commit path: propose memory, propose edit, resolve, and onboarding bootstrap. A write never fails on text it did not add: a line counts as added only when it occurs more often in the postimage than in the target's current content, so moving an existing line is not adding it, while a new offending line is always caught. A line ending in <!-- prose-lint: allow --> is exempt. KB_WRITING_RULES_MODE selects enforce (reject as rejected_writing_rule, HTTP 422), warn (the default: commit, return the findings in writing_rule_findings, record rule names and line numbers in the audit event, and log a warning) or off; an unknown value fails startup. KB_WRITING_RULES_EXCLUDE_PATHS skips paths by glob. The check runs after the secret scan, so a postimage with both a credential and a finding is still reported as the redacted secret rejection, and a failure of the check itself rejects in enforce rather than being skipped. The machine-rendered maintenance ledger is exempt through its own call path, which no client can reach. Upgrade note: the default warn changes nothing for existing writes; set enforce once a knowledge base adopts these rules.

  • The stable release now publishes the MCP Registry entry (#303, part of #111). After the GitHub release, the promotion workflow signs in to the official registry with GitHub OIDC and publishes server.json as io.github.knaisoma/data-olympus, then reads the entry back. It first checks that server.json names the released version and that the release's PyPI description carries the ownership marker, and it runs a pinned, hash-verified mcp-publisher. No secret or personal sign-in is involved.

Changed
  • MCP SDK 1.30.0, Starlette 1.7.0, uvicorn 0.54.0 and sse-starlette 3.5.0, with a direct mcp>=1.30.0,<2 requirement. The MCP SDK now closes a streamable HTTP session that has had no request in flight for 30 minutes and holds at most 10000 sessions (HTTP 503 beyond that); an open SSE stream keeps a session alive. See Upgrading in the release notes.

  • Onboarding bootstrap creates only new files under the workspace it onboards. Every target must lie under projects/<workspace>/, or under projects/<workspace>/components/<component>/ when a component is given, in every onboarding state; other paths are refused as rejected_path_not_indexable_or_blocked, and a target that already exists is refused at commit time as rejected_already_onboarded. Use kb_propose_edit to change an existing document. The bootstrap commit subject lists every path written.

  • kb_propose_edit accepts base_commit only as HEAD (or head) or a commit id of 7 to 64 lowercase hex characters. Any other value is refused as rejected_invalid_base and is not recorded.

  • Configuration is checked at startup where it used to be ignored. A set but unusable KB_AUTH_PRINCIPALS (invalid JSON, not a list, an empty list, an entry that is not an object or has no non-empty token) stops startup with an error naming the setting and entry number. KB_SECRET_SCAN_EXTRA_PATTERNS is parsed once at startup, accepts a JSON array of patterns, no longer splits a pattern at a comma inside {...} or [...] (so ACME_[A-Z0-9]{20,40} stays one pattern), and an invalid or nested-quantifier pattern stops startup instead of being skipped. KB_READ_ONLY, KB_DISABLE_VERSION_CHECK and KB_STATUS_AUTOFILL accept 1, true, yes, on, 0, false, no and off (case-insensitive) and stop startup on any other value; unset or empty keeps each default, so autofill stays on (#314, #315; the KB_STATUS_AUTOFILL case was raised by @thejedi433).

  • The benchmark receipt guard accepts a measured commit held by a benchmarks/receipt-* tag. A change to a benchmark corpus has to be re-measured in its own pull request, because corpora are compared with the current tree, and a squash merge then removes the measured commit from main. A receipt tag pointing exactly at that commit keeps it resolvable, so the pull request check accepts it; the other receipt checks are unchanged. benchmarks/README.md describes the three refresh routes.

  • Every path under universal/ is tier T1 in the default taxonomy. Only seven universal/ subdirectories were classified; any other path there, such as universal/README.md, fell through to meta. Those seven keep their categories, other paths get the category universal, and the CLI fallback matches. Search facets for those documents move from meta to T1, and KB_WRITE_BLOCK_TIERS=T1 now covers the whole of universal/.

  • kb enforce report judges consult coverage like the live gate (#309). The report and its --staged commit gate count a governed commit as verified only when the explicit consults in the window cover every signal of its governed paths, with the same family rules as the gate. The consult audit event records the consult's coverage, and the audit API now returns it with trigger. Rows written before this release keep timing-only judgement and are reported as TIMING ONLY (timing_only in --json); KB_GATE_CLEARANCE=pair keeps timing-only behaviour throughout, and an invalid value makes the report exit 2. The gate's suggested kb_consult call now stays well formed for a path containing a quote character.

  • data-olympus lint warns when frontmatter disagrees with the path taxonomy (#304). A document whose declared tier or category differs from what its path implies (honouring KB_TAXONOMY_PATH) gets a warning naming the path value, because the index uses the declared value while the write blocklist governs writes by the path tier. It is never an error and never changes the exit code.

  • A consult now clears only the governed actions it covers (#296). The enforcement gate used to clear any governed action after any fresh explicit consult in the session, whatever that consult was about. Each consult now records the keywords, command fragments and paths its intent names, and a governed action is allowed only when all of its signals are covered by a fresh consult, directly or through a small family mapping (a "dependency" consult covers manifest edits and install commands; a "schema" or "migration" consult covers migration and SQL files). Coverage from several fresh consults combines, and each part expires on its own TTL. Naming the action's own path or command always clears it. A denial still answers consult_required; its reason lists the uncovered signals and suggests one intent that covers them all, and the gate_block audit event gains an uncovered field. Upgrade note: KB_GATE_CLEARANCE=intent is the new default; set KB_GATE_CLEARANCE=pair to keep the previous rule. An invalid value fails startup. A session that consulted before the upgrade needs one new consult.

  • fastembed 0.8.1 for the optional embeddings extra (#298). It fixes a path traversal in model archive extraction that could write outside the model cache, and a padding regression on mixed-length batches. Upgrade note: fastembed now resolves the default BAAI/bge-small-en-v1.5 (and BAAI/bge-base-en-v1.5) to a repository name with different casing, so on a case-sensitive filesystem the cached model is not reused and downloads once more. A deployment with KB_EMBEDDINGS_MODE=on that runs without network access must refresh its model cache before upgrading, or startup fails with the model unavailable. Lexical-only deployments are unaffected.

  • ruff 0.16.9 for development (#297).

  • The CLI's fallback taxonomy is held to the index's (#304). The dependency-free search fallback (bin/_kb_fallback.py) keeps its own copy of the path taxonomy; tests now assert that both copies list the same rules in the same order and classify the same paths identically, including with a custom KB_TAXONOMY_PATH. A comment was the only thing aligning them before.

  • The writing-rule linter ships inside the package (groundwork for #283). The vendored gate moved from scripts/prose_lint.py to data_olympus._vendor.prose_lint, so the installed server can apply the same rules the CI gate applies. scripts/prose_lint.py remains as a shim that runs it and now needs the package installed, so run it as uv run python scripts/prose_lint.py <path>.

Fixed
  • A session closed by the idle reaper now leaves the session table. With MCP SDK 1.29.1, reaped sessions stayed listed, so live_sessions in health never decreased and the closed session objects were kept until restart.

  • REST routes refuse a non-string value in a string field (#310) with HTTP 400 naming the field, and record nothing. Previously consult and record event could write such a value to the audit log, after which the audit and compliance readers failed. kb_audit and kb_compliance now skip a malformed audit line and report it in a new skipped count.

  • An empty bootstrap bundle is refused as rejected_empty_bundle (HTTP

    1. before any claim, rate-limit slot, commit or pending entry (#311). Previously it answered 500, or reported a pending bootstrap that was never parked.

  • Without KB_REMOTE_URL, the MCP tools kb_list_pending, kb_get_pending and kb_audit answer like their REST equivalents instead of failing with an empty tool error (#312).

  • REST resolve answers 400, not 200, for a symlink-escape refusal (#313).

  • REST resolve no longer answers 200 for an unknown decision (#307). A decision other than approve, reject or edit (for example the typo aprove) returned HTTP 200 with status: rejected_bad_decision, so a client checking only the status code read it as applied. It is now HTTP 400. A test pins the HTTP code of every status the write tools can return, so a new one cannot fall through to a default unnoticed.

  • A bare glibc.malloc.trim_threshold tunable no longer stops the trim threshold pin (#302). The server leaves the trim threshold alone when the operator has set it, and it treated any glibc.malloc.trim_threshold entry in GLIBC_TUNABLES as such a setting, including one with no =value. glibc ignores a tunable without a value, so under that malformed configuration nobody set the threshold and the memory retention fixed in 0.10.0 (#284) came back. Only an entry with a non-empty value now counts.

Security
  • PyJWT 2.15.1 (GHSA-r6x4-923q-g947, GHSA-w2cx-738m-mc7w, both high). The version 0.10.0 resolved, 2.13.0, could accept public key material, such as a JWK container or a BOM-prefixed key, as an HMAC secret. PyJWT reaches Data Olympus only through the MCP SDK's optional crypto extra, and Data Olympus does not call it directly; the lock now resolves the fixed release.

  • urllib3 2.8.0 (GHSA-8988-9cw3-xx77 and GHSA-vxq7-64xx-v4gw, high; GHSA-gh4c-6fx4-qh6g, medium): HTTPS proxy TLS settings could be ignored, and a chunked response could make the client buffer an unbounded line. urllib3 reaches Data Olympus only through requests, which the optional embeddings extra uses to download its model. Upstream notes that a configuration relying on destination TLS settings for an HTTPS proxy may need proxy_ssl_context instead.

  • cryptography 50.0.2, whose wheels are built against OpenSSL 4.0.3.

0.10.0 - 2026-09-28

Added
  • KB_GIT_BRANCH: serve a knowledge base whose trunk is not called main. Every git operation the server ran against the knowledge-base remote named main literally, so a repository on master (or any other trunk) could not be served: the refresh loop's fetch origin main failed and health stayed degraded with the index frozen at whatever was built at bootstrap. Two further consequences were quieter. A write supplying an ENFORCEABLE base marker (a pinned base_commit, a base_blob_sha or a target_file_hash; base_commit: "HEAD" is advisory and does not count) was refused rejected_stale_base, because the compare-and-swap base could not be refreshed. A write with no enforceable marker committed and pushed HEAD:main, creating that branch on a remote that had none. The refresh loop then fetched and merged the branch it had just been given, so a server could end up following a trunk its writes had invented while the repository's real trunk went on being ignored. The setting now feeds the fetch, the fast-forward, the session worktree rebase, the push, and the reachability checks that decide whether a session worktree still holds unpushed commits and may be garbage collected. Every ref built from it is fully qualified (refs/heads/<branch>, refs/remotes/origin/<branch>), so a name shaped like a ref path or colliding with a tag cannot select another namespace. It defaults to main, so an existing deployment behaves exactly as before, and it is validated when configuration loads: a value that is not a usable branch name fails startup with the setting named and the reason stated, rather than reaching a git command line, which matters most for a value starting with - that git would read as an option. A blank value means unset and yields the default, as the path settings already document. (#289)

Changed
  • Release workflows publish only what is on main. rc-publish.yml now stops unless it is dispatched from main and its ref input resolves to an ancestor of main, so a candidate can no longer be built from an unmerged branch or an arbitrary SHA. tag-release.yml also refuses a dispatch from any other branch, and prints the candidate tag, source SHA and image digest it resolved, so the exact promotion source is visible in the run log before the protected PyPI environment is approved. (#293)

  • Benchmark extra: sentence-transformers 6.0.1 to 6.1.0. Lock only, and the benchmark extra is not installed by the server or by the ordinary test job, so nothing a deployment runs changes. The regenerated lock moves that one package and no transitive dependency shared with a runtime package. (#285)

  • The doc-consistency CI guard now also checks documented configuration defaults, not just schema enums. For each session reaping variable it reads every (default ...) the prose of docs/serving.md states immediately after that variable, outside fenced examples, and compares it with the corresponding Config field, so lowering a default in code and updating only one of the places the documentation states it fails CI instead of ageing quietly. The guard reports the document line and both values. It covers this one document and this one list of variables rather than every setting the project has, and it requires the value as a whole number in the unit the configuration uses, with any gloss after a comma, so that a form it cannot read whole fails loudly instead of being certified from its leading digits. (#286)

Fixed
  • Resident memory no longer ratchets up with every index rebuild. A server with a 1 GiB limit could be OOM-killed after a burst of writes and, short of that, sat for hours at several times its starting size while idle. Nothing leaked at the Python level. A rebuild frees its working set when it returns, but glibc keeps free memory in its per-thread arenas, and rebuilds and request handlers run on different long-lived threads; glibc also raises its trim threshold after large frees, so each worker arena kept its free top as well. The server now pins the trim threshold at startup (mallopt) and returns free memory after the bootstrap build and at the end of every refresh tick (malloc_trim(0)). Both are glibc only and do nothing elsewhere. An operator's own trim threshold (MALLOC_TRIM_THRESHOLD_ or GLIBC_TUNABLES) is left alone. Replaying the burst that triggered the report with concurrent read traffic, the unchanged server held 321 MiB after 32 rebuilds and a short idle, from a start of 110 MiB; with this change it held 126 MiB after 24, from 81 MiB, and its peak fell from 359 MiB to 208 MiB. docs/serving.md ("Memory between rebuilds") has the measurements. (#284)

  • Several documented settings were read and then silently ignored. load_config() read KB_CONSULT_TTL_SEC, KB_PENDING_CLAIM_TTL_SEC, KB_TRIGRAM_MODE and KB_TRIGRAM_FALLBACK_THRESHOLD correctly, but build_app rebuilt its configuration from its own keyword arguments, and it took none of these as parameters, so the running server used their defaults. In practice the trigram typo and identifier fallback could not be enabled in a deployment started through the shipped entry point; the consultation freshness window, for kb_consult, for kb_gate_check and for the consultation ledger's retention, was fixed at 300 seconds; and the claim guard used a 900-second claim TTL while the background sweep in the same process used the configured one. The same rebuild also dropped ledger_path and the co-occurrence settings from the running state and hardcoded http_port to 8080 there, which was harmless only because those values happen to be read from the original configuration elsewhere. None of this was new in 0.10.0. The production entry point now hands the loaded configuration to the server unchanged, so the running state is exactly what was loaded. A parity test moves every configuration field it can safely change off its loaded value, names any field that does not reach the running state, and asserts the running configuration is the loaded object itself, which also covers the few fields it leaves alone; an end-to-end test checks the objects the server builds from these four settings. (#291)

  • docs/serving.md described session reaping as it behaved before 0.3.3. Its session reference section still said KB_SESSION_IDLE_TIMEOUT_SEC defaults to 1800s / 30 min, and still said a quiet long-lived GET SSE stream stamps no activity and is reaped on that timer, with excluding open streams from reaping listed as a possible follow-up. None of that has been true since the SSE session-churn fix, which is in 0.3.3: the default is 300, the activity middleware runs a keep-alive task for an open stream and re-stamps the session every KB_SESSION_TOUCH_INTERVAL_SEC, and the server clamps that interval to at most a third of the idle window, so the stamps are scheduled at least three times per window. The summary bullet near the top of the same document already said all of this, so the document contradicted itself, and the reference section is the half an operator reads when sizing a client keep-alive. Both halves now agree, and the corrected text also states the condition the old text left out: the reaper measures elapsed time since the last activity stamp, so a client that polls less often than the idle window is reaped between calls. (#286)

0.9.0 - 2026-09-21

Added
  • New kb_curate tool: which in-force documents are due for review, most overdue first. Aggregates the freshness/freshness_reason signal kb_search and kb_get already expose per-document into one list, so finding what needs a look no longer requires already knowing to filter for validity_state:stale. Advisory and read-only, with no write-pipeline dependency; registered in read-only replicas alongside kb_search/ kb_get. Candidates are exactly the kb_search(in_force=true) set, and an empty or nothing-review-due result is a valid empty list, never an error. The tool name is reserved on issue #31 (avoiding a collision with the existing kb_audit event-log tool), but #31's request -- pattern promotion: detecting repeated patterns and proposing to hoist them up the tier chain -- is not implemented here and #31 stays open. (#142)

  • Review-due can now be derived from how long a document has actually gone unverified, not only from a hand-set deadline. last_verified used to be advisory and evaluated by nothing, so a document nobody ever re-verified looked exactly as fresh as one checked yesterday. With the new KB_REVIEW_DUE_AFTER_DAYS setting, a document with no explicit recheck_by is reported freshness: "stale" once its last_verified is older than that many days, or has no last_verified at all -- a document never verified does not default to fresh, since that would hide exactly the documents most worth looking at. An explicit recheck_by remains an override in either direction: past, it is stale regardless of verification age; future, it suppresses the automatic check entirely. A new freshness_reason field on kb_search hits and kb_get names the field and the date or day count behind a non-empty freshness. Unset (the default): behaviour is unchanged from before this release. This is advisory only and never affects in_force or default search. (#142)

  • A proposal can now declare that it contests existing indexed knowledge (#241). kb_propose_edit accepts an optional contest mapping carrying contradicts document IDs and an optional dispute reason, so a resolved contradiction becomes distinguishable from an ordinary edit. A valid contest always parks for confirmation, even at high confidence with auto-commit otherwise permitted: disputing in-force knowledge is a human decision, never an automatic one. Validation runs in two stages: structural bounds and a credential scan before the rate limiter, then contradiction IDs are resolved against the index after it, rejecting a target that contradicts itself and never echoing a submitted ID or a resolved path back to the caller. Duplicate contradicts items are rejected with rejected_invalid_contest. PendingQueue persists intent: "contest" and contradicts in the path lock, which the claim and restore transitions preserve, and kb_list_pending projects the proposal's operational reason and its dispute rationale as separate fields. Index.id_to_path_map now propagates database operational errors (sqlite3.Error) instead of returning an empty map, so contest validation and governed-target classification can tell a failed index read from an empty index and fail closed on it; the duplicate-id checks in write_gate keep their deliberate fail-open behaviour. Read-side projection in held_locks() is deliberately left to a later slice; it is not blocked, since #270's health allow-list already covers these fields by construction. Contributed by @RemanenetSpy in #278.

Fixed
  • A path lock is now published atomically, and a failure after a proposal is published no longer strands it unlocked. The lock file used to be created empty and then written into, so a reader (health, GC, the #241 contest projection) could observe an existing-but-empty or truncated lock for the whole write. It is now written complete to a private temp file and published in a single os.link, which still fails if the path is already locked, so exclusive-create semantics are unchanged. Separately, atomic_write_json publishes a pending entry before its trailing parent-directory fsync; a failure in that fsync used to release the entry's lock unconditionally even though the entry was already live on disk, letting a second proposal take the same path. The lock is now released only when the entry never actually landed. A crash-leftover publish temp file is reaped by the existing periodic GC pass, past the auto-commit lock TTL; see docs/operations.md §4.6. (#272)

Security
  • Health no longer hands pending ids to unauthenticated callers. When auth is configured, GET /api/v1/health, the degraded 503 body every read route can return, and MCP kb_health now omit each path_locks record's pending_id for a missing or invalid token; a valid token still sees it. Behaviour is unchanged when auth is not configured. Responses served with auth configured now carry Cache-Control: private and Vary: Authorization. (#270)

  • Caller-supplied text that cannot be encoded as UTF-8 no longer suppresses an audit event, and no longer breaks reads for everyone else. An unpaired surrogate passed every write-path gate: json.loads accepts the escape, the secret scanner is a regex match, and the entry is persisted with ensure_ascii=True. Two consequences, both fixed. Identity and routing fields (agent_identity, source_session, target_path, pending_id, edited_text) are now REJECTED with rejected_invalid_encoding before any side effect: an unencodable one used to raise inside the suppressed block in _emit_audit, so the write proceeded and no audit record was written for it. Advisory text (reason) is replaced with a placeholder, matching the existing secret-scan treatment of that field, and evidence items are rejected, matching theirs. Separately, the pending listing, the pending detail response and the health path_locks projection now degrade an unrenderable value to a placeholder instead of failing the whole response, which covers records written before this release. Encodability is tested rather than the surrogate code-point range, so correctly-paired astral characters are unaffected. A resolve refused this way returns HTTP 400; the resolve route's status map used to fall through to 200 for any status it did not name, so the refusal would have read as success. (#280)

Changed
  • Healthy REST reads no longer enumerate or read every path lock. The degraded-precheck every read route runs first listed the lock directory and opened/parsed each *.lock file even though it only needed degraded, so cost scaled with the pending queue on the overwhelming-common healthy path. get, search, outline, list, /readyz and /metrics now skip the enumeration entirely when the index is healthy; a degraded response still carries the full path_locks list in its 503 body. (#271)

  • The benchmark receipt is now checked for a reachable source_commit before merge. A receipt refreshed on a pull request branch named a commit that the squash merge then removed from main, so the benchmark-docs guard passed on the pull request and failed afterwards, and a reproduction could not check out the revision the numbers were measured at. CI now fails the pull request that introduces such a receipt, with a message explaining how to re-measure. benchmarks/README.md documents the resulting workflow. (#268)

  • Writing rules are enforced in CI. scripts/prose_lint.py runs over the repository's Markdown on every pull request and fails on an em-dash, a spaced en-dash used as a sentence dash, agent authorship credit, and a short list of LLM diction. It is a vendored copy of the operator's canonical linter; changes belong upstream first, and the header records the upstream commit and sha256 so the copy is verifiable. .prose-lintignore lists what is out of scope, notably the benchmark material whose CURRENT bytes a measurement receipt hashes: the generated result documents in _OUTPUT_PATHS and the measured corpus under benchmarks/corpus/ and benchmarks/governance/. The rest of benchmarks/ is linted, README included: its Python sources are verified against the bytes committed at the receipt's own source_commit, so editing a comment today does not invalidate a past measurement.

  • Documentation prose backfilled to match. 118 em-dashes across README, SPEC, WHY, CHANGELOG, docs/ and deploy/ are replaced with a colon, a comma or two sentences, chosen per sentence rather than mechanically. Prose only: no behaviour, no interface, no documented enum or value changed, and the SPEC edits are punctuation inside existing paragraphs. (No issue; raised by the operator after banned patterns reached public output.)

0.8.2 - 2026-09-15

Security
  • Hardened MCP argument-validation errors and diagnostic logging. Argument-validation failures now return concise rule summaries without submitted values. Trusted parameter names are retained; untrusted names use placeholders. Unknown tools return Unknown tool. This applies to direct calls and the search-mode call_tool proxy. Other errors raised while a tool runs are returned as before, except that pydantic validation errors raised inside a tool are summarized.

    Log records emitted by the MCP SDK, FastMCP (excluding its client modules) and sse_starlette are rewritten before they are written. Message arguments become <redacted>, except the request type in the SDK's request trace and the tool name in the invalid-arguments warning. A message that arrives already formatted is replaced by a marker naming its source file and line, and tracebacks keep frames and exception types, up to a nesting limit, but not exception messages. Uvicorn access lines keep the method, path and status and drop the query string. This application's own log records are unchanged. A client that parsed pydantic's raw validation text will see the shorter message instead.

Fixed
  • A malformed lock file no longer takes down health, readiness and REST reads. A lock whose content was valid JSON but not an object made the lock listing raise, which failed /api/v1/health, /readyz, /metrics, MCP kb_health and the REST outline, search, get and list routes, since they all build health first. It is now listed as owner_kind: "unreadable", and the ownership and cleanup helpers treat it as a lock whose owner cannot be established and leave it in place. (#269)

0.8.1 - 2026-09-14

Fixed
  • kb_list_pending no longer fails when one claimed entry is damaged. A claimed record whose reconcile value is not an object (for example a string left by a hand edit) raised an error that hid every other entry in the listing. It is now listed as claimed, which is how the running-contest check already reads it. (#265)

  • A proposal with a malformed base marker is refused when it is made. kb_propose_edit accepted any string as target_file_hash or base_blob_sha. A git blob id in the sha256 field parked a proposal that could never be approved: every approval came back rejected_stale_base although the file had not changed. Both markers are now checked for their exact format and a bad one is refused as rejected_invalid_base, naming the field without echoing it. docs/serving.md describes how to recover a proposal parked before this release. (#263)

  • An approval's claim record no longer keeps the edited text. Approving with edited_text wrote the full text into the claim on the state volume, where nothing reads it back. After an interrupted approval whose outcome could not be proven, it stayed there indefinitely, including text committed under the explicit secret-scan override. The claim now records only a sha256 digest, and a claim written by 0.8.0 has its text replaced by the digest on the first reconciliation pass after it is older than KB_PENDING_CLAIM_TTL_SEC. Recovery behaves exactly as before. (#264)

  • A supersession that names a missing document is refused and fails lint. A supersedes value naming an id that does not exist retires nothing, so a document it was meant to retire could stay in force, and a superseded_by naming a missing id records a successor that does not exist. Both passed the write gate with only a lint warning. A write that introduces such a target is now refused as rejected_invalid_document (unresolved_supersedes_target or unresolved_superseded_by_target in the reason; a wrong shape is malformed_supersedes / malformed_superseded_by), checked against the commit being made, so a document created in the same bootstrap bundle resolves. Targets already present in a document, and unchanged malformed values, keep passing. data-olympus lint and data-olympus import report an unresolved target as an error (format spec 0.4); --resolve-root <dir> resolves against a larger bundle and --unresolved-targets warn restores the warning while a backlog is fixed. Bootstrap commit-time rejections now carry a reason, and among a bundle's commit-time checks a credential-shaped file is reported before a duplicate id or a content defect. A blank or whitespace-only target is refused as malformed. Validating a write also reads the committed tree with three git processes instead of one per document. (#259)

Upgrading
  • data-olympus lint and data-olympus import can now exit 1 for a bundle that linted clean before: an unresolved supersedes or superseded_by target is an error (format spec 0.4). Run data-olympus lint --unresolved-targets warn to list them, fix each, then drop the flag. Lint a subtree with --resolve-root <bundle root>.

  • Among a bootstrap bundle's commit-time checks, a credential-shaped file is now reported (rejected_secret_detected) before a duplicate id or a content defect in the same bundle. Structural path checks still run first.

0.8.0 - 2026-09-14

Added
  • A session can read back its own parked proposal. A write that parks returned a pending id, and the listing showed its target path, confidence and park reason, but nothing returned the text. So an agent could learn THAT it proposed something about a path and never WHAT it proposed: it could not re-read its own draft, quote it back to the operator, or check it against a second proposal. GET /api/v1/pending/<id> and the kb_get_pending MCP tool now return the postimage, with in_force: false and a note saying the content governs nothing until approved. It stays out of kb_consult and every in_force retrieval exactly as before.

    Ownership is the AUTHENTICATED principal recorded when the proposal was made, or any principal holding resolve; an entry parked before this release is resolver-only, since its owner cannot be established. A postimage the secret scanner flagged is withheld from everyone but a resolver, since handing one back would turn the queue into a place to retrieve a credential from. Only the postimage is owner-scoped: the pending LISTING remains visible to every authenticated principal with each entry's target path, reason and evidence, unchanged by this release and documented in docs/serving.md.

    Because a principal name is now an ownership identity, the server refuses to start on a configuration with two credentials sharing a name, including two unnamed ones, which both defaulted to agent. Give each credential a unique name before upgrading, and avoid operator when KB_AUTH_TOKEN supplies that identity. Requested in #256 after a public discussion of read-your-own-writes.

  • The governed action vocabulary is configurable. The enforcement gate classified actions from three lists compiled into enforce_policy.py, and the shipped entry point never threaded a classifier, so extending coverage meant embedding the server in your own Python. KB_GOVERNED_EXTRA_KEYWORDS, KB_GOVERNED_EXTRA_PATH_GLOBS and KB_GOVERNED_EXTRA_COMMAND_PATTERNS now add to those lists. They extend and never replace, so a configuration error cannot silently remove enforcement the product already provided, and this changes only what counts as governed: a fresh explicit consultation is still the only thing that clears the gate. Part of #257; the SubagentStart half of that issue remains open and needs its payload verified first.

  • A reproduction of the benchmarks can now be compared against the published one. python -m benchmarks.receipt compare --reference <path> --candidate <path> reports two levels that fail independently: whether the recorded inputs match (commit, environment, commands, seeds, configuration) and whether the recorded results match (artifact digests and the reported summary). A run can match the first and not the second, which is the discrepancy worth reporting, and it can match the second and not the first. reproduction_status is therefore the outcome of a comparison against a named reference, never a property a single receipt carries. A receipt missing a field the comparison needs is rejected rather than counted as a match, and the CI guard now fails if the committed receipt stops being usable as a reference. Exits 0 when both levels match, 1 on a difference, 2 on a missing field. benchmarks/README.md documents how a third party publishes a reproduction and reports a discrepancy. The comparison is over RECORDED inputs and results: this tool does not execute or observe a run, and the published tables stay labelled maintainer-produced and not independently reproduced.

  • Release channels are documented for users. docs/operations.md section 7.1 states the stable and candidate names on every surface (GHCR immutable tags and the rc, stable and latest channels, PyPI versions, GitHub releases), how to try a candidate by its exact version, how to check it with data-olympus verify and what each exit code means, what promotion guarantees (the stable image is the candidate digest, and the stable wheel is compared with the candidate), and how to roll back. The README and quickstart candidate examples no longer pin an old version. The maintainer procedure is unchanged and now section 7.2. Part of #151.

  • data-olympus can be listed in the official MCP registry. The registry is where modelcontextprotocol/servers now sends new server implementations, and data-olympus was absent from it: two searches of its v0 API returned zero results. A server.json at the repository root declares io.github.knaisoma/data-olympus against the PyPI package, with the streamable-http transport this server actually serves rather than stdio, and README.md carries the mcp-name ownership marker the registry looks for in the description PyPI serves. 0.8.0 is the first stable release whose description carries that marker, which is what unblocks verification.

    This release declares the entry; it does not publish it. Publication authenticates as a maintainer through GitHub OAuth or from Actions through OIDC, which is a deliberate step rather than a file change. docs/mcp-registry.md records what it needs. Part of #111.

  • Groundwork for telling a contested decision apart from an ordinary edit. PendingQueue.derive_running_contest(target_path) reports, at read time and without writing anything, whether a path is held by a proposal and whether that proposal declares itself a contest: intent: "contest", dispute: true, or a contradicts reference in its metadata. supersedes is ordinary succession and deliberately does not count. The receipt carries a state spelled as kb_list_pending spells it, so a claimed or uncertain entry is distinguishable from one awaiting a decision, and an entry file that cannot be opened is reported as unreadable rather than raising. An entry file that is not valid JSON is still treated as absent, unlike the listing, and under_review=False does not by itself mean a path is free. No tool or endpoint calls it yet, and nothing writes the contest fields today: this is the read-side half of #241. Contributed by @RemanenetSpy in #261.

Changed
  • OKF v0.2: data-olympus writes generated instead of timestamp. OKF v0.2 supersedes timestamp with generated: { by, at }. data-olympus init, the importers and server-rendered memories now write generated, and init declares okf_version: "0.2" and format spec_version: "0.3". When data-olympus synthesizes this record, its writer is always the tool actor data-olympus/<version>, never a principal name or agent identity, so that metadata never attributes agent-written content to a human author; memories keep created_by and created_at. A proposed edit still carries whatever frontmatter its author supplies. The OKF importer keeps a source's generated and legacy timestamp as written, and flags a malformed generated for review. When a source uses two aliases for one field, the canonical key wins, then identifier over uid, kind over doctype and updated over date, and the dropped value is reported; previously the later key in the source won, so the imported id, type or date could depend on key order. Lint accepts a generated.at or a legacy timestamp as the content-change time and warns on a structurally malformed generated. Existing bundles are not rewritten; a document relying on timestamp sees no new finding, but one that already carries a malformed generated (for example generated: null) now gets a warning. Compatibility: an OKF v0.1-only consumer that requires timestamp will not accept newly written documents. Part of #173.

Fixed
  • Interoperability evidence is pinned to where OKF now lives. OKF moved to GoogleCloudPlatform/open-knowledge-format, and the okf/ directory the pin pointed at in knowledge-catalog became a frozen snapshot. The pin, the conformance script, CI and the freshness workflow now use the dedicated repository at OKF v0.2 commit ad30107c; the vendored sample has nine concepts; the frozen copy is rejected as a pin. The freshness workflow monitors the fixture only, and says so. Closes #173.

  • A dropped connection mid-approval is now visible, and recovers on its own whenever the evidence allows. Approving a pending write claims the entry, holds its path lock, commits, then releases both. If the process died in between, the entry was left in a claimed state that nothing could reach: it was excluded from kb_list_pending, the orphan-lock GC treated its sidecar as a legitimate lock holder, and pending-owned locks were never TTL-reclaimed. The path stayed refused with rejected_path_lock_busy across restarts, while the queue read as empty, which is indistinguishable from "the decision was applied".

    Entries now carry an explicit state (pending, claimed or uncertain) and claimed ones are listed rather than hidden, health reports path_locks with each lock's target path and age instead of only a count, and a background pass reconciles claims older than KB_PENDING_CLAIM_TTL_SEC (default 900). Reconciliation runs on evidence, not on age: the write records its own outcome durably before anything else, and failing that the commit is looked up by its new KB-Pending-Id trailer on the recorded session ref. Only a recorded failure restores an entry to pending. Anything unproven stays uncertain with its lock held and is re-checked on every later pass, because a rebase or a squash can remove a commit that really was made, and restoring on its absence would offer an already-applied decision for approval a second time. An entry that stays uncertain keeps its lock deliberately, and docs/serving.md documents the manual recovery for it. The named history-rewriting paths (the base rebase, the push recovery that calls it, and the worktree GC) reconcile first and defer while a claim on that branch has begun its write.

    Because the claim now stores the approved content, an operator edit (edited_text) is scanned for credential-shaped values before the entry is claimed. A flagged edit is refused as rejected_secret_detected and the pending entry is left exactly as it was, so the refused text is never written to the state volume. An entry restored after any other rejection returns to the proposal as enqueued, without the edit. The explicit operator override is unchanged.

  • Indexing a modest corpus no longer needs more than a gigabyte of memory. The co-occurrence pass that powers query expansion counted every token pair in the corpus in one in-memory table. The per-document token cap bounded each document's contribution but nothing bounded the union across documents, and the union is what was held: on a 536-file, 3.6 MiB corpus that was 6.2 million distinct pairs and a 1445 MiB peak, above a 1 GiB container limit on its own, so every index rebuild OOM-killed the pod. Pair counting now drops terms that cannot appear in any surviving pair, keys pairs by integer rather than by string tuple, bounds the per-term candidate lists, and spills to a temporary SQLite database once it reaches KB_COOCCURRENCE_MAX_PAIRS distinct pairs (default 500000). The same corpus now peaks at 105 MiB, measured for the co-occurrence build itself. For identical inputs and configuration, a successful build produces a byte-for-byte identical related-terms table, so no search result moves; a build that degrades on a spill failure has no table at all until the next successful one. The trade is build time: that corpus goes from 6.6 s to 16.3 s, measured on one machine. Set KB_COOCCURRENCE_MAX_PAIRS=0 to disable the spill and keep the previous speed where memory allows. See docs/serving.md.

  • A failed co-occurrence build no longer fails the whole index build. The spill writes to a temporary file, so it can fail on a full or read-only disk. Because the index only rebuilds when the corpus changes and /readyz rejects a failed build, that would have left the service unready until the next commit. The build now logs a warning and continues without a related-terms table: the index still serves, and only query expansion is lost until the next successful build.

0.7.3 - 2026-09-07

Fixed
  • A blank KB_MAIN_PATH no longer makes the server index its own working directory. KB_MAIN_PATH= and KB_INDEX_PATH= -- what an unsubstituted compose, Helm or CI variable produces -- reached Path(""), which is Path("."), so the server silently indexed whatever happened to be in its working directory. A blank or whitespace-only value now means unset and the documented default applies, while a non-blank value is used verbatim. And when the corpus path does not resolve to a directory, startup now fails with a message naming the setting, the path it resolved to, and whether that path came from the environment or the built-in default, instead of KB root not a directory: /kb-main -- a path the operator never configured and no setting name to search for. An existing but empty corpus keeps starting normally, as before. A deployment that relied on blank-means-cwd changes behaviour; that reliance was on the silent defect being fixed here. (#244)

  • The Kubernetes first-boot log no longer prints the configured git remote. The StatefulSet initContainer echoed the remote URL immediately before cloning it. The remote is documented as "SSH or HTTPS", and an HTTPS remote may carry a credential inside the URL, so an adopter using one wrote that credential into the pod log on every first boot, where no application-level redaction reaches it. The message now names no URL, matching the Docker entrypoint. Nothing else about the clone changes. (#248)

0.7.2 - 2026-09-07

Changed
  • The README links the Lulu MCP marketplace listing. Lulu aggregates several registries including Glama, which this README already badges, so the listing may well be derived from that one rather than being an independent signal. It is one more place an agent or a person can find the server. The badge is a static label, so treat it as a pointer rather than as live listing status.

  • The release contract now says what to do when main advances past the reviewed revision. The delivery rule assumed the release merge was still the tip of main. That stops being true when anything else merges between review and publication without itself carrying a qualifying release review and human merge: the reviewed revision is no longer main, main's tree has not been reviewed as a unit, and no revision satisfies the rule as written. The contract now names the preferred resolution (make the reviewed revision the tip again) and defines a narrow fallback with explicit conditions, including that every intervening commit was independently reviewed and human-merged, that none of them changes an input to a published artifact, that a change to a publication workflow always blocks the fallback, and that provenance and the OCI org.opencontainers.image.revision label name the exact source rather than main.

  • Python 3.14 now has test coverage. requires-python has always been >= 3.13, so 3.14 was inside the supported range and users could install there, but nothing verified it. CI now runs mypy and the pytest suite on 3.14 alongside 3.13. Installed-artifact smoke tests still run on 3.13 only, so this is interpreter coverage of the test suite rather than a full parallel validation, and the shipped container image is unchanged and still runs 3.13.

  • The release contract now separates candidate remediation from post-merge alert closure. Publication still requires zero open security alerts, and that requirement is never waived. The contract now states the one narrow case in which a final release pull request may reach its human merger while an alert is still open: the alert's fix is in the exact reviewed candidate and its closure is mechanically waiting only for that fix to reach the default branch. It requires per-alert remediation proof against that exact candidate, agreement from both the implementer and the independent reviewer, and fresh zero-alert clearance after the merge before anything is published. Previously the rule read as an unqualified zero-alert gate, which no branch fixing a default-branch alert could ever satisfy before merging.

Fixed
  • Product source changes no longer fail the benchmark guard. The receipt's source_tree digest was compared against the working tree, so any edit under src/data_olympus/ or benchmarks/ failed benchmark-docs with source_tree sha256 does not match the repository. That blocked every product fix, which is the same defect already corrected for dependency_lock and the same wrong assertion: that today's source produced numbers measured before it existed. The digest is now recomputed from the source as committed at the receipt's own source_commit, read from git history. Integrity of the historical record is stronger, not weaker: the recorded file list is compared as well as the digest, so a file that existed at the measurement commit but was dropped from the receipt is now caught, which the previous per-file walk could not see because it iterated the very list being shortened. What the guard deliberately stops asserting is any correspondence between today's source and the published numbers. It never established that the benchmarks actually ran at the declared commit, and it does not now.

  • Bootstrap /kb-main on the docker compose path. The first-boot clone in deploy/docker/entrypoint.sh keyed off KB_GIT_REMOTE_URL, but deploy/docker/compose.yaml carries only KB_REMOTE_URL, which is also the variable docs/adoption.md and docs/quickstart.md document. A compose deployment that configured a remote therefore started a read-write server on an empty /kb-main, and the first symptom was git_pull_loop iteration failed: … rev-parse HEAD … exit status 128, which does not point at the cause. The clone now falls back to KB_REMOTE_URL; KB_GIT_REMOTE_URL still takes precedence, so the k8s path where the initContainer has already cloned is unchanged.

  • Bind the benchmark receipt to its measurement commit instead of the working tree. The receipt's dependency_lock was compared against the current uv.lock, so every dependency change failed the benchmark-docs CI guard with dependency_lock does not match uv.lock, including security updates. The comparison now runs against uv.lock as committed at the receipt's own source_commit, read from git history and still checked byte for byte, while the current lock is free to move. A past measurement is not invalidated by a later dependency bump, and the guard no longer asserts that today's dependency set produced numbers measured before it existed. Every other receipt failure is preserved unchanged.

  • Correct the receipt's recorded lock binding. dependency_lock.sha256 had been regenerated in place against a newer uv.lock while source_commit was left pointing at the original measurement commit, so the receipt described a pairing that never existed. It is restored to the lock actually committed at that commit; the measurement revision and all benchmark outputs are unchanged.

  • fix(release): require human merge and preserve published artifacts (#240)

  • fix(operations): report why a command failed, not just which one (#230)

  • fix(operations): make the release no_action deterministic (#226)

Security
  • All locked dependencies updated to their latest resolved versions. 71 packages move, including mcp 1.28.1 to 1.29.1, fastmcp 3.4.4 to 3.4.7, starlette 1.3.1 to 1.6.0 and regex 2026.7.10 to 2026.9.3. fastmcp 3.4.7 carries an upstream CIMD assertion-audience security fix. This is the first full dependency refresh possible since the benchmark guard defect described below was blocking every lockfile change. The three major-component moves (sentence-transformers 5.6.0 to 6.0.1, setuptools 83 to 84, websockets 16.0 to 17.1) do not affect a default install: sentence-transformers belongs to the optional bench extra, which the runtime never imports.

  • Update cryptography from 49.0.0 to 50.0.1 (GHSA-g6cj-pr64-35w5). PKCS#7 EnvelopedData decryption reported failures in distinguishable ways, one of which disclosed the length recovered from the RSA operation, and the same distinction was observable by timing. An application decrypting attacker-supplied EnvelopedData and reflecting the outcome therefore exposed a Bleichenbacher oracle against the content-encryption key. Affected from 44.0.0, fixed in 50.0.0. The GitHub Advisory Database rates it high; the upstream advisory rates it moderate.

    Data Olympus does not depend on cryptography directly, and no Data Olympus source file imports it or calls the affected PKCS#7 entry points. This release does not establish whether the affected code is reachable through a transitive dependency, so the remediation is scoped to the dependency version rather than to a claim about exploitability. The locked environment and the container image install exactly 50.0.1. A clean installation of the wheel resolved 50.0.1 when tested, but the metadata does not guarantee it: this distribution declares no minimum for cryptography, and the combined transitive requirements (authlib >= 45.0.1, joserfc >= 45.0.1, and pyjwt >= 3.4.0 for its crypto extra) still permit 49.0.0. An existing environment is not upgraded automatically. See docs/releases/v0.7.2.md for the explicit upgrade command.

  • The first-boot clone no longer logs the remote URL. KB_REMOTE_URL is documented as an SSH or HTTPS push target and the compose path mounts no SSH key by default, so a writable remote there is most likely an HTTPS URL with an embedded token. The entrypoint printed that URL verbatim before cloning, into a container log no application-level redaction can reach. It now reports only that it is cloning the configured remote. That removes the unconditional disclosure rather than every path to one: git inherits the same stdout and stderr, and a token embedded in a clone URL is still written to /kb-main/.git/config once the clone succeeds. Provisioning the credential outside the URL is the way out of both.

  • Scan for LLM provider API keys. The built-in secret-scan set covered private keys, GitHub, AWS, Slack, password= assignments and connection strings, but no LLM provider key format, so an sk-ant-… or sk-proj-… key in a proposed memory was committed clean. Users of this project hold these credentials by definition, which makes them the class most likely to be pasted into a memory. Adds llm_provider_api_key (Anthropic, OpenAI project/service-account/legacy, OpenRouter, Hugging Face, Groq, xAI, and OpenAI-compatible gateways) and google_api_key (AIza…, which authenticates Gemini and the other Google APIs that accept API keys).

  • Detect two further forms of existing credential classes. github_token now covers ghu_ user-to-server tokens alongside ghp_/gho_/ghs_/ghr_, and aws_access_key_id now covers the ASIA temporary key id issued by STS as well as AKIA.

  • Secret-scan boundaries no longer depend on word characters. The new key classes above anchored with \b, which cannot fire between _ and a letter because both are word characters. A key wrapped in Markdown emphasis (_KEY_) was therefore invisible to the scanner, for every provider, and the same shape occurs in YAML flow keys and dunder names. The boundaries now exclude alphanumerics instead, which still rejects an immediately preceding ASCII letter or digit while letting a delimiter sit against the key. That is narrower than refusing every longer token: text joined to a key by - or _ can still match, which is the direction a credential scanner should err in. The xAI alphabet also gained _, without which a key containing one was missed completely. The gate remains shape-based: it can still miss a credential whose format it does not model, and it does not scan or revoke anything already in history.

0.7.1 - 2026-08-02

Fixed
  • fix(release): bind no action review to remote main

Fixed
  • Bound no action review to current remote main. Release admission now preserves the freshly fetched origin/main revision in the compact review evidence, so an independent reviewer can verify that the admitted source and current remote source are identical before accepting that no release is due.

0.7.0 - 2026-08-02

New features
  • feat(automation): execute weekly release outcome

  • feat(automation): make weekly releases outcome based

Fixed
  • fix(release): complete protected workflow delivery

  • fix(release): serialize workflow number input

  • fix(release): retry unbound gateway reads (#217)

  • fix(release): ground autonomous review packets (#215)

  • fix(release): preserve prepared roll forward (#213)

  • fix(automation): preserve recovery result binding (#212)

  • fix(release): resume unpublished preparation

  • fix(release): retry bound tag inventory read (#208)

  • fix(release): isolate prereview model evidence (#206)

  • fix(release): inspect exact GHCR version tag (#203)

  • fix(release): isolate development quality tools

  • fix(release): retry transient artifact smoke (#198)

  • fix(release): remove unavailable quality gate (#196)

  • fix(release): require approval before merge (#195)

  • fix(release): wait for complete check matrix (#193)

  • fix(release): wait for complete check conclusions (#191)

  • fix(release): allow metadata-only lock version bump (#189)

  • fix(automation): bind release consultation to run

  • fix(automation): accept FastMCP text content

  • fix(automation): read pull number from gateway receipt

  • fix(automation): make release imports deterministic (#182)

  • fix(automation): align final main checks

  • fix(security): update setuptools to 83.0.0

  • fix(ci): allow immutable history reconciliation

Fixed
  • Separated deterministic release controls from model review. The release review packet now carries an explicit compact response contract and omits the already validated native prereview decision, which is not a model judged release gate. A failed model subprocess is now reported as an invocation failure instead of being misclassified as invalid JSON.

  • Made the GHCR immutability guard exact and credential independent. The release gate now inspects the public vX.Y.Z registry tag directly instead of requiring GitHub Packages API scope or scanning a bounded version list. Only an explicit missing manifest proves the version is free; every other client, timeout, or registry failure still blocks the bounded, noninteractive check without exposing command diagnostics.

  • Isolated autonomous release quality tools. The release runtime now installs the declared dev extra when it runs Ruff, mypy, and pytest. This prevents a missing project executable from falling through to an unrelated global Python environment and validating the wrong installed Data Olympus version.

  • Moved autonomous release approval before merge. The release routine now reviews and approves the exact unmerged candidate, rederives every control replaced by the deliberate GitHub ruleset bypass, conditionally merges the expected head, and proves exact parent and tree transfer before any publication or deployment. The routine preserves exact CodeQL, alert, review, ruleset, and tree transfer evidence without depending on the paid GitHub Code Quality product, which is unavailable on the approved Free plan.

  • Accepted the canonical FastMCP pull request receipt. The automated release routine now derives the new pull request number from the exact repository URL when the gateway receipt omits a separate numeric field.

  • Accepted the FastMCP standard text content envelope. Release automation now reads the gateway's legacy tool bound text result when optional structured content is absent, while rejecting unbound or mismatched output.

  • Made release commits satisfy the governance gate autonomously. Before changing release files, the routine now records a run scoped Data Olympus MCP consultation, validates receipt freshness, and requires a server side gate receipt bound to the exact run and workspace. The unchanged precommit audit gate still verifies the staged governed change.

Fixed
  • Resume a merged but still unpublished release preparation only when its version, empty Unreleased block, release note, canonical changelog prefix, source SHA, and tree exactly match the current computation. The current main SHA receives fresh independent review without inventing a branch, pull request, merge, or parent. Candidate spelling is then confirmed absent from PyPI, public GHCR manifest inspection, and GitHub releases before the ordinary publication, deployment, verification, and notification tail runs.

  • Retry one transient unbound FastMCP response during the idempotent postmerge tag inventory, while preserving strict tool binding and exact merge evidence when the bounded retry still fails.

Fixed
  • Give the autonomous release reviewer the exact bounded, hash bound candidate diff, changelog section, and release note, and preserve its bounded reason when it blocks a release instead of reducing the diagnosis to a generic verdict.

  • Keep the operations runner candidate revision null when resuming a release that is already prepared on main, while retaining the exact admitted source revision throughout review, approval, delivery, and verification evidence.

  • Select exact prepared recovery only when the current release documents match the complete computed change set. A new reviewed change now reaches the existing atomic roll forward pull request path instead of blocking recovery.

Fixed
  • Resolve release image digests from the exact public GHCR tag without requiring GitHub Packages API scope.

  • Advance only the exact reviewed release workflow through its single pending pypi environment approval, preserving the protected environment gate while removing a manual scheduled-run follow-up.

0.6.0 - 2026-07-18

Added
  • Added searchable MCP discovery. tools/list now exposes seven core tools plus tool_search and call_tool by default. Operators can set KB_TOOL_DISCOVERY_MODE=all for the complete native catalog. Hidden tools remain directly callable and retain the same authorization checks.

  • Added executable OKF interoperability checks. CI verifies both Data Olympus consuming the pinned Google Knowledge Orchestration Framework sample and the pinned upstream reference consumer reading the Data Olympus example bundle. The immutable upstream revision and fixture checksums are recorded in the repository.

  • Added complete release candidates across every public channel. One exact source SHA now produces a PyPI prerelease wheel and sdist, GHCR image, GitHub prerelease, and release-provenance.json. Stable promotion is an explicit human approved dispatch that accepts only the highest complete candidate.

  • Added deterministic benchmark provenance receipts. The committed receipt records source, corpus and query hashes, dependency lock, environment, tokenizer and model identities, exact commands, seeds, result hashes, and metric summaries. The documentation drift guard verifies it before checking generated benchmark tables.

Security
  • Bumped transitive dependency mcp 1.28.0 to 1.28.1 to close CVE-2026-59950 / GHSA-vj7q-gjh5-988w (Dependabot #18, high severity). The advisory covers the MCP Python SDK WebSocket server transport, which data-olympus does not use (Streamable HTTP only); the bump is applied as a precaution and to clear the release security gate. No API surface change. (KNA-146)

  • Bumped transitive benchmark dependency torch 2.12.1 to 2.13.0 to close CVE-2025-3000 / GHSA-rrmf-rvhw-rf47 (Dependabot #19, low severity). The affected torch.jit.script path is available only through the optional bench extra and is not used by the production embedding implementation. The patched dependency remains compatible with sentence-transformers.

Changed
  • Made PyPI the primary installation and onboarding path. The README and quickstart now lead with uvx and uv tool install. Every packaging workflow runs clean wheel and source distribution smoke tests at the expected version. They exercise both console entry points, packaged enforcement files, REST health, setup doctor, compact discovery, hidden direct calls, and complete catalog mode.

  • Hardened candidate to stable provenance. Candidate and stable Python artifacts are rebuilt from the same exact source, compared with only version metadata normalized, and published through Trusted Publishing with no stored PyPI credential. Stable OCI tags reuse the verified candidate digest and never rebuild the image.

  • Updated the supported runtime and development dependency baseline. FastMCP is now locked to 3.4.4 and requires >=3.4.4,<4, guaranteeing the catalog search transform used by the default MCP tool-discovery mode. Regex is updated to 2026.7.10 with the secret-scanning timeout and false-positive regressions green. Mypy, types-regex, and Ruff were updated to their current Dependabot revisions and pass the strict type and lint gates. (gh #159, #160, #161, #162, #163)

  • Added Glama related server metadata and maintenance notes. glama.json now names adjacent knowledge and memory MCP servers, and docs/glama.md records the maintainer response and usage seeding steps for the Glama maintenance profile. (gh #155 / KNA-145)

  • Improved tool definitions for 7 tools (kb_gate_check, kb_onboarding_status, kb_compliance, kb_cleanup_plan, kb_get, kb_list, kb_outline). Each docstring now includes a "Use when / use X instead when Y" guidance sentence. kb_gate_check also discloses its audit-log side effect (readOnlyHint=false is intentional). (gh #154 / KNA-144)

  • Improved kb_record_event tool definition. The docstring now discloses behavior (durable, non-destructive audit append; requires write capability), documents both event_type values (gate_bypass, gate_degraded) and all required parameters, and gives explicit usage guidance on when to call vs. when to use kb_gate_check, kb_consult, kb_audit, or kb_compliance instead. (gh #153 / KNA-143)

Fixed
  • Closed the remaining release verification gaps. Every PyPI publishing path now fails closed and verifies remote file hashes. Candidate provenance validates its source, candidate tag, and image digest before publication. Stable promotion derives its version from the requested complete candidate and verifies that version against the candidate source commit. OKF freshness reports only upstream fixture changes rather than unrelated repository commits.

  • Corrected exact SHA CI readiness for conditional jobs. Completed skipped jobs are now non-blocking only when they are not required. Missing, pending, failed, neutral, or skipped required checks still fail closed. This lets the release gate accept a fully green pull request whose publishing workflow intentionally skips upload and manual release paths.

  • Hardened read tools against present SQLite indexes without tables. kb_search, kb_get, kb_list, kb_outline, onboarding status, and cleanup planning now degrade to empty results or empty health instead of surfacing raw sqlite3.Error failures. The Glama Docker sandbox now serves the bundled example-bundle corpus so catalog introspection starts with a populated, safe index. (gh #156 / KNA-142)

0.5.0 - 2026-07-12

Added
  • Version-immutability release guard (scripts/check_version_free.py). A published version (PyPI, the ghcr :vX.Y.Z image tag, or a GitHub release/tag) is immutable, so the release process now refuses to republish an existing version. The guard runs as the first step of tag-release.yml's decide job (before the tag is pushed, so a duplicate never leaves an orphaned public tag) and in PR CI (to catch a duplicate declared version before merge). A legitimate reconcile (local tag vX.Y.Z already at HEAD) is allowed and skips the registry lookups; a registry outage fails closed. The operator can override a stuck registry with KB_BYPASS_VERSION_CHECK=1. (gh #148 / KNA-67)

  • First-class reverse-proxy Host allowlist (KB_PUBLIC_HOSTNAMES). New config knob mapped onto fastmcp's Host-header allowlist when the HTTP app is built, so operators configure data-olympus directly instead of reaching for the dependency's FASTMCP_HTTP_ALLOWED_HOSTS env var (both are honoured and merged). The server now emits a startup WARN when Host protection is on, it binds a non-loopback address, and no public hostname is allowed: the exact silent-breakage shape (readiness green, every proxied request 421) behind the 2026-07-09 kn-dev outage. A regression test boots the app with a configured public hostname and asserts the good Host reaches the route while a foreign Host is answered 421. (gh #139 / KNA-70)

  • Prometheus /metrics endpoint (optional metrics extra). prometheus-client is an OPTIONAL dependency; with it installed, GET /metrics serves the standard exposition format (pending/push queue depths, frozen count, push-failure counter, staleness, live sessions, per-tool call counters, index-build duration + last-build timestamp). Without the extra the route returns 501 and every metric update inside the core background loops is a silent no-op, so a standard deployment is unaffected. (gh #69 / KNA-71)

  • Cached update-available signal. The server now checks PyPI/GitHub for a newer published data-olympus version in a background task and surfaces latest_version / update_available in kb_health and GET /api/v1/health. The health request path reads the cache only, and compact health includes latest_version only when update_available is true. Air-gapped deployments can set KB_DISABLE_VERSION_CHECK=on to make zero outbound version-check calls. (gh #146 / KNA-68)

  • Legacy corpus status migration. Status-less documents from pre-0.4.0 corpora now default to active in the SQLite index when KB_STATUS_AUTOFILL=on (the default), preserving in-force retrieval without mutating source markdown during index builds. Operators can persist the same default explicitly with the idempotent data-olympus migrate status --apply command. (gh #147 / KNA-69)

Fixed
  • kb_consult demoted_writes CTA count. The maintenance CTA now counts only the session's live pending entries and is omitted once none remain, instead of reporting stale "N write(s) awaiting operator review" after every demoted entry had been resolved (the demoted_writes / pending_actions surface shipped since v0.4.0 via #131). (gh #137 / KNA-72)

0.4.2 - 2026-07-11

Added
  • data-olympus verify command. New CLI subcommand that runs pass/fail checks against a running instance (--target, --json, --checks, --token): liveness (/api/v1/health), readiness (/readyz), a search round-trip (/api/v1/search), and a deployment-tolerant enforcement-plane probe (/api/v1/gate/check). Exit codes: 0 all pass, 4 a check failed, 1 the target is unreachable. Used as the pre/post-release verification gate.

  • Staged-promotion release pipeline (release engineering). A release candidate is published to a X.Y.Z-rc.N ghcr channel (rc-publish.yml), deployed as a canary and verified, then promoted by re-tagging the verified digest to vX.Y.Z byte-identically (tag-release.yml), with a moving-channel helper (set-channel.yml) and project-local runbooks under .rules/.

  • Glama registry score support. Added the Glama score badge to the README and documented the claimed server listing, sandbox release settings, and score-maintenance checklist in docs/glama.md.

  • Release security-clearance gate. scripts/security_alerts.py fails a release when any Dependabot or CodeQL alert is open; the release planner clears every alert (fix or justified dismissal) and the cutter verifies zero open before building the release candidate.

Changed
  • Improved MCP tool schema metadata for registry introspection. MCP tools now expose meaningful titles, MCP annotations, and per-parameter JSON Schema descriptions so registries such as Glama can score the running server's tool definitions more accurately.

Fixed
  • Documented the fastmcp 3.4.3 host allowlist requirement behind proxies/ingress (docs/serving.md, docs/operations.md section 3.4). v0.4.1's fastmcp bump enables DNS-rebinding protection that answers 421 Misdirected Request for undeclared public hostnames while health probes keep passing; deployments behind a reverse proxy must set FASTMCP_HTTP_ALLOWED_HOSTS. First-class handling tracked in #139.

0.4.1 - 2026-07-08

Changed
  • Dependency refresh from the post-0.4.0 Dependabot queue: runtime dependencies fastmcp 3.4.2 -> 3.4.3 and numpy 2.5.0 -> 2.5.1; dev dependency mypy 2.1.0 -> 2.2.0 (strict run clean); CI actions actions/upload-artifact v4 -> v7 and actions/download-artifact v4 -> v8 (exercised together on the packaging dry-run and by this release's own publish chain). Full test suite green on the combined set.

0.4.0 - 2026-07-08

Fixed
  • data-olympus init stamped a stale format version (found by the release-gate review): the scaffold's root index.md declared spec_version: "0.1" while the shipped format is 0.2. Corrected, with a drift test tying the scaffold constant to SPEC.md's header and example-bundle/index.md so future format bumps fail fast.

  • Graph-exclusion source guard now applies the memory-inbox floor (found by the v0.4.0 release-gate review). graph_excluded_ids_sql checked the superseding source's status class and validity window but not is_inbox, so a forged or legacy memory/inbox/ document claiming status: active with supersedes: <real-rule> could retire a real rule from in_force=true retrieval, kb_consult, and the computed in_force boolean. The source guard now requires src.is_inbox = 0, matching the full single-sourced in-force predicate; regression-tested with an inbox-source scenario.

Added
  • Write-path enforcement of mandatory status (issue #114). status has been a required frontmatter field (SPEC.md 4.2) and a kb lint error since the format's 0.1 draft: this was already true of the lint layer and is not a change of required-field semantics. The gap this closes is at the write path: kb_propose_edit now rejects a postimage that creates a NEW document without status (rejected_invalid_document, reason missing_status), where previously the write path was more permissive than kb lint and let a brand-new status-less document through.

    • Staged migration, not a hard break. Editing an EXISTING status-less document still requires no status, so a legacy corpus already carrying status-less docs (tracked by the issue #113 maintenance ledger's status_present_in_all_kb_entries flag and missing_status file list) is never locked out of incremental fixes. Those documents keep being served by kb_search/kb_get exactly as before; they are simply never in force (already true via IN_FORCE_STATUSES membership) and never surfaced by kb_consult.

    • kb_propose_memory is unaffected: every server-rendered memory already stamps status: proposed (issue #109).

    • Reserved filenames (index.md/log.md/template.md) and documents under the memory-inbox prefix are exempt, consistent with their existing exemptions from the write-path's other schema checks.

    • No SPEC.md version bump: see SPEC.md section 10 for why this is a reference-implementation enforcement-timing change, not a format change.

    • See docs/operations.md section 5.1 for the operator migration runbook (lint, read the ledger's missing-status list, fix, done) and the new integration test (tests/test_status_mandatory_migration.py) tying lint-error + still-served + never-in-force + ledger flag + the pending_actions CTA together as one tested contract.

  • Governed-lane write protection (issue #112) -- BREAKING DEFAULT-BEHAVIOR CHANGE. "Agents can propose, only humans can promote." Behind KB_GOVERNED_LANE_PROTECTION (default ON; set to off to restore the exact pre-#112 behavior). No write is ever rejected by this feature -- affected writes land as a PENDING entry instead of auto-committing:

    • Status clamp. Any non-operator-confirmed write (the auto-commit path of kb_propose_memory / kb_propose_edit, and kb_bootstrap_project files) whose postimage sets or changes status INTO the in-force class (active/accepted/approved, single-sourced from format.validate.IN_FORCE_STATUSES) is demoted to pending with demotion_reason: "status_promotion". An operator-confirmed resolve of that entry commits it -- the human step IS the promotion.

    • Governed-target edit demotion. Any kb_propose_edit whose target document is CURRENTLY in force (the full composed predicate: status class AND validity window AND not-inbox AND not-graph-excluded, evaluated against the live index) is demoted with demotion_reason: "governed_target", regardless of confidence. An expired or superseded-out target is NOT in force and so is not protected by this rule. The lookup FAILS CLOSED: an edit whose target's in-force state cannot be verified (no index, or an index read failure) is demoted with the distinct demotion_reason: "governed_target_unverified" rather than auto-committed on the strength of a broken lookup. An authoritative in-worktree backstop additionally re-judges the target's CURRENT bytes on the refreshed commit base (inside the serialized commit section, after the hard gates), consulting nothing from the index (a stale graph-exclusion edge could otherwise remove protection the base's own bytes assert), so an in-force doc that exists in git but is not yet re-indexed is still demoted -- index lag cannot bypass the rule in either direction.

    • Injection-pattern annotation (advisory only). Postimages are scanned for agent-directed injection patterns ("ignore previous instructions", exfiltration-shaped imperatives, base64-looking blobs, "do not tell the operator", ...); a match annotates the pending entry (injection_suspect, injection_patterns: pattern names + line numbers, never the matched text) but never blocks or demotes by itself.

    • Ordering: the issue #71 secret-scanning gate (and the content- validation / duplicate-id gate) always run BEFORE this feature's demotion decision -- a postimage that would be rejected outright is REJECTED, never silently demoted.

    • Feedback loop, so a demotion is never silent: every demotion response carries the pending_id, demotion_reason, and an operator_prompt instructing the model to inform the operator; a new read-only kb_session_recap(source_session) MCP tool / GET /api/v1/session-recap REST route / kb session-recap SOURCE_SESSION CLI subcommand reports N committed / M demoted-to-pending / K rejected for a session; kb_consult surfaces a demoted_writes item in its pending_actions envelope when the calling session has open demotions; and bin/kb-session-recap-hook is a ready-to-wire SessionEnd/Stop hook script (documented for Claude Code / Codex) that prints the recap one-liner automatically.

    • CLI review gate: bin/kb propose never flows a demoted (demotion_reason) or secret-flagged (matching_pattern) proposal into its same-command interactive resolve (which defaults to accept without a TTY); it prints the operator prompt and requires an explicit later kb resolve. The MCP kb_session_recap tool requires an authenticated principal when auth is configured, matching the REST route.

    • Unaffected: the maintenance-ledger system write path (issue #113) and the operator pending-resolve path (which IS the promotion).

  • Provenance surfacing + consult hardening (issue #109). Revised decision: no authority_state/allowed_use enum (rejected as a parallel vocabulary that could drift from status); the real gaps are fixed at the root instead:

    • Memory stamping. Server-rendered memory files (kb_propose_memory) now carry type: memory and status: proposed in frontmatter (neither was stamped before), so the EXISTING status filter, status rerank, and compact deviation-only status emission all apply to an agent-written memory with zero new code paths. Promotion out of proposed happens at operator review time, not here.

    • Memory-inbox in-force floor. A document under the memory-inbox prefix (KB_MEMORY_INBOX_PREFIX, default memory/inbox/) is now NEVER in force, regardless of claimed status. It covers both a legacy inbox file and forged frontmatter on an agent-written memory. Implemented as a new is_inbox column derived once at index-build time (format.validate.is_inbox_path/memory_inbox_prefix, the single source also consulted by the write path) and composed into the existing single-sourced is_in_force predicate and SQL fragments (index.py schema version bumped 10 -> 11), not a forked predicate.

    • Computed in_force: bool on verbose kb_get and verbose kb_search hits, derived from the single-sourced predicate (status class AND validity window AND not-inbox AND, composing the issue #110 slice-2 graph rule, not-graph-excluded). Never stored in frontmatter: it is a serving-layer-only derivation (see SPEC.md's new "runtime envelope" note). Compact responses emit it deviation-only (in_force: false only when the doc is NOT in force; an in-force doc's compact shape is byte-for-byte unchanged): the compact status/freshness fields reflect raw frontmatter, so a memory-inbox doc with a forged status: active would otherwise read as an ordinary current rule with no correcting signal. The existing deviation-only status/freshness emissions stay as they are.

    • evidence: list[str] optional parameter on kb_propose_memory and kb_propose_edit (max 10 items, 500 chars each). For a memory proposal it is rendered into frontmatter via the same yaml.safe_dump path as tags (so a secret-shaped evidence item is caught by the existing full-postimage secret scan, no separate scan needed); for an edit proposal (whose postimage is caller-supplied verbatim, with no template) it is validated and redacted the same way reason already is. In both cases it is persisted in pending meta, echoed on the propose/commit audit events, and surfaced by kb_list_pending.

    • kb_list_pending surfaces provenance it already persisted: source_session, reason, and the new evidence were already written into pending meta at enqueue time but never returned; they are now on the PendingEntry model (None when absent, e.g. a memory proposal has no reason).

  • Maintenance ledger: committed corpus-state audit + pending_actions CTA surface (issue #113). A frontmatter-only markdown doc (default tooling/maintenance-ledger.md, KB_MAINTENANCE_LEDGER_PATH) is now computed at every index build: whether status is present on every indexed document (except reserved filenames), plus a capped list (50 paths

    • a total count) of the ones missing it (the migration vehicle for making status mandatory), and, consuming issue #107 validity data, documents that recently expired or are expiring soon within configurable windows (KB_MAINTENANCE_RECENTLY_EXPIRED_DAYS / KB_MAINTENANCE_EXPIRING_SOON_DAYS, both default 30 days), each capped the same way. When the computed state changes since the last committed copy, the ledger is committed through the same serialized write/commit machinery every other write uses (system agent identity data-olympus-system, a normal maintenance_ledger audit event); a commit failure is logged/audited and never breaks index refresh or serving. The ledger doc is excluded from its own audit and always carries valid concept frontmatter, so it lints clean on its own merits rather than relying on reserved-filename semantics. The same computed state drives a new pending_actions field (a list of {kind, message, count} items) on kb_consult and kb_health responses, deliberately never kb_search, present only while the corpus is dirty; both tool descriptions now instruct the model to surface open items to the operator and act on them only with operator confirmation. kb health's plain-text CLI summary displays any open items. Silencing is automatic: remediate, the next index build flips the flag, the next ledger commit records it, and pending_actions disappears with no manual acking. See docs/operations.md §5.

  • Validity/freshness frontmatter metadata with hard expiry semantics (issue #107, format 0.2). Concept documents may now declare an optional nested validity object: valid_from, valid_until, last_verified, recheck_by (ISO date or datetime, normalized to a date at index/lint time), and free-text verification_source. Indexed as columns on the docs table (schema version bumped 9 -> 10, on top of the lifecycle-edges bump below). See SPEC.md section 4.2.

  • data-olympus init <dir> bundle scaffold command (issue #66). Creates a new knowledge bundle: the tier directories (--tiers, default universal, tech-stacks,projects,decisions,workflows,tooling), a root index.md carrying the spec_version/okf_version frontmatter, a template.md authoring scaffold, and one example concept document per SPEC-supported type (standard, decision, workflow, project, memory, reference), including a real superseded/superseded_by/supersedes pair and applies_when trigger metadata. The generated bundle passes data-olympus lint with zero errors and zero warnings and builds cleanly under data-olympus index. Refuses to scaffold into a non-empty directory (no --force in this slice).

  • Typed lifecycle relationships: parsed, indexed, and lint-validated (issue #110, slice 1). supersedes (scalar ID or list of IDs, normalized to a list at parse time so both shapes the ADR importer emits lint clean), superseded_by (scalar ID), and a new contradicts field (scalar ID or list of IDs, normalized to a list the same way as supersedes; unresolved-conflict evidence that never affects retrieval ranking) are now parsed into ParsedDoc and extracted into a new edges table (source_id, rel, target_id) at index-build time (schema version bumped 8 -> 9). kb lint gained a cross-file pass (an in-memory id map over the discovered bundle, no database) that reports: errors for a malformed field shape, self-supersession, or a supersession cycle of any length; warnings for a dangling target id, an asymmetric supersedes/superseded_by pair, a path-shaped target value (concept ids are the only stable target, never paths), superseded_by set on an in-force document, status: superseded with no superseded_by, and an in-force contradicts pair. SPEC.md section 4.2 documents the normalized shapes and the full lint severity list; the "not validated today" note is removed. Slice 2 (a separate change) will consume the edges table for in-force graph exclusion, kb_get/kb_search surfacing, and a health counter.

  • Supersession edges become executable retrieval policy (issue #110, slice 2). in_force=true now ADDITIONALLY excludes any document that is the TARGET of a supersedes edge whose SOURCE document is itself in force (the full status-class-AND-validity-window predicate, not merely status: superseded): a draft, expired, or already-retired document can never retire another document just by naming it in supersedes.

    • Behavior change: a document superseded only via an edge (its own status was never flipped) is now excluded from in_force=true results, even though its own status class would otherwise qualify it. It remains visible in a default (non-in_force) kb_search, same as an upcoming document. This closes the "forgotten status flip" gap.

    • A mutually-supersessive in-force cycle (already a kb lint ERROR at parse time) is NOT special-cased: every member independently satisfies the exclusion rule, so ALL members of the cycle are excluded from in_force=true.

    • A dangling edge (source or target id with no corresponding document) excludes nothing and is not counted; the exclusion query joins edges to docs on both ends, never trusting either raw id.

    • kb_consult now queries with in_force=true (previously it ran a plain default search), so it never returns a not-yet-in-force, retired, or graph-excluded document as a "governing rule". The unconditional not-expired exclusion already gave this guarantee for expired docs; this closes the same gap for status/graph exclusion.

    • New graph_excluded_docs health counter (kb_health / /api/v1/health, alongside malformed_frontmatter / malformed_validity): the count of documents currently excluded by this rule, evaluated LIVE at health-read time against the current date from the same SQL definition (format.validate.graph_excluded_ids_sql) the retrieval-time filter uses, so the counter can never drift from the filter it reports on, even across a date boundary where an in-force source's validity window opens or closes between index rebuilds. Exposed on the Index as graph_excluded_count(today=...) (injectable date for deterministic tests; health reads the real wall clock, bounded by the short health cache).

    • Retirement is explainable. kb_get (regardless of in-force/graph- exclusion status, same as it already ignores expiry) gains superseded_by: the sorted UNION of the document's own frontmatter superseded_by claim and any reverse supersedes edge naming it -- ONE consistent computed shape covering both the honest self-declared case and the forgotten-status-flip case. kb_get also gains contradicts (the document's own frontmatter list) and the computed reverse contradicted_by. All three are omitted from the compact response when empty. contradicts is annotation only: it has NO filtering or ranking effect anywhere in the retrieval path.

    • Compact kb_search hits gain a deviation-only superseded_by (the same computed union above), omitted when the document is not superseded; computed and attached to every hit regardless of in_force, so a plain search result still explains why a hit is historically superseded.

    • SPEC.md section 4.2 documents the graph-exclusion rule, the health counter, and the surfacing contract.

  • Secret-scanning gate on the write path (issue #71). Every commit path (kb_propose_memory / kb_propose_edit auto-commit, kb_resolve_pending approve, including a resolved edited_text, and onboarding bootstrap) now scans the final postimage for credential-shaped content (PEM private-key blocks, GitHub/Slack tokens, AWS access key ids, generic password=/ secret= assignments with a non-placeholder value, and connection strings with an inline password) before anything is written to disk. A match rejects the write with a distinct rejected_secret_detected status; nothing is committed and no pending entry is created on the auto-commit/bootstrap paths. Only the pattern name and an approximate line number are ever surfaced in the response, the audit event, or a log line, never the matched secret value. Operators can extend the built-in pattern set with KB_SECRET_SCAN_EXTRA_PATTERNS (comma-separated regexes; an invalid entry is logged and skipped, never crashes the server). kb_resolve_pending gains an operator-only override_secret_scan flag to consciously commit a flagged postimage that is a confirmed false positive; the override is recorded on the resulting audit event, and no auto-commit or bootstrap path exposes it (kb resolve --override-secret-scan on the CLI). A low-confidence proposal containing a secret still enters pending, so the override workflow is not defeated, but its scan result is redacted to a pattern name in the propose response and tagged (secret_scan_flagged) on the entry so kb pending surfaces the warning without ever showing the matched value; the memory filename slug itself falls back to a neutral placeholder instead of embedding the flagged text. The scan runs before the content-validation gate on every commit path so an invalid-document rejection can never echo a credential value through its own error message. The gate also covers the fields around the postimage: a credential-shaped target_path (filename) is rejected on edit/bootstrap/resolve without echoing the path back, a flagged edit reason is replaced with a redacted note before reaching pending meta / push metadata / audit events, and a flagged memory tag is stored redacted in pending meta. Extra patterns (KB_SECRET_SCAN_EXTRA_PATTERNS) with the classic nested-quantifier ReDoS shape are rejected at load time alongside invalid regexes, and every accepted custom pattern executes through the regex engine (a new runtime dependency) with a hard 1-second match timeout so a catastrophic pattern cannot hang the single-writer write path.

  • OKF profile document (issue #108). docs/okf-profile.md: a field-by- field reference distinguishing stable governance extensions (id, type, status, tier, applies_when, supersedes/superseded_by/contradicts, validity) from runtime-only serving envelope fields (in_force, freshness, contradicted_by, pending_actions) and from experimental candidates tracked against open OKF ecosystem discussions (structured relationships:, version-keyed validity, a confidence/reliability axis). Documents the issue #109 decision to reject an authority_state/ allowed_use enum, and a short factual comparison against inkxel/throughline, inkxel/dotKnowledge, and dynamicfeed/signed-okf. Cross-linked from README.md, WHY.md, and SPEC.md section 11. Documentation only; no behavior change.

Changed
  • BEHAVIOR CHANGE: kb_consult now hard-filters retrieval to the in-force class (issue #109). Previously kb_consult ran an UNFILTERED search, so an unreviewed agent-written memory, a superseded/deprecated/rejected decision, a draft/proposed document, an expired or upcoming doc, or a memory-inbox file could all be handed back as "the" governing rule for an intent, the single highest-value gap this change closes. kb_consult now passes in_force=true internally, so only active/accepted/approved documents within their validity window and NOT under the memory inbox are ever returned as rules. Operational caveat: a document with no status field at all (or a status outside IN_FORCE_STATUSES) now stops appearing as a governing rule via kb_consult, where it previously did (the prior unfiltered search surfaced it same as any other hit). status becoming a mandatory field is tracked separately (issue #114); until a bundle finishes that migration, a status-less doc that WAS relied upon as an implicit rule via kb_consult will need status: active (or accepted/approved) added before it is retrievable there again. Plain kb_search/kb_get are unaffected (they still surface a status-less doc by default); this change is scoped to the enforcement-facing kb_consult retrieval path only.

  • BEHAVIOR CHANGE: a document past its validity.valid_until now leaves the DEFAULT kb_search result set, not just in_force=true queries. Previously there was no validity concept at all, so nothing was ever excluded on this basis; a bundle that starts dating its documents will see expired ones silently drop out of ordinary search results (they are still reachable via kb_get(id), include_expired=true, or the new validity_state=expired audit facet). Rationale: unlike a superseded document, an expired one has no named successor to outrank it, so left visible it could surface as the top hit and incorrectly govern.

    • A document with a future valid_from ("upcoming") is NOT excluded by default search, only by in_force=true; it is flagged freshness: "upcoming".

    • A document past recheck_by ("stale") stays in force and visible; it is flagged freshness: "stale" (advisory only).

    • kb_search gains include_expired (default false) and a validity_state facet ("expired", "stale", "expiring_within:N") for audit queries. kb_get by id always resolves regardless of expiry, returning the full validity object plus a computed freshness indicator. kb_consult never returns an expired document (as of issue #109 below, via the in_force=true hard filter; previously via the default search path's own expired-exclusion).

    • Compact search hits gain a deviation-only freshness field (stale/expired/upcoming), omitted when fresh or absent.

    • kb lint gains three new WARNING-only findings (never errors, since they are wall-clock-relative): recheck_by in the past; valid_until in the past while status is in the in-force class; a malformed validity value (treated as absent for indexing/search, fail open).

    • kb_health / data-olympus report gain a malformed_validity counter mirroring the existing malformed_frontmatter one.

    • New data-olympus validity-report CLI subcommand lists expired and soon-to-expire documents by scanning a bundle directory directly.

    • timestamp is unaffected and remains content-change metadata only; SPEC.md now explicitly forbids deriving staleness from timestamp or last_modified.

Fixed
  • data-olympus index silently regenerated zero indexes for a bundle whose absolute path passes through a skip-named ancestor (found in companion review of the init scaffold). regenerate_indexes matched skip-directory names (.git, .venv, node_modules, ...) against the bundle's absolute path components instead of the bundle-relative ones, so a bundle located under e.g. a .venv/ or node_modules/ parent produced "wrote 0 index.md file(s)" with no error. Skip matching is now relative to the bundle root, matching discover_bundle_files in the lint pipeline.

0.3.5 - 2026-07-06

Fixed
  • Ambiguous rejection reason for writes outside the deployment's indexed prefixes. kb_propose_edit (and kb_propose_memory) rejected a well-formed, non-malicious path outside KB_INDEXED_PREFIXES with the same opaque reason string (traversal_or_excluded / not_md_or_excluded) used for an actual traversal or control-character attempt. A syntactically fine path outside the configured prefixes is an ordinary deployment-configuration fact, not a structural or security rejection - conflating the two sent an operator hunting for a nonexistent traversal bug during a company-knowledge KB audit session (operator/laptop.md, readable and indexed for search, came back rejected on write with no indication it was simply outside the deployment's writable-prefix allowlist). rejected_path_not_indexable responses (and audit events) now carry a specific reason: structurally_invalid (empty/control-chars/absolute/traversal/excluded segment), not_markdown, or not_in_indexed_prefixes.

  • Investigated the same session's second report (a rejected_stale_base resolve appearing to "leak" its path lock) and confirmed it is not a bug: restore_resolve deliberately keeps the path lock held after a gate rejection so the operator's still-pending proposal can't be clobbered by a concurrent write to the same path (documented behavior, hardened across multiple rounds of review). rejecting the stale entry (which does release the lock) and re-proposing fresh, exactly what that session did, is the correct and only intended recovery for a pending entry with a permanently-wrong base marker - there was nothing to fix.

0.3.4 - 2026-07-05

Fixed
  • Enforcement gate false-positives on prose that merely mentions a governed keyword, for tool calls with a known file path. IntentClassifier.classify scanned action_diff (the raw Write/Edit content or Bash command text, up to 4000 chars) for free-text keywords like auth, rls, tenant, secret in addition to intent, for every tool call. A scratch write whose text simply discussed one of those words as a topic - not an actual governed action - tripped the pre-tool gate regardless of the real (ungoverned) write target, and a fresh kb_consult call did not clear it because the consult's own classification (intent-only) and the gate's classification (intent + action_diff) disagreed on whether the action was governed. Keyword matching on action_diff now only applies when the tool call carries no action_path (Bash commands and OpenCode's patch, whose content is the only signal available); when an action_path is present (Write/Edit/MultiEdit), classification uses the path globs and dependency command patterns, unchanged, so this keeps full detection for real governed edits (a patch touching package.json) without flagging unrelated prose in files the path globs don't consider governed. Found via a company-knowledge KB audit session that reproduced it live (a scratch .txt file containing "RLS tenant boundaries auth trust model" was blocked with no governed path or command in sight); independently reviewed via codex exec before release.

    • The audit's other finding (CX1: the pre-tool gate and the pre-commit report --staged gate resolving two different workspace keys) was already fixed in v0.2.0 (#64, worktree-invariant workspace key); no further change needed. It was still visible in that audit only because it predated the deployed kn-dev redeploy.

0.3.3 - 2026-07-05

Fixed
  • MCP streamable-HTTP session churn under long-lived clients. A persistent client (notably opencode serve) that holds the GET /mcp SSE stream open was being forced to reconnect on a cycle, piling up abandoned transports on the server and leaking event listeners on the client until it degraded. Two causes addressed:

    • The activity middleware now keeps a session non-idle for as long as its SSE stream is actually open (re-stamping every KB_SESSION_TOUCH_INTERVAL_SEC, default 30s, clamped to a third of the idle window), so the reaper evicts a session only after its stream has genuinely closed. With that safety in place, the idle-reap default (KB_SESSION_IDLE_TIMEOUT_SEC) drops from 1800s to 300s, so an abandoned handshake is cleared in minutes instead of half an hour.

    • The shipped ingress manifest (deploy/k8s/ingress.yaml) now sets a long proxy-read-timeout/proxy-send-timeout and disables buffering, so a proxy with a short default idle timeout (nginx: 60s) no longer closes the SSE stream and triggers the reconnect loop. See docs/serving.md.

0.3.2 - 2026-07-05

Fixed
  • gate/check no longer self-DoSes multi-agent fleets with 429s. The enforcement hook calls POST /api/v1/gate/check (and kb_gate_check) once per gated tool action for every agent, but it shared the write/consult quota (KB_RATE_LIMIT_PER_HOUR, default 100). Behind an ingress with no auth, all clients collapse to a single limiter bucket, so a few active agents exhausted the hourly quota in minutes and everything after got 429 Too Many Requests. gate-check now has its own ceiling, KB_GATE_CHECK_RATE_LIMIT_PER_HOUR, defaulting to 0 (unthrottled, consistent with the read routes) since it is a mandatory per-action probe that does only cheap classification and no writes. Set a positive value for an explicit backstop. consult and onboarding/cleanup-plan stay throttled by KB_RATE_LIMIT_PER_HOUR.

0.3.1 - 2026-07-05

Fixed
  • prepare-git initContainer is now idempotent across pod restarts. It staged the deploy key with cp /etc/git-key-mount /state/git-key then chmod 0400. Because /state is a PVC, on any pod restart the 0400 (unwritable) key already exists and the plain cp fails under set -e, leaving the pod stuck in Init:Error. The manifest now rm -fs the staged key before copying (the /state dir is writable to uid 65534 via fsGroup). Only the first boot was ever exercised before; a kubectl rollout restart or any pod recreation hit it.

0.3.0 - 2026-07-03

Added
  • Serving/ops hardening (WP3a). Production-readiness work on the serving path, container, and operator docs:

    • Readiness split from data staleness. New GET /readyz (200 when the process is up and the index is loaded, independent of staleness) and GET /livez (always 200). The Kubernetes readiness probe now targets /readyz instead of /api/v1/health, so a git-remote outage that makes the KB stale no longer ejects a single-replica pod from the Service and turns "reads are slightly old" into a hard 503. /api/v1/health keeps its 503-on-degraded contract for the bin/kb --no-stale flag; alert on it rather than probing it.

    • Audit-log rotation with chain continuity (KB_AUDIT_MAX_BYTES). Size-based rotation that carries the tamper-evident hash chain across files (the first event of a new segment links to the last hash of the previous one), so verify validates across the rotation boundary. The read path can include rotated segments for --since queries and bounds the reverse scan. Off by default (single-file, backward compatible; old single-file logs still verify).

    • Proxy-header configuration (KB_TRUSTED_PROXIES). Enables uvicorn proxy_headers + forwarded_allow_ips so the rate limiter sees the real client IP behind an ingress instead of collapsing every client into the proxy address. Off by default (X-Forwarded-For ignored) so a direct client cannot spoof its address.

    • Rootless container. The root+gosu entrypoint phase is removed. Deploy-key staging and the first-boot /kb-main clone move to a non-root prepare-git initContainer; the main container and initContainer both run as uid 65534 with runAsNonRoot: true, all capabilities dropped, and a read-only root FS, so the manifest passes the restricted Pod Security Standard. The base image is digest-pinned, the git SSH host is build-arg + runtime-env configurable (KB_SSH_KEYSCAN_HOST), Docker Compose binds 127.0.0.1:8080, and the Ingress is commented out of the default kustomization (opt-in after auth is set) with a deploy/k8s/README.md note.

    • malformed_frontmatter in health. The count of docs whose front-matter was present but malformed at the last index build (from WP2b) is surfaced on the health payload. It is a warning signal and deliberately does not flip degraded.

    • Operations runbook. New docs/operations.md (linked from README and docs/serving.md) covering backup (git remote plus the audit chain / pending proposals / unpushed commits it does not cover), upgrade (image bump, taxonomy compatibility, index rebuilds), recovery playbooks (degraded/fetch-failed, history rewrite, frozen and rebase-conflict-demoted push entries, orphaned locks), and the health/readiness/alerting model.

  • Write-pipeline integrity core (epic #72). This wave makes the write-safety claims in SPEC.md section 8 and docs/serving.md actually true; each item ships with regression tests, including two integration tests that drive a real second clone against a shared bare remote:

    • Write serialization. A process-wide write serializer wraps the write → git add → commit → enqueue critical section, and a per-path advisory lock (now shared between the auto-commit path and the pending queue) prevents an auto-commit from racing a concurrent write or landing on a path with a pending proposal in flight (whose later approval would clobber it). The auto-commit path previously took no lock, so concurrent kb_propose_* calls in one session could interleave and one thread's commit could sweep another's staged file.

    • Non-fast-forward push recovery. A push rejected non-FF (a second overlapping session moved origin/main) is now classified distinctly from a network failure: the push loop fetches + rebases the session branch onto origin/main and retries. On a rebase conflict the commit is demoted to a pending entry for operator resolution (with a push_conflict_demoted audit event and the queue entry removed) instead of retrying identically forever. Pure contention (a repeated non-FF race) is retried in-line for a bounded number of passes and then demoted the same way, so it never silently freezes.

    • CAS also enforces base_commit. When the base marker is a specific commit (not the HEAD sentinel), the blob the target had at that commit must equal the current blob, closing the bypass where a caller supplied only base_commit. If the base cannot be refreshed onto origin/main (remote unreachable / rebase conflict) while an enforceable marker was supplied, the write is rejected rejected_stale_base rather than committed against a stale base.

    • CAS enforcement. base_commit / base_blob_sha / target_file_hash were accepted and stored but never checked. They are now enforced at commit time on both the auto-commit and resolve paths: a supplied base marker that does not match the current content on the worktree's refreshed base is rejected rejected_stale_base without committing. No marker → behavior unchanged.

    • Content-validation gate. No validation ran on the write path, so malformed YAML, a forged/duplicate id, or an invalid enum value committed and pushed to origin/main, and a duplicate id broke every subsequent index rebuild (one bad write, persistent degraded state). Every postimage is now format-validated plus checked for a duplicate id, using the EFFECTIVE id the rebuild assigns (explicit frontmatter id OR the path-derived id) against BOTH the live index and the session worktree's committed tree, and including reserved files, before commit; failures are rejected rejected_invalid_document with machine-readable errors. Rendered memories pass the gate as a cheap self-check. The bootstrap path now commits its whole file bundle through the same serialized, per-path-locked, validated, reset-on-failure write path as the single-file writes (it previously wrote and committed directly, unserialized and ungated).

    • Atomic pending claim. approve/reject were get-then-remove, so two concurrent resolves of the same id both committed. Resolution now claims the entry via os.rename before reading (test-and-set); the loser gets already_resolved / not-found. Orphaned path locks (a crash between lock create and entry write) are reclaimed by the pending GC loop. The gated resolve path holds the claim + path lock across the CAS/validation gates and restores the pending entry if a gate rejects, so an operator's proposal is never consumed without a commit.

    • Post-commit enqueue is recoverable and truthful. A push-queue enqueue failure after a successful commit no longer turns a made commit into an exception path (which would drop the bootstrap in-flight guard and the resolve claim). Instead an in-process recovery re-enqueue is attempted so the live push loop still drains it, and the response push_state is truthful: queued only when the entry landed, else enqueue_failed_recovery_pending (the durable commit on the session branch is republished by startup init_recovery).

    • Unique memory filenames. <date>-<slug>.md collided for same-day, same-slug memories and the second auto-commit silently overwrote the first; a short session/body-derived uniquifier is now appended.

    • Pending expiry audit. Expiring a >24h pending entry now emits a pending_expired / auto_rejected audit event instead of rejecting silently.

    • Ordering of side effects. The commit message and all gates are now evaluated before the file is written and git add-ed; on any post-add failure the worktree is hard-reset so no staged leftover is swept into the next commit.

    • Git identity in the shipped image. The Docker entrypoint (and the server main() as a fallback) now set a default GIT_AUTHOR_*/GIT_COMMITTER_* identity (overridable via KB_GIT_AUTHOR_NAME / KB_GIT_AUTHOR_EMAIL) so a commit inside a fresh container no longer fails with "who are you".

  • PyPI distribution + data-olympus setup wizard (issue #70). The package is now installable from PyPI (uvx data-olympus, uv tool install data-olympus), published by the release chain via GitHub Actions Trusted Publishing (OIDC, no API token). A new .github/workflows/publish-pypi-reusable.yml builds the sdist + wheel, runs twine check --strict and a wheel smoke test, and uploads through pypa/gh-action-pypi-publish to the pypi environment; tag-release.yml calls it off the same decided tag as the container image, and .github/workflows/publish-pypi.yml adds a PR-time dry run (build + twine check, no upload) plus a manual-tag fallback. The publish path is inert until the operator completes the one-time pypi.org publisher setup documented in docs/releases/pypi-trusted-publishing.md. The wheel now ships the enforcement machinery (bin/kb-enforce-hook, _kb_enforce.py, the OpenCode gate plugin) as package data so the wizard works from a clean install.

  • data-olympus setup (new src/data_olympus/setup_wizard.py + CLI subcommand): an idempotent, first-run/update wizard that probes the server endpoint (/api/v1/health), detects installed agents (Claude Code, Codex, Gemini, OpenCode), writes each agent's MCP registration with timestamped backups (preferring claude mcp add / codex mcp add when the CLI is present, merging documented config files otherwise), offers enforcement-hook install via the existing provider registry, and prints a doctor summary (endpoint reachability, agents wired, hooks installed, installed vs latest version with offline-tolerant PyPI/GitHub lookup). data-olympus setup --check is a fully read-only summary and update-check path.

  • data_olympus.__version__ now reads from the installed distribution metadata instead of a hand-maintained literal, so it cannot drift from the packaging version the release chain tags on.

  • data-olympus import command to migrate an existing agent-rule corpus into a governed draft bundle (issue #67). Supports six source kinds via --kind:

    • Flat rule files (claude-md / agents-md / gemini-md / cursorrules): heuristic splitting into candidate concepts (markdown headings first, then blank-line-separated bullet clusters for heading-less files). Each becomes a draft with stamped frontmatter (generated <prefix>-NNN id, type: standard, status: draft, tier/category from flags, title from heading, description from the first sentence, tags heuristically from content). The original text is preserved verbatim as the body.

    • adr (adr-tools doc/adr/NNNN-title.md directories): maps number+title to ADR-NNNN/title, parses the ## Status section into supersedes / superseded_by references, preserves the parsed adr-tools status in a non-activating source_status field, type: decision.

    • okf bundles: normalizes alias field names into the profile, fills missing required fields with draft-safe defaults, and reports every inference.

    • Everything lands as status: draft; nothing is committed to git and nothing auto-activates (imported ADRs keep their original status in source_status for the reviewer). Duplicate ids are refused. The command writes files into the output dir and prints a human-readable and (with --json) machine-readable report: files created, sections skipped as too short, inferences made, and "needs human review" items. It runs the existing lint machinery over the output (lint-clean on the happy path) and points at kb_cleanup_plan for dedup when importing into an existing KB. Re-running into the same output dir is refused unless --force is given (no silent duplication or clobber). Type/status/tier vocabularies are single-sourced from format/validate.py.

  • Specified applies_when in SPEC.md (WP4c, 0.3.0). applies_when is the highest-weight indexed field in Index.search (tied with title, ahead of description and body) and one of the three discriminating columns for the abstain signal gate, but was previously implemented and used in the benchmark story without ever being documented. SPEC.md section 4.2 now lists it as a recommended field with authoring guidance (short verb phrases in an agent's own vocabulary) and a worked example; docs/adoption.md gained a matching authoring section. Documented honestly: unlike tags, a malformed applies_when produces no kb lint warning today.

  • Upgraded example-bundle/ to demonstrate the format's remaining differentiators (WP4c, 0.3.0): a real supersession pair (STD-U-003/STD-U-004, status: superseded/supersedes/superseded_by), a memory document and a reference document (the two concept types the bundle previously lacked), and applies_when on the standards most likely to be hit mid-task. example-bundle/index.md and the new directory index.md files were updated to match; uv run data-olympus lint example-bundle remains zero-error.

  • Quickstart lifecycle-aware retrieval demo (WP4c, 0.3.0). docs/quickstart.md gained a verified section showing default search (soft status rerank: the active STD-U-004 outranks the superseded STD-U-003 it replaced, both still returned) versus in_force=true (hard filter: STD-U-003 excluded from the result set entirely), against the upgraded example bundle.

  • CI doc-consistency guard (WP4c, 0.3.0): scripts/check_doc_consistency.py, wired as a new doc-consistency-guard CI job. Fails when the type/status/tier enum lists restated in SPEC.md or docs/adoption.md drift from data_olympus.format.validate's canonical TYPES/STATUSES/TIERS, or when SPEC.md's reserved-filename list drifts from validate.RESERVED. Checks every restatement of an enum in a file (SPEC.md states type/status twice; both are now checked, not just the first), tolerant of reordering, rewrapped lines, and an Oxford "or".

  • Cheap OKF minimal-field structural check (tests/test_okf_minimal_fields.py, WP4c, 0.3.0): asserts every example-bundle concept doc has non-empty id/type and the bundle-root index.md declares okf_version. This is a structural floor, not a conformance test; the module docstring states exactly what it does and does not prove.

  • Benchmark re-cut with honest baselines and regenerated public numbers (epic #75, WP2c). Methodology fixes so the benchmark measures retrieval, not string-echo, and so every attribution is defensible:

    • Status-aware BM25 baseline (bm25-status-aware): the same BM25 ranker that also reads status frontmatter and skips superseded/deprecated docs. It isolates "the win is having governance metadata" (serves-stale 0.000, like data-olympus) from "the engine is a better ranker".

    • Honest ranking for the strawman baselines. whole-dump no longer scored on recall@k/NDCG/MRR (it does not rank; ranks = False, reported only on token cost / precision / order-free Contains-Gold); grep-read now ranks by query-term match count instead of alphabetical file order.

    • De-leaked synthetic corpus. Lifecycle answer-vocabulary ("previous", "current", "(old)"/"(current)") removed from doc bodies and titles; the lifecycle signal now lives only in status + the supersedes chain, and old/new pairs are lexically identical. Shared distractor vocabulary added. Remaining known leak (the exact topic-word echo) is documented on purpose.

    • serves_stale metric (headline lifecycle metric): fraction of supersession-topic queries where the retired doc reached the payload at all (tiebreak-independent), replacing the tiebreak-sensitive staleness rate as the honest signal.

    • Bootstrap confidence intervals (metrics.bootstrap_mean_ci, seeded and deterministic): every reported mean now carries a 95% percentile-bootstrap CI.

    • Normalized token policy: token cost reported both as-shipped and under a normalized policy (top-1 full doc per method), so the token comparison separates retrieval from response-payload convention.

    • Grown governance strata for non-degenerate CIs: 30 governing topics, 10 supersession pairs, 31 distractor topics (trigger_covered n=30, negative n=31, supersession n=10, paraphrase_uncovered n=74 after enforcing FTS-token disjointness from gold triggers).

    • Committed real-corpus result: benchmarks/real_corpus_eval.py --lexical-only over the committed example-bundle (18 hand-authored docs, 9 paraphrase queries) writes a reproducible non-templated number to benchmarks/real_corpus/example_bundle_result.json.

    • Docs-drift guard: docs/comparison.md and WHY.md benchmark tables are generated from the committed result JSONs by benchmarks/docs_tables.py between <!-- BENCH:* --> markers; scripts/check_benchmark_docs.py (wired into CI) fails the build if any quoted number drifts.

    • All committed results (benchmarks/results/, governance_results/, the embeddings ablation, the real-corpus result) regenerated against the hardened engine. With the B1 in_force fix, data-olympus exact recall rises from the 0.858 artifact to 1.000 and ALL recall to 0.582 (edging BM25's 0.572).

  • Deployable in_force and abstain modes on kb_search (issue #68, epic #75). kb_search (MCP tool and GET /api/v1/search) gains two parameters:

    • in_force: bool HARD-filters results to the in-force status class (active/accepted/approved) before ranking, excluding superseded/deprecated docs entirely rather than soft-downranking them. The class is defined once in format.validate.IN_FORCE_STATUSES and shared by the status reranker and this filter; Index.search gained an in_force parameter (the single-status status filter is unchanged and composes with it). The filter also applies to the dense (embedding) candidate source.

    • abstain: bool applies a signal gate: a query that matches no discriminating column (title/tags/applies_when) returns no hits with abstained: true and abstain_reason: "no_signal_match" (distinct from an ordinary empty result). The gate logic is single-sourced in data_olympus.search_gate; the benchmark ablation now imports it. Fixes benchmark bug B1: benchmarks/methods/data_olympus.py switched from the status="active" filter (which silently dropped accepted gold decision docs) to in_force=True. Committed benchmark numbers, report.md, and docs/comparison.md are intentionally NOT re-cut here (a later package does).

  • Actionable gate deny message: the BLOCKED text (and the consult_required verdict reason) now includes the exact workspace key, the session id (the one parameter an agent cannot guess), and a copy-pasteable kb_consult(workspace=..., source_session=..., intent=...) call. GateCheckResponse gained session_id and workspace echo fields so MCP callers see the gate key.

  • Write-pipeline visibility (issue #72, Wave 0). kb_health and /health now report the live pending-queue and push-queue sizes (previously hardwired to zero because the counters were static attributes that were never updated), plus a new push_queue_frozen count so a stuck write path is observable.

  • Startup push recovery. On boot the server scans every per-session worktree and re-enqueues any commit reachable from its HEAD but not from origin/main, recovering commits orphaned by a crash between git commit and the push-queue enqueue. Already-queued shas are skipped, so recovery cannot double-enqueue.

  • Periodic worktree GC. A background task honors KB_WORKTREE_IDLE_SEC to remove idle per-session worktrees (deferring any with unpushed commits) so KB checkouts no longer accumulate one-per-session forever. GC also deletes the session's kb-session/<safe_id> branch, so a returning session can create its worktree again instead of hitting a fatal "branch already exists" error.

Changed
  • BREAKING (default response shape): token-compact read-tool responses (#65). The read tools kb_search, kb_get, kb_list, kb_outline, and kb_health now return a token-compact representation by default, because their consumer is almost always an LLM paying for every token. Measured against the example-bundle with a real tokenizer (tiktoken cl100k; reproduce with python -m benchmarks.token_compact --tokenizer tiktoken) this saves ~37% of tokens on kb_search, ~42% on kb_list, ~41% on kb_health, and ~6-7% on kb_get (which keeps its full body and provenance); 26.1% aggregate. What changed in the default shape:

    • kb_search: each hit is {id, title, snippet} plus status only when the hit is not currently in force (e.g. superseded/deprecated) and type when set. The query echo, the per-hit path, and the raw bm25 score are dropped; snippets are capped at 160 chars. Array order still conveys rank; to read a hit in full, call kb_get(id).

    • kb_get: keeps the full content_markdown body (unchanged) plus source_commit/last_modified provenance, and trims the envelope: path, git_remote_url, and last_modified_source are dropped, and empty status/type/applies_when/description are omitted.

    • kb_list: drops per-entry path; omits a null category.

    • kb_health: keeps the core snapshot and omits diagnostic fields that are null/empty (a no-error steady state no longer emits a run of nulls).

    • kb_outline: already lean; its shape is unchanged.

    Opt out with verbose=true (a REST query parameter, or the verbose argument on each MCP tool), which restores the exact pre-#65 JSON shape byte-for-byte. The bundled kb CLI requests verbose=true automatically, so its output is unaffected. Any first-party or third-party consumer that parsed the dropped fields (query, per-hit path/score, the full health envelope) must either adopt the compact shape or pass verbose=true. A committed measurement harness (benchmarks/token_compact.py) reproduces the per-tool token table under either tokenizer (python -m benchmarks.token_compact defaults to the dependency-free simple splitter; add --tokenizer tiktoken for the cl100k numbers quoted above), and a tokenizer-based regression test (tests/test_token_compact_budget.py, with both a simple budget and a tiktoken aggregate-savings guard) fails loudly if a future change re-bloats the compact default.

  • MCP/REST write-surface parity (deferred WP0b items). The MCP kb_resolve_pending tool now applies the KB_MAX_POSTIMAGE_BYTES edited_text cap (REST already did), and the MCP kb_consult / kb_gate_check / kb_cleanup_plan tools now apply the shared rate limiter consistently with their REST counterparts.

  • BREAKING (auth): A KB_AUTH_PRINCIPALS entry that omits an explicit capabilities list now defaults to least privilege (read, propose) instead of all capabilities. Previously such an entry silently received resolve and auto_commit, letting an agent approve its own proposals and skip operator review. Action required: any deployment relying on the old implicit full-capability default must now list the capabilities it needs explicitly, e.g. {"name": "operator-agent", "token": "...", "capabilities": ["read", "propose", "resolve", "auto_commit", "bootstrap", "record_event"]}. The single KB_AUTH_TOKEN operator principal is unaffected and still receives all capabilities.

  • Search pipeline hardening (WP2b, epic #75): false-positive reduction, dense-channel fixes, and index/query performance.

    • Penalized expansion backfill (finding a). Query expansion (synonym #38 + co-occurrence #40) previously folded derived terms into the single FTS5 MATCH; bm25 has no positional preference, so a doc matching only a synonym got full idf weight and could outrank a doc matching the user's actual term. Index.search now matches the user's own terms in a PRIMARY pass and the expansion terms in a SEPARATE penalized backfill pass whose hits can only rank BELOW the worst primary hit. The "expansion is down-weighted" claim in the query_expansion / cooccurrence docstrings is now actually true.

    • Rank-class invariant (finding d). SearchHit gained a rank_class (RANK_CLASS_PRIMARY / RANK_CLASS_BACKFILL); the status and hybrid rerankers now sort by (rank_class, score). This makes the documented "backfill hits are never reordered above primaries" invariant genuinely true (the previous score-only "strictly worse" floor could be lifted by an active-status boost or the hybrid re-normalisation).

    • Co-occurrence defaults hardened (finding b): min_count 2->3, min_pmi 0.0->0.1, plus a corpus-size floor (KB_COOCCURRENCE_MIN_DOCS, default 50) that auto-disables the table on small corpora where PMI is noise, and a per-doc unique-token cap (KB_COOCCURRENCE_MAX_DOC_TOKENS, default 400) that bounds the O(n^2) pair counting so a multi-thousand-word doc is no longer a build-time memory cliff.

    • Gated trigram index (finding c): the fts_trigram secondary table (a full second tokenized copy of the corpus, ~2-3x index-size / build-time tax) is now built and populated ONLY when the trigram fallback is enabled (KB_TRIGRAM_MODE=on), not unconditionally.

    • Dense-channel intent vocabulary (finding f): the text embedded at build time now includes applies_when and tags (the curated intent phrases a paraphrase query embeds near), not just title/description/body.

  • Enforcement gate policy: the gate now clears only on an explicit consultation, not on any recent consult (behavioral change). Previously the gate meant "an HTTP call to /consult happened recently in this session", so the per-agent installers' per-prompt auto-consults (Claude/Codex UserPromptSubmit, Gemini BeforeAgent) kept the ledger perpetually fresh and the gate only fired during autonomous stretches longer than the TTL. Consults now carry a trigger: an explicit consult (a deliberate kb_consult call, the default, and any old client that omits the field) clears the gate, while a prompt_hook consult (the installer auto-consults, now marked as such) is recorded for audit/compliance and injects rules but never clears the gate. A prompt-hook consult never downgrades a still-fresh explicit consult. Deployments that relied on the per-prompt auto-clear will now see the gate require an explicit kb_consult before governed edits. Backward compatible for old clients (a bare consult with no trigger counts as explicit). See docs/enforcement.md.

  • Frozen push-queue entries (those that hit max_attempts) are now skipped by the retry loop instead of being retried every interval forever; the freeze is logged once at WARN and surfaced via push_queue_frozen in health. See docs/serving.md for the operator unfreeze procedure.

  • The push-retry loop now drains the push queue in a thread executor (like the git-pull loop) rather than on the event loop, and GitOps.push takes a bounded timeout (default 60s), so a hung git remote can no longer block the loop that answers the readiness probe. A push timeout is classified as a retryable failure.

  • Quickstart and adoption docs lead with the uvx data-olympus / uv tool install install path and the setup wizard (issue #70). The adoption guide's per-agent wiring section is corrected to the current registration surfaces (Claude Code via claude mcp add, not a hand-edited ~/.claude/settings.json; Codex via codex mcp add; Gemini's "type": "http"; OpenCode's mcp-remote wrapper).

Scope note: this wave makes write-path failures visible and recovers orphaned commits. It does not fix the non-fast-forward publication stall itself (rebase-retry redesign); that is tracked separately as Wave 1.

Fixed
  • Release delivery for 0.3.0. Two release-pipeline bugs surfaced on the first 0.3.0 run and are fixed so the tag, image, and PyPI package publish together: the Docker image build now copies everything pyproject.toml references into the build context before the project-installing uv sync (README.md, LICENSE, NOTICE, and the bin/ machinery force-included into the wheel); and the PyPI publish runs inline in tag-release.yml rather than through a reusable workflow, which PyPI trusted publishing does not reliably match through.

  • Stale auto-commit path locks are reclaimed. A hard kill while an auto-commit held a per-path advisory lock left the lock on the /state volume with no in-process holder to release it, wedging that path (rejected_path_lock_busy) until manual cleanup. Locks now record an owner_kind; crash-orphaned auto-commit locks are reclaimed unconditionally at server startup (a fresh process holds none) and, while running, once older than KB_AUTO_COMMIT_LOCK_TTL_SEC (default 600s, far above a seconds-long commit critical section). The periodic reclaim runs under the same process-wide write serializer that path_lock acquire/release runs under, so a stalled holder that resumes cannot free the path (and let a successor re-acquire it) mid-reclaim; the delete is additionally inode + timestamp ownership-checked as defense in depth. Pre-fix legacy auto-commit locks (no owner_kind, auto-commit: pending_id prefix) are recognised too, so an upgrade on a persistent /state volume frees any old wedged path. Pending-proposal locks are never TTL-reclaimed (they legitimately live until the operator resolves or the entry expires).

  • Search pipeline hardening (WP2b, epic #75), bug + perf fixes:

    • Index.search could return MORE than limit hits when embeddings were configured but no reranker was set (finding h): the dense union widened the pool but truncation lived only inside the reranker branch. Truncation to limit is now unconditional.

    • Vectors are batch-fetched once per build and cached as an in-memory id->vector matrix (finding g). The hybrid reranker previously opened one SQLite connection per candidate hit via get_vector, and dense_candidates re-read + re-deserialized the whole doc_vectors table on every query. The cache is keyed by the db file's (size, mtime) so the atomic build swap invalidates it transparently.

    • Index.build now runs a single git log --name-only pass for every file's last-commit time instead of one git log subprocess per file, and reads + parses each markdown file exactly once (previously three times: two parses plus a separate content read) (finding i). Per-file (last_modified, source) metadata is byte-for-byte identical.

    • Malformed front matter is no longer silently swallowed (finding j): a doc whose front-matter block is present but is invalid YAML (so its status/supersedes are dropped, quietly disabling staleness protection) is now logged at WARN per doc, counted, and surfaced via Index.malformed_frontmatter_count and the malformed_frontmatter field in health(). (Wiring this count into the /health degraded signal is a tracked follow-up.)

  • Onboarding pipeline (tools_onboarding.py, issue WP3b). Four coherence and atomicity fixes, each with regression tests:

    • partial is no longer a dead end. kb_bootstrap_project accepted only absent and rejected partial with rejected_already_onboarded, contradicting the playbook that told the agent to bootstrap on absent OR partial. It now proceeds on partial, narrowing the request to only the exact missing_files paths under this workspace/component so an already-committed file is never overwritten and a basename collision cannot smuggle in a write to a different project or component; if nothing supplied fills a gap it rejects truthfully rather than committing an empty change. The playbook text now matches this behaviour.

    • Double-bootstrap race closed. A committed bootstrap is invisible to the index until it is pushed and re-pulled, so a second call in that convergence window passed the absent re-check and double-committed. A short-lived, self-expiring in-flight marker (onboarding_inflight.py) is now claimed on the committed path and rejects a concurrent bootstrap for the same workspace/component as rejected_already_in_progress. The playbook now instructs waiting for kb_onboarding_status to report onboarded before running kb_cleanup_plan.

    • Pending bundle is now atomic. In the low-confidence path a PathLockBusyError was silently swallowed per file, so a partially-enqueued bundle returned pending_confirmation with only the subset that fit. Lock-busy is now treated like queue-full: the whole bundle rolls back (zero orphan pending entries or held locks) and rejects as rejected_path_locked. Every entry of a bundle is stamped with a shared bundle_id in its metadata (the seam for a future bundle-aware resolve UX).

    • Safe remote-URL injection confirmed. The git_remote_url front-matter injection parses and re-serializes YAML with yaml.safe_dump (a newline-laden URL cannot forge keys) and its presence check is key-aware, so a URL mentioned only in the body no longer false-positives and suppresses injection. Regression tests lock both behaviours in.

  • OpenCode gate (data-olympus-gate.ts): resolves the workspace to the main git worktree basename (matching the key every other surface records consults under) instead of the raw absolute path, which could never match; and passes the tool arguments (command/patch/content) as action_diff, so bash and patch actions are actually classified instead of the gate seeing empty everything and allowing.

  • Enforcement hook (bin/kb-enforce-hook): a null session_id now renders as an empty string instead of the literal "None" (which became a bogus ledger key); the critical-path pre-tool gate now uses tight --max-time 2 --connect-timeout 1 timeouts; and the pre-tool payload is parsed once (base64-framed) instead of spawning python3 per field.

  • Anchored the installer tool matchers (^(...)$) so Bash no longer substring-matches BashOutput (and likewise for the Codex and Gemini matchers).

  • kb enforce doctor now also verifies the managed marker/version in the live settings file and that the hook dispatcher exists and is executable, and warns (and fails) when the dispatcher resolves inside a .worktrees/ or .claude/worktrees/ checkout, which dangles after pruning and silently fails open.

  • Docs: docs/enforcement.md now states that Codex DOES gate Bash (matching the installer) and documents the explicit-consult gate policy honestly.

  • scripts/run-local.sh no longer deletes an arbitrary user-supplied $1 path. It previously ran rm -rf "$KB_DIR" unconditionally, so passing your own bundle's path as the first argument would silently delete it. The script now refuses to touch a pre-existing directory that is not its own default demo path and does not carry a marker file it created itself, printing the correct way to serve your own bundle (direct data-olympus-mcp invocation with KB_MAIN_PATH, per docs/quickstart.md section 6) instead. The no-argument default demo flow is unchanged.

  • data-olympus index and data-olympus visualize no longer treat template.md as a concept document. Both modules maintained their own {"index.md", "log.md"} reserved-filename set, drifted from format/validate.py's RESERVED (used by lint and validation), so a directory containing only a template.md was wrongly treated as holding concept docs: index generated an index.md for it and listed template.md as a concept entry, and visualize counted it as a graph node. Both now import the same RESERVED set as the rest of the format tooling, so index.md, log.md, and template.md are consistently reserved everywhere.

Security
  • Hardened the write and enforcement surface (issue #74). Ten fixes, each with regression tests:

    • Request body caps. The resolve, consult, gate/check, and audit/event REST routes read the body with an uncapped request.json(); they now go through the same KB_MAX_BODY_BYTES streaming cap as propose/bootstrap and return 413 on oversize input.

    • resolve.edited_text cap. Operator-supplied edited_text on approve became the committed postimage with no size check; it is now bounded by KB_MAX_POSTIMAGE_BYTES and rejected with a distinct rejected_edited_text_too_large status, leaving the pending entry in place.

    • YAML frontmatter injection. Memory frontmatter was string-concatenated, so a tags / agent_identity value containing a newline or ] could forge reserved keys (id / status / supersedes); a forged duplicate id breaks every index rebuild. Frontmatter is now serialized with yaml.safe_dump, and the size cap counts the full rendered postimage (frontmatter + body), not just the body.

    • Backslash / control-char path bypass. decisions\x.md validated as decisions/x.md but wrote a literal root-level file outside every indexed prefix and invisible to KB_WRITE_BLOCK_PATHS. Path normalization now returns the canonical form and every downstream operation (classification, blocklist, join, git add, pending record) uses it; control characters (newline, CR, tab, NUL) in a target path are rejected. The same canonical-path handling and safe YAML frontmatter (with the size cap applied after remote-URL injection) now also cover the onboarding bootstrap path.

    • Enforcement-plane auth + rate limiting. /consult, /gate/check, and /onboarding/cleanup-plan were anonymous-allowed even when auth was configured and were unthrottled. They now require an authenticated principal when auth is configured (no-auth deployments are unchanged) and are subject to the shared rate limiter. kb_cleanup_plan is added to the MCP auth-required tool set so the MCP enforcement plane matches REST.

    • Viewer HTML injection. A doc body containing </script> could break out of the embedded <script> block and run arbitrary JS; </ is now escaped to <\/. Fixed a substitution-ordering bug where a body containing the literal __DISPLAY_NAME__ was mangled (both placeholders are now substituted in a single pass).

    • Search limit clamp. kb_search clamped only the upper bound, so limit=-1 reached SQLite as LIMIT -1 (unlimited full-corpus dump); it is now clamped to 1..100.

    • Actionable status codes. Malformed since / limit query params on /audit and /compliance now return 400 instead of an opaque 500, and resolve of an unknown/expired pending_id returns 404 (with path-traversal-shaped ids rejected).

    • Rate-limiter hygiene. The sliding-window limiter now evicts empty bucket keys (bounding memory under varying identities) and guards its read-modify-write with a lock (correct under the threadpool the REST handlers run on).

Documentation
  • Reworded OKF-compatibility claims for honesty (WP4c, 0.3.0). README.md, SPEC.md, WHY.md, and docs/comparison.md asserted "OKF-compatible" / "any OKF consumer can read a data-olympus bundle unchanged" / "every data-olympus bundle is a valid OKF bundle" with zero executable backing: no test runs a real OKF reference consumer/producer against a data-olympus bundle. The strongest claims are now worded as design intent ("designed to be readable by OKF consumers") backed by the shared structure that does exist by construction (directory layout, frontmatter conventions, reserved filenames, link model), with formal conformance testing tracked in issue #82. A cheap, non-behavioral structural floor (non-empty id/type on every example-bundle concept doc, okf_version on the bundle-root index) was added as tests/test_okf_minimal_fields.py; its docstring is explicit that this proves frontmatter shape, not that any OKF tool can read the bundle.

  • Corrected several stale or inaccurate claims ahead of 0.3.0: docs/adoption.md now describes the actual KB_MAIN_PATH-ignoring behaviour of scripts/run-local.sh (pointing to the direct server invocation instead), and its frontmatter schema section and supersedes/superseded_by semantics now match src/data_olympus/format/validate.py and SPEC.md (concept IDs, not bundle-relative paths). SPEC.md no longer promises kb lint warnings for missing supersedes/superseded_by/owner or broken links, neither of which is implemented; its reserved-filename list in section 9 now includes template.md, and its okf_version example matches what example-bundle/index.md actually ships ("0.1"). README.md drops the stale "pre-release (v0.1)" status line and adds the Python 3.13+ requirement to the quickstart, plus links to SECURITY.md and benchmarks/README.md. CONTRIBUTING.md's documented data-olympus lint output now includes the (N linted) suffix the CLI actually prints. CHANGELOG.md gains the previously missing [0.1.1] section (the tag was released 2026-06-24 without a changelog entry) and its compare link. docs/comparison.md's RAG section and "Honest weaknesses" no longer claim there is no embedding/semantic retrieval at all; they now describe the optional local-embedding hybrid (off by default) shipped in 0.2.0. example-bundle/index.md no longer claims to demonstrate "all supported concept types" (it has no memory, reference, or superseded example documents).

0.2.0 - 2026-07-03

Added
  • Content-free real-corpus retrieval eval harness (benchmarks/real_corpus_eval.py

    • benchmarks/real_corpus_eval.md). Point it at your own KB directory and a labeled query set to measure what the optional local-embedding hybrid (issue #42) adds over the lexical stack (FTS + synonym + co-occurrence expansion) on your corpus; it prints only aggregate metrics (recall@k, MRR, recovered / regressed counts) and never document text, so it is safe to run over a private KB. The writeup includes a worked private-corpus example and an honest reading positioning embeddings as opt-in rather than default-on.

  • Automated, human-gated release pipeline (conformant with the STD-U-810 versioning standard). A daily data-olympus-release-cutter routine computes the SemVer bump from Conventional Commits and opens a release PR; merging that PR cuts an annotated vX.Y.Z tag, a GitHub Release, and the container image via CI. New helpers scripts/compute_release.py, scripts/should_tag.py, scripts/lint_pr_title.py (a PR-title Conventional Commit check enforced by .github/workflows/pr-title-lint.yml), and scripts/compute-version.sh (preview version plus invariant check). New tag-release.yml cuts the tag on a pyproject.toml version bump, builds the image via a reusable release-image-reusable.yml (which also stamps OCI version and revision labels), then publishes the GitHub Release. Process is documented in .rules/versioning.md and .rules/release-routine.md. No tags are ever cut unattended.

  • Optional local-embedding hybrid ranking (issue #42). Real paraphrase / synonymy handling via a LOCAL ONNX MiniLM-class model (no external API, no query-time network), blended with BM25. OFF by default (KB_EMBEDDINGS_MODE, default off): with the feature off and the embeddings extra uninstalled, nothing imports the embedding stack and the lexical product is byte-for-byte unchanged. When enabled, each doc is embedded at Index.build time into a new doc_vectors table (schema v8, float32 blobs) written into the same tmp DB and swapped atomically, so vectors rebuild atomically with the rest of the index. A hybrid reranker embeds the query once and blends NORMALISED bm25 with cosine over the candidate hits' stored vectors, then re-sorts. It composes as the INNER layer under the exact-id/exact-tag short-circuit (so an exact id/tag still wins) and wraps the status prior (so an active doc still outranks the superseded one it replaced); when disabled the reranker stack is exactly today's. If enabled but the extra/model is unavailable, startup fails LOUDLY with an actionable message rather than silently reverting to lexical-only. Config: KB_EMBEDDINGS_MODE (on/off, default off), KB_EMBEDDINGS_MODEL (default BAAI/bge-small-en-v1.5, 384-dim), KB_EMBEDDINGS_WEIGHT (cosine fraction of the blend in [0, 1], default 0.35). The extra pulls in fastembed (which bundles onnxruntime; no torch); the default model is a ~33 MB quantised ONNX file fetched once at enable time and cached locally. Build cost: one batched local embed pass over the corpus at Index.build (seconds for a small KB, no network at query time); storage cost is 384 x 4 bytes (~1.5 KB) per doc in doc_vectors. Install with uv sync --extra embeddings. When enabled, Index.search now adds a semantic candidate SOURCE (not just a reranker over the FTS pool): the top dense neighbours by query-doc cosine are unioned into the FTS candidate pool before the hybrid blend, so a paraphrase with ZERO lexical overlap (e.g. querying "car" against a corpus that only says "automobile") is retrieved where bm25 alone returned nothing. A minimum-cosine threshold guards abstention: an out-of-scope query whose nearest neighbour is only weakly similar pulls in nothing. A dense-only candidate (absent from the FTS results) is scored at the pool's worst (neutral-floor) bm25 so the blend ranks it by its cosine component. Two new Index knobs (dense candidate count, default 10; minimum cosine, default 0.5). With embeddings OFF (the default) search is byte-for-byte pure FTS. Embeddings settings (KB_EMBEDDINGS_MODE/MODEL/WEIGHT) are now read from env ONLY in load_config and threaded into the Index (and its embedder) via Config; the build and query paths no longer re-read the environment, so a programmatic caller's Config values are honoured.

  • Trigram fuzzy-match fallback for typos and partial identifiers (issue #41). A secondary FTS5 table (fts_trigram, trigram tokenizer, schema v7) is built into the same tmp DB and swapped atomically with the primary FTS index, so it rebuilds atomically. At query time it is used only as a FALLBACK: the primary porter unicode61 FTS query runs first and, only when it returns at or below a small threshold of hits, a trigram match (an OR of the query's own quoted trigrams, so no FTS operator injection) backfills the results. Trigram hits are APPENDED after the primary hits and scored strictly worse than any primary hit, so an exact/primary match is never diluted or reordered by a fuzzy hit, and a query with good primary hits is unaffected. A query shorter than 3 chars safely no-ops the fallback. Opt-in and off by default (existing search behaviour is unchanged): KB_TRIGRAM_MODE (on/off, default off) and KB_TRIGRAM_FALLBACK_THRESHOLD (default 3).

  • Corpus co-occurrence query expansion (embedding-free semantics, issue #40). At index-build time the indexer learns, per term, the top-k terms it most strongly co-occurs with across documents (pointwise mutual information at document granularity) into a bounded related_terms table (schema v6) built into the same tmp DB and swapped atomically with the FTS index. At query time a query_expander appends a term's related terms, down-weighted by appending them after the originals so BM25 still favours typed terms, bounded to <= 32 terms. It composes WITH the synonym expander (issue #38) rather than replacing it: synonyms run first, then co-occurrence broadens the synonym-expanded set (compose_expanders). Config knobs: KB_COOCCURRENCE_MODE (on/off, default on), KB_COOCCURRENCE_K (default 5), KB_COOCCURRENCE_MIN_COUNT (default 2), KB_COOCCURRENCE_MIN_PMI (default 0.0). Stopword-like and short tokens are skipped so the table stays focused and build cost negligible.

  • Search now short-circuits exact-id and exact-tag queries: a single-token query that is a document id (e.g. STD-U-002, or a path-derived id) is surfaced as the top hit via a direct lookup even when it is absent from the bm25 results, and a single-token exact-tag query lifts docs carrying that tag ahead of the rest. Implemented as a composable reranker (issue #39); the match stage and _build_match_expr are untouched.

  • kb_search and kb_get now support and return status and type, making the documented "filter by status / tier / type" capability real (index schema v4).

  • Retrieval now indexes applies_when trigger metadata and description with column-weighted ranking, improving coding-intent to governing-rule matching (index schema v5). kb_get returns both fields.

  • Status-aware search ranking (enabled by default): kb_search reranks hits by a status prior on top of BM25, so an in-force doc (active, accepted, approved) outranks a superseded/deprecated/rejected one that matches the same query, and drafts do not beat live guidance. Status matching is case-insensitive; unknown or empty statuses stay neutral and are never dropped. This changes result ordering for queries that hit both a live and a retired doc. A deployment overrides the built-in status->weight map via KB_STATUS_WEIGHTS (a JSON object of {status: weight}, negative boosts, positive penalizes).

  • Enforcement core: data-olympus can now act as a gated, mandatory consultation proxy for code/architectural decisions, not only an advisory KB. New MCP tools kb_consult, kb_gate_check, kb_compliance and REST endpoints /api/v1/consult, /api/v1/gate/check, /api/v1/compliance, backed by a shared heuristic intent classifier and an in-memory consultation ledger.

  • kb enforce install|uninstall|status|doctor CLI plus a kb-enforce-hook dispatcher install an idempotent, reversible Claude Code enforcement shim (SessionStart / UserPromptSubmit / PreToolUse / Stop). Fail-open by default (KB_ENFORCE_FAIL_MODE), consultation freshness via KB_CONSULT_TTL_SEC.

  • Cross-agent enforcement providers: kb enforce install --agent codex|gemini|opencode|copilot-cli|copilot-ide and --all. Codex and Gemini get hard PreToolUse/BeforeTool gates (merging into existing hooks), OpenCode gets a tool.execute.before plugin, and Copilot CLI/IDE get a soft instructions-file + MCP provider. Antigravity is a documented-unsupported stub. The kb-enforce-hook dispatcher gained a --dialect gemini output mode.

  • Detection floor for un-hookable agents: kb enforce report (and data-olympus report) correlates governed git commits against the consult audit and lists changes with no consultation on record. An opt-in repo-scoped git provider (kb enforce install --agent git) installs a post-commit warning hook, or a pre-commit blocking hook with --block. Reuses the existing audit endpoint; no server change.

  • Streamable-http session idle reaper bounds transport accumulation (issue #43). A background loop terminates sessions with no request activity for longer than KB_SESSION_IDLE_TIMEOUT_SEC (default 1800s / 30 min; 0 disables reaping and keeps observability only), scanning every KB_SESSION_REAP_INTERVAL_SEC (default 60s). kb_health and /api/v1/health gained a live_sessions field reporting the current live transport count (null until the HTTP app is serving); a value that only climbs signals leaked sessions.

  • Synonym / acronym query expansion (default-on). Before the FTS MATCH is built, the search pipeline rewrites the query term list through a curated, bidirectional synonym map, so a short-form query (k8s, rls) also reaches documents that only use the long form (kubernetes, row level security) and vice versa. Adjacent query tokens are scanned as n-grams so multi-word canonical keys match from a long-form query. Expansion is bounded (32 terms) and de-duplicated, originals are ranked first, and it is configurable via KB_SYNONYMS (extra/override groups) and KB_SYNONYMS_MODE (merge default / replace / off). This is curated lexical expansion, not semantic (embedding) retrieval.

  • Enforcement hardening and observability: gate_bypass and gate_degraded events are now recorded (via data-olympus report --emit-events / the git warn hook, and the pre-tool hook on a reachable-degraded gate), so kb_compliance surfaces them. The gate receives action_diff and the classifier uses word-boundary matching plus dependency-install command signals (Codex and Claude now gate the Bash tool). New kb enforce install --mode off|soft|hard, ledger persistence via KB_LEDGER_PATH, a friendly PATH hint for kb enforce report, and a CI guard requiring a changelog entry for functional changes.

  • Guided onboarding: MCP prompts onboard, onboard_project, and onboard_component walk an agent through bootstrapping a new workspace or component, backed by a single-sourced playbook (render_playbook). A read-only kb_cleanup_plan MCP tool plus POST /api/v1/onboarding/cleanup-plan classify local repo docs against KB content and propose thin-pointer replacements for duplicates. GET /api/v1/onboarding/playbook and kb onboard playbook expose the same script to agents without native MCP prompt support.

  • Composable search pipeline: Index.search() now runs expand-query / match / re-rank stages with pluggable query_expander and reranker hooks, so ranking and query-expansion features compose without rewriting the core query. When a reranker is installed the query is matched against a wider BM25 candidate pool (over-fetched) and the reranked result is truncated back to the requested limit.

  • Read-only replica serving mode (KB_READ_ONLY, issue #44) for horizontal read scaling. When set, the server registers only the read tools and read REST routes; the write/enforcement-write tools and routes (propose/resolve/bootstrap/consult/gate/record-event plus the observability GETs /api/v1/pending, /api/v1/audit, /api/v1/audit/verify, /api/v1/compliance) are not registered and return 404, and the write pipeline (worktrees/push queue/pending) is never initialised, so a replica is never a git writer. A new deploy/k8s/read-replica/ overlay (Deployment, Service, ConfigMap, NetworkPolicy, kustomization) runs N interchangeable read pods with per-pod ephemeral clone + index scratch and references a separate read-only git deploy key Secret (data-olympus-mcp-readonly). The replica logs an unambiguous "starting in READ-ONLY mode; write pipeline disabled" line at startup.

Fixed
  • Enforcement consult gates now agree on a worktree-invariant workspace key. The pre-tool gate and the pre-commit data-olympus report --staged gate derived the workspace differently (the hook from the detector or raw cwd, the report from Path.cwd().name), so a single kb_consult could not clear both gates from a linked git worktree. Both now resolve to the main worktree's basename (the first non-bare entry of git worktree list, correct for separate-git-dir and bare-repo layouts): report's default --workspace, and the hook's resolve_workspace, which now tries git first so it never disagrees with the report even when KB_WORKSPACES_ROOT resolves the linked worktree. A new kb-enforce-hook resolve-workspace [dir] prints the resolved key.

  • data-olympus lint (and discover_bundle_files) no longer discovers zero files when the bundle sits under an ancestor directory named like a skip-dir (for example a checkout under .worktrees/, .git/, or node_modules/). Skip-dir matching now applies only to path components inside the bundle, not the absolute ancestor path.

  • Intermittent 503 Service Unavailable from the single-replica MCP under load. The readiness probe was too aggressive (timeoutSeconds: 1) while every REST/enforcement handler ran synchronous work on the one asyncio event loop, so a burst stalled the probe and the only pod was ejected from the Service (nginx then served a 503 with an empty upstream, leaving no application-log trace). Fixes: relax the readiness probe (5s timeout, 5 failures) and raise the CPU limit to 2; offload blocking handler work to the anyio worker pool while serving /api/v1/health inline (off that pool) from a short-TTL cache of Index.health(); add locks to the audit log and consultation ledger for safe concurrent access.

  • The consultation ledger grew without bound (every (session, workspace) pair, the whole file rewritten on every consult). It now evicts entries past the consult TTL, enforces a hard max-entries cap, and bounds an oversized persisted ledger on load rather than holding it all in memory until the first prune.

  • data-olympus-mcp --help now prints usage and exits 0 instead of crashing with NotADirectoryError: KB root not a directory: /kb-main. Argument parsing runs before config loading, so the documented quickstart command works on first run.

  • Git sync failures are now visible instead of masquerading as a fresh no-change. kb_health / /api/v1/health expose last_git_fetch_status (changed / no_change / no_remote / fetch_failed / ff_failed), last_git_fetch_error, last_git_fetch_at, last_successful_refresh_at, and remote_head_sha. A fetch or fast-forward failure no longer advances the freshness marker, so staleness climbs and health degrades rather than reporting the KB as up to date against a broken or diverged remote. A deployment with no remote (read-only) is classified no_remote and stays healthy.

  • Write routes return structured errors instead of opaque 500s. In read-only mode (KB_REMOTE_URL unset) the write pipeline is disabled, and POST /api/v1/propose/*, /resolve/{id}, and /onboarding/bootstrap now return a 503 {"error":"write_pipeline_disabled"} (and the MCP write tools return {"status":"write_pipeline_disabled"}) instead of crashing on an internal assertion. Missing-field validation was extended to /api/v1/consult, /api/v1/gate/check, and /api/v1/resolve/{id}, so a request that omits a required field gets an actionable 400 rather than a KeyError-driven 500.

Security
  • Hardening from the companion security review:

    • Audit chain forgery fixed (blocker): verify() now tolerates unhashed legacy lines only as a prefix before the chain starts; any unhashed line after the first hashed event breaks verification, so an appended legacy-shaped record can no longer forge an event while keeping /audit/verify green.

    • Path containment tightened: safe_join_under_root() now also requires the resolved path to equal the lexical target, rejecting an in-tree symlink that redirects an allowed target_path to a different in-root file (which would decouple classification/blocklist/audit from where bytes land).

    • Body cap is real for chunked clients: the REST body limit is enforced by streaming the request and returning 413 past the cap, instead of trusting Content-Length.

    • MCP observability tools gated: kb_list_pending, kb_audit, kb_consult, kb_gate_check, and kb_compliance require an authenticated principal over MCP when auth is configured, matching the REST gating.

    • Aggregate caps: onboarding bootstrap rejects over KB_MAX_BOOTSTRAP_FILES (default 50), and KB_PENDING_QUEUE_CAP is now enforced in the pending queue.

    • Bootstrap confidence parse: /api/v1/onboarding/bootstrap returns a clean 400 on non-numeric confidence instead of a 500.

    • Compliance route gated: GET /api/v1/compliance now requires an authenticated principal when auth is configured, like the other observability routes (it was the one REST route still open).

    • Atomic bootstrap: a low-confidence bootstrap that would overflow the pending queue is rejected up front (and rolls back on a capacity race), so it never leaves a partial set of pending entries.

  • Observability routes are gated when auth is configured. With KB_AUTH_TOKEN set, GET /api/v1/pending, /api/v1/audit, and /api/v1/audit/verify now require an authenticated principal (they leak target paths and agent identities); they stay open when no auth is configured. bin/kb pending and kb audit send the bearer header automatically.

  • Deploy hardening for the sample Kubernetes manifests: the StatefulSet pins an immutable image tag (was data-olympus:latest), drops all Linux capabilities except those the root entrypoint needs before it gosu-drops to uid 65534, forbids privilege escalation, sets a RuntimeDefault seccomp profile, and runs a read-only root filesystem (writes go to the PVCs and a /tmp emptyDir). A new default-deny NetworkPolicy restricts ingress to the ingress controller and same-namespace clients, and the ingress gained cert-manager/TLS guidance. The release workflow no longer moves the latest channel on manual edge builds, so latest always means the last promoted release.

  • The audit log is now tamper-evident. Each appended event carries an event_id, the prev_hash of the previous event, and its own hash over the canonical body (SHA-256, or keyed HMAC-SHA256 when KB_AUDIT_HMAC_KEY is set). Any later edit, deletion, or reordering breaks the chain. A new GET /api/v1/audit/verify route and kb audit --verify recompute and report the first broken line. Legacy unhashed lines are tolerated, so enabling the feature on an existing log does not retroactively flag it.

  • Resource limits bound write-side abuse. New configurable caps reject oversized proposals before any disk side effect: KB_MAX_TEXT_BYTES (memory text, default 256 KiB), KB_MAX_POSTIMAGE_BYTES (edit/bootstrap file, default 1 MiB), and KB_MAX_BODY_BYTES (REST request body via Content-Length, default 2 MiB, returns 413). The sliding-window rate limiter gained an optional per-IP cap (KB_RATE_LIMIT_PER_IP_PER_HOUR, default 0 = disabled) so a client cannot multiply its quota by varying agent_identity.

  • Identity + capability authorization unifies MCP/REST write auth, per-agent policy, and a confidence clamp. A Principal is resolved from the Authorization: Bearer header against a registry built from KB_AUTH_TOKEN (a full-capability operator) and an optional KB_AUTH_PRINCIPALS JSON list of per-agent tokens with explicit capabilities (read, propose, auto_commit, resolve, bootstrap, record_event).

    • MCP write tools are now authenticated. A FastMCP middleware enforces the same capabilities on kb_propose_*, kb_resolve_pending, kb_bootstrap_project, and kb_record_event that the REST layer enforces, closing the gap where MCP write tools bypassed KB_AUTH_TOKEN.

    • Confidence is clamped for non-privileged callers. A principal lacking the auto_commit capability has its proposals parked as pending regardless of the client-asserted confidence, so a caller can no longer self-assert confidence: 1.0 to skip operator review. REST returns 401 for anonymous and 403 for an authenticated principal missing a capability.

    • When no auth is configured (no token, no principals) every caller is the fully-trusted local principal, preserving the prior trusted-local behavior.

  • Path containment is now enforced uniformly across every write path. A new shared safe_join_under_root() guard rejects any proposed write whose resolved location escapes the per-session worktree (symlink, traversal, or absolute path). This closes a memory-proposal symlink-escape where a malicious KB commit could plant memory/inbox as a symlink and cause a high-confidence proposal to write a file outside the worktree before git add aborted. The edit, resolve, and onboarding-bootstrap paths now use the same guard. Regression tests cover the helper directly and each write path.

  • Cleanup-plan input validation is now centralized in kb_cleanup_plan_fn itself, so both POST /api/v1/onboarding/cleanup-plan and the MCP kb_cleanup_plan tool are protected identically (previously only the REST route validated, and the MCP tool called the function directly, unvalidated). The shared function now rejects a non-list local_files, a non-object entry, a non-string path, and a non-string content (including an explicit content: null, which used to slip through .get() and crash later). It also enforces a per-file max_content_bytes cap, an aggregate max_files cap, and requires jaccard_threshold to be a finite number in [0.0, 1.0] (rejecting nan, inf, and out-of-range values). REST and MCP callers pass the existing KB_MAX_BOOTSTRAP_FILES / KB_MAX_POSTIMAGE_BYTES caps through to the shared function.

Changed
  • uv run mypy src is now green and runs as a CI gate (added types-PyYAML, fixed the source type errors). Type regressions in src/ now fail CI; tests/ typing is intentionally out of scope.

  • The path-to-(tier, category) taxonomy now ships a deployment-neutral default and is configurable at deploy time, with no code change: KB_TAXONOMY_PATH (a JSON file of [prefix, tier, category] triples that replaces the default table), KB_INDEXED_PREFIXES (comma-separated writable top-level prefixes that replace the default set), and KB_MEMORY_INBOX_PREFIX (directory new memory proposals are written under, default memory/inbox/). tech-stacks/<stack>/ is now classified dynamically as stack:<stack> rather than from a fixed allow-list. Deployments that relied on the previous built-in directory names must set these variables to preserve prior classification and writable paths.

Fixed
  • The enforcement hook now reports the correct per-agent identity to the consult audit. Previously every agent (Claude, Codex, Gemini) was recorded as claude-code, so the kb_compliance per-agent view was wrong for Codex and Gemini. The kb-enforce-hook dispatcher gained an --agent flag and each provider threads its own identity.

  • data-olympus lint no longer false-greens when its file discovery matches nothing. The summary now reports how many concept files were actually linted (e.g. 0 errors across 0 files (9 linted)), and the command exits non-zero when a bundle has no concept files to lint, so a broken or over-broad skip walk surfaces as a red CI gate instead of a silent pass. Reserved files (index.md, log.md, template.md) are exempt from the concept schema and so are excluded from the linted count, preventing a bundle that kept only its generated indexes from passing the guard. File discovery is now exposed as discover_bundle_files for direct testing.

0.1.1 - 2026-06-24

Added
  • data-olympus lint skips repo-meta directories and root-level meta files.

  • CI builds and publishes a multi-arch (amd64 + arm64) container image to GHCR on release.

Fixed
  • Bumped fastmcp to 3.x to close Dependabot-reported CVEs.

  • Fixed the broken OKF link (now points to the knowledge-catalog repository).

0.1.0 - 2026-06-24

Added

Format (OKF-compatible profile)

  • Governance-grade knowledge-base format as a conformant extension of the Open Knowledge Format.

  • Controlled frontmatter fields: id (stable slug), type (decision, standard, workflow, project, component), status (draft, proposed, accepted, deprecated, superseded), tier (T1-T4 plus freeform), and supersedes chain for tracing decision history.

  • Bundle layout: decisions/, universal/, tech-stacks/, projects/, workflows/, log.md, plus per-prefix index.md generated by the index command.

  • SPEC.md: format specification covering bundle layout, frontmatter schema, reserved filenames, and serving contracts.

MCP server (single-writer, streamable HTTP)

  • Streamable-HTTP MCP server (data-olympus-mcp) backed by FastMCP.

  • Read tools: kb_search, kb_get, kb_list, kb_health, kb_outline.

  • Write pipeline: kb_propose_memory, kb_propose_edit, kb_resolve_pending with pending-confirmation flow, path blocklist (KB_WRITE_BLOCK_TIERS, KB_WRITE_BLOCK_PATHS), and structural write rules.

  • Onboarding tools: kb_onboarding_status, kb_bootstrap_project.

  • Audit tools: kb_audit with JSONL event log.

  • Single-replica advisory locking and durable push queue to prevent concurrent write races.

  • Per-session worktree isolation for in-flight proposed edits.

  • Health endpoint with degraded/healthy state.

  • Optional bearer-token authentication for the write routes via KB_AUTH_TOKEN. When set, POST /api/v1/propose/memory|propose/edit|resolve/{id}|onboarding/bootstrap require Authorization: Bearer <token> (constant-time compared with hmac.compare_digest); a missing or wrong token returns 401. Read routes stay open. When unset (the default), there is no auth. The bin/kb CLI sends the header automatically (via a curl config FD, never in argv) when KB_AUTH_TOKEN is set. There is otherwise no built-in auth, so operators should still deploy behind a trusted network or authenticating reverse proxy. See SECURITY.md.

data-olympus CLI

  • data-olympus lint <bundle>: validates bundle conformance against the format spec; exits non-zero on errors (warnings are informational only).

  • data-olympus index <bundle>: regenerates index.md files for each prefix in the bundle.

  • data-olympus visualize <bundle>: produces a self-contained Cytoscape-backed HTML graph of document relationships and cross-links (adapted from the OKF visualizer). Bundle markdown is sanitized with DOMPurify and the CDN scripts are pinned with SubResource Integrity (SRI) hashes.

kb bash REST client (bin/kb)

  • REST client over the MCP server's HTTP API.

  • Subcommands: kb search, kb get, kb list, kb health, kb outline.

  • Write subcommands: kb propose memory, kb propose edit, kb resolve, kb pending, kb audit.

  • Onboarding subcommands: kb onboard status, kb onboard project, kb onboard component, kb onboard rename, kb onboarding-check.

  • Workspace auto-detection via bin/_kb_detect_workspace.sh (resolves workspace and component from git remote of the current working directory).

Example bundle

  • example-bundle/: a multi-tier example with decisions (ADR-001, ADR-002), universal standards, a backend-nestjs tech-stack stub, an acme-app project stub, a workflow, and a log.md.

  • Pre-generated index.md files and viz.html graph for the example bundle.

Deploy

  • deploy/docker/: Dockerfile, compose.yaml, and entrypoint.sh for local Docker deployment.

  • deploy/k8s/: Kustomize manifests (StatefulSet, Service, Ingress, ConfigMap, Namespace) for Kubernetes deployment.

  • deploy/k8s/secret.template.yaml: template (not real credentials) for the SOPS-encrypted Secret.

Documentation

  • docs/quickstart.md: verified local-run procedure with curl and kb CLI examples.

  • docs/serving.md: single-replica serving model, read-only replicas, and git pull loop.

  • docs/adoption.md: bring-your-own-KB guide (author, lint, index, serve, wire an agent).

  • docs/comparison.md: how data-olympus relates to OKF, enterprise catalogs, markdown KB tools, agent-context conventions, RAG, and ADR tooling.

512 files · 486 not served here

Advantages

  • Local-first: the memory stays in files or a database on your own machine (a rule-derived fact from the brain’s files, evidence: README.md)
  • Works with 14 MCP clients (a rule-derived fact from the brain’s files, evidence: README.md)
  • Plain markdown memory that you can read and edit yourself (a rule-derived fact from the brain’s files, evidence: README.md)
  • No API key is named in its files (a rule-derived fact from the brain’s files, evidence: README.md)
  • Open licence (Apache-2.0) (a rule-derived fact from the brain’s files)

Limitations

  • No ZIP download: install it from its source (a rule-derived fact from the brain’s files)

Security

Risk level: Low risk

This brain runs

Agent instructions that run or fetch something (5)

  • AGENTS.md: `uv run pytest` on its own is not equivalent.
  • AGENTS.md: `tests/test_server_cli.py` and `tests/test_smoke.py` spawn a subprocess with `sys.executable` and read installed distribution metadata, so without the editable install they fail with `No module named data_olympus.server` and a stale version assertion.
  • AGENTS.md: Anything that gates on `uv run pytest` without the editable install (a release routine, a scheduled job) will block on a false failure.
  • AGENTS.md: Run the linter before the tests.
  • tests/importer-fixtures/.cursorrules: Run the type checker in CI and treat type errors as build failures rather than warnings.
  • Low risk: network-instructions (AGENTS.md:75)
  • Low risk: scripts-present (scripts/check_benchmark_docs.py)
  • Low risk: scripts-present (scripts/okf_conformance.py)

Attribution and licence

Created by
Knaisoma
Declared authors
  • knaisoma (declared in pyproject.toml)
Publisher
Knaisoma (Official Knaisoma project)
Source
knaisoma/data-olympus · commit 6d46033
Licence
Apache License 2.0 (files served here)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Knaisoma.

Install from source

486 of its files are not served here, so there is no ZIP. knaisoma/data-olympus at 6d46033