# Changelog All notable changes to Agnes AI Data Analyst. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html), pre-1.0 — public surface (CLI flags, REST endpoints, `instance.yaml` schema, `extract.duckdb` contract) may shift between minor versions; breaking changes called out under **Changed** or **Removed** with the **BREAKING** marker. CalVer image tags (`stable-YYYY.MM.N`, `dev-YYYY.MM.N`) are produced for every CI build; semver tags (`v0.X.Y`) are cut at release boundaries and reference the same commit as a `stable-*` tag from the same day. --- ## [Unreleased] ## [0.54.26] — 2026-05-18 ### Changed - **BREAKING:** eight `DELETE` endpoints that previously returned `200` with a JSON body now correctly return `204 No Content` (HTTP semantics for idempotent removal). External clients that parsed the response body (e.g. `r.json()["status"]`) will hit JSON-decode errors against the now- empty payload and must drop the body read: `DELETE /api/admin/metrics/{id}`, `DELETE /api/memory/{id}/dismiss`, `DELETE /api/store/entities/{id}`, `DELETE /api/store/entities/{id}/install`, `DELETE /api/marketplace/curated/{marketplace}/{plugin}/install`, `DELETE /api/marketplaces/{marketplace}/plugins/{plugin}/system`, `DELETE /api/admin/store/submissions/{id}`, and `DELETE /api/admin/observability/views/{id}`. - **BREAKING:** `POST /api/memory/admin/contradictions` now returns `201 Created` instead of `200 OK` on success (creator-POST contract). ### Internal - Added `tests/test_api_design_rules.py` — four forward-only design guardrails that prevent new endpoints from adding to existing REST debt: no new verbs in URL paths, `DELETE` must declare 204, creator `POST`s must declare 201, and all protected `/api/*` routes must declare 401 and 403. - `_add_auth_error_responses()` injected into `app.openapi()` at startup to declare 401/403 on all protected `/api/*` operations centrally — 220 ops now carry the auth-error responses in the spec. ## [0.54.25] — 2026-05-18 ### Fixed - `POST /api/sync/table-subscriptions` now enforces the same RBAC gate as `POST /api/sync/settings` — authenticated users can no longer subscribe to tables they have no `resource_grants` row for (ADV-001, issue #336). - **BREAKING:** `GET /webhooks/jira/health` is now admin-only; `jira_domain` removed from the response to prevent anonymous information disclosure (ADV-002). Uptime monitors that polled this endpoint anonymously must now attach an admin PAT or switch to `/api/health` (which remains public). - **BREAKING:** `GET /api/version` no longer exposes `commit_sha` or `schema_version` — only `version`, `channel`, `image_tag`, `deployed_at` remain (ADV-003). Deploy scripts / dashboards scraping the removed fields must either authenticate against a (separate, forthcoming) admin endpoint or read them from the GHCR image labels. - **BREAKING:** `/docs`, `/redoc`, and `/openapi.json` now require a valid session — the full admin API surface is no longer visible to unauthenticated requests (ADV-005). CLI tools generating client code from the schema must attach a PAT or use an authenticated browser session. ### Changed - `/cli/` and `/webhooks/` prefixes added to `_API_PATH_PREFIXES` so any future auth-gated endpoint under those paths returns JSON `401` rather than an HTML redirect (ADV-006). - `GET /api/users` and `GET /auth/admin/tokens` accept `limit` (default 1000, max 10 000) and `offset` query parameters; `POST /api/sync/table-subscriptions` now rejects `tables` dicts with more than 500 entries (ADV-008, ADV-009). - `GET /api/catalog/tables` now has a typed `response_model` (`CatalogTablesResponse`) so Swagger generates an accurate schema for that endpoint (ADV-007). ### Internal - Added `TestFullLifecycleFromInstaller` integration test class (`tests/test_store_entity_versions.py`) covering the full flea-market lifecycle from issuer / admin / subscribed-user perspectives. Main test walks v1 upload → installer subscribes → v2 promote → v3 blocked → admin force-overrides → restore v1, asserting BOTH entity state AND served `marketplace.zip` bytes + ETag at each transition. Plus 5 corner cases: unsubscribed-user negative control, late-subscriber-during- quarantine, non-owner privacy gate, second-restore reuse path (PR #332 lifecycle validation), and archived-entity-keeps- serving-installs (CLAUDE.md contract). ## [0.54.24] — 2026-05-16 ### Fixed - Flea-market admin submissions UI now derives the per-submission `v#` label by **submission_id**, not **hash**. Hash-based lookup mislabeled every byte-identical reupload (and every reused-verdict restore — common after the restore-reuse fix below) as `v1` because the loop picked the FIRST history entry with matching hash. Affected both the admin queue column (`v#`) and the per- section chips on the detail page. Same fix pattern as PR #330 (runner / override paths). - Flea-market admin submission detail page gained a version-switcher card listing every submission linked to the same entity with status badge + reviewed_by_model + click-to-jump. Lets admins compare verdicts across versions without bouncing back to the queue. - Flea-market initial POST now backfills the v1 seed entry's `submission_id` immediately after creating the v1 submission row. Pre-fix the v1 history entry always carried `submission_id=None` so downstream lookups (`_version_no_for_submission`, admin queue v#, admin detail chip, restore-reuse) silently failed for v1. - Flea-market restore endpoint now reuses the prior approved submission's LLM verdict when the restored bundle is byte-identical to a history entry already reviewed by the same `review_model`. Pre-fix every restore re-ran the LLM; Anthropic structured output is non-deterministic — same bytes flipped `content_quality.verdict` pass↔fail across calls, so a second restore of an already-approved version could spuriously land at `blocked_llm`. Reuse skips the LLM, stamps the new submission with the prior verdict + `reused_from_submission_id` marker, and saves the Anthropic token cost. Surfaced live on a development deployment where the third restore of a v1 bundle (same hash as v1/v2/v4/v6 — multiple identical re-uploads) landed `blocked_llm` while sibling submissions were `approved`. - Admin submission detail page now surfaces `llm_findings.content_quality.issues` in its own table next to the security-findings table. Pre-fix the template only rendered security findings, so a submission blocked purely on `content_quality.verdict='fail'` (no security findings) showed up as "No findings — model verdict was clean" even though `status='blocked_llm'`. Also adds an explicit "Blocked but no findings recorded" notice when the verdict is blocked but neither findings list is populated (transient LLM non-determinism), pointing admin at Rescan / Override. Reuse markers (`reused_from_submission_id`) render too. ## [0.54.23] — 2026-05-16 ### Fixed - Flea-market admin **Rescan** of a non-current v2+ submission with `guardrails.enabled: false` now promotes the entity forward (mirrors the inline-promote in create / update / restore). Pre-fix the branch flipped submission status to `approved` and entity visibility to `approved` but never called `promote_to_version` — the rescan re-approved the version without making it current. Codex adversarial-review follow-up on PR #330. The guardrails-on path is unchanged (rescan schedules an LLM review; promotion lands when the verdict approves through `runner.run_llm_review`). ## [0.54.22] — 2026-05-15 ### Fixed - **Flea-market — promote-on-approve + admin-override now look up the submission's `version_no` in `version_history` by `submission_id`, not by `hash`.** Hash-based lookup broke whenever the user uploaded byte-identical bundles across versions (e.g. same content as v2 and v4): the loop matched the FIRST history entry with that hash — always v1 — so `target_version_no` landed at 1, the forward-only `target > current` guard skipped the promote, and the entity stayed stuck at v1 even though the new submission was `status='approved'`. UI kept showing v1 as "current". Both `runner.run_llm_review` (background auto-approve) and `admin_override_store_submission` now reuse the existing `_version_no_for_submission` helper. Closes the live development- deployment case where an entity had 5+ identical-hash history rows. - **Admin "ask telemetry" feature** (`POST /api/admin/telemetry/ask`) no longer emits SQL against the dropped `usage_plugin_daily` table. `src/usage_ask.py` `SCHEMA_DIGEST` and `SYSTEM_PROMPT` bumped to describe the v48 rollups (`usage_marketplace_item_daily` / `_window`) and rule 5 of the prompt updated. Pre-fix, the LLM would happily emit `SELECT … FROM usage_plugin_daily` per the stale prompt and the DuckDB binder would reject it. ### Internal - **CHANGELOG `[0.54.20]` section restored to its canonical content from the `v0.54.20` git tag.** The #329 self-merge had carried 226 lines of author's pre-rebase bullets that ended up mis-attributed to `[0.54.20]`; the published v0.54.20 GitHub Release (FTS BM25 + batch bar) now matches the CHANGELOG section verbatim. - `tests/conftest.py` — dropped the unused `conn_with_usage_schema_and_attribution` fixture that seeded into the now-removed `usage_attribution_*` tables. Zero callers today but a tripwire — the first future test to request it would have failed with a DuckDB binder error. - `app/web/templates/marketplace.html` — replaced a customer- specific token (`groupon-marketplace`) in the Most Popular sort- tiebreaker comment with a generic `-marketplace` placeholder per `CLAUDE.md § Vendor-agnostic OSS`. ## [0.54.21] — 2026-05-15 ### Added - **Marketplace — flea inner skill/agent detail page parity with curated.** New backend endpoints `GET /api/marketplace/flea/{id}/skill/{name}` and `…/agent/{name}` plus matching web routes that render `marketplace_item_detail.html`. Stack-install is blocked on inner items (same rule curated has had since launch — "Open parent plugin →" button + helper text instead). Breadcrumb: `Marketplace › Flea Market › `. - **Marketplace — funnel chip + Most adopted sort + listing polish.** The funnel chip (`N active · N calls · ±X% trend · N installed`) now lives on the marketplace cards, plugin detail hero, inner detail hero, AND the inner skill/agent cards on the parent plugin detail page. New `Most adopted (30d)` sort; deterministic Most Popular ordering. Trending sort hidden when no trend data. Breadcrumb second segment is now a generic clickable `Curated Marketplace` / `Flea Market` link instead of the opaque per-instance marketplace name. Flea sidebar uses `Owner` label (vs `Curator` on curated); flea-inner sidebar mirrors curated nested layout (Parent plugin / Bundle size / Active days / Last used / Owner). ### Changed - **BREAKING:** `MarketplaceItem.unique_users_30d` renamed to `distinct_users_30d` in the `/api/marketplace/items` response. The new value is a true distinct count across the 30-day window (from the `usage_marketplace_item_window` snapshot), not the old sum-of-daily proxy that over-counted active multi-day users. - `usage_events.source` is now populated per-event by `MarketplaceItemLookup` (live join against `marketplace_plugins` + `store_entities`). Previously it sat at `'builtin'` for every row because the v42 `AttributionLookup` matched skill/command names without the plugin prefix that Claude Code actually writes — `usage_events.source = 'curated'` / `'flea'` / `'builtin'` becomes meaningful for the first time. `usage_events.ref_id` semantics shift in lockstep — curated stores the plain plugin name, flea stores `NULL`. - `USAGE_PROCESSOR_VERSION` bumped 5 → 6 so the session-pipeline reprocess loop re-attributes historic events on next tick. - `_build_telemetry` returns `None` (not a zero-shape dict) when `invocations_30d == 0`, so detail endpoints can omit the chip payload entirely. The frontend hero / sidebar are already None-safe (`d.telemetry || {}` guard, `if (!d.telemetry || …)` on daily_series). ### Removed - **BREAKING:** four schema-v42 telemetry tables (v48 migration): - `usage_attribution_skills`, `usage_attribution_agents`, and `usage_attribution_commands` — replaced by live prefix-split lookup against `marketplace_plugins` + `store_entities`. Verified empty or derivable; no unique data lost. - `usage_plugin_daily` — replaced by `usage_marketplace_item_daily` + `_window`. Verified empty in production-shape data (the v42 rollup `INSERT` was gated on `source IN ('curated','flea')` but the broken attribution layer always produced `'builtin'`). - `src/repositories/usage_attribution.py`, `src/usage_attribution_helpers.py`, `scripts/backfill_usage_attribution.py`, and their test fixtures (`tests/test_usage_attribution.py`, `tests/test_backfill_usage_attribution.py`) — no callers remain. ### Internal - **Schema v48** — marketplace telemetry refactor. `usage_marketplace_item_daily` (per-day fact with count + distinct_users + error_count, keyed by `(day, source, type, parent_plugin, name)`) and `usage_marketplace_item_window` (sliding-window snapshot, labels `last_7d` refreshed every UsageProcessor tick, `last_30d` refreshed hourly) replace the dropped v42 attribution + plugin-daily tables. Auto-migrates on first boot; fresh installs receive the new tables via `_SYSTEM_SCHEMA`. The migration was renumbered v45→v46 → v47→v48 on rebase since the v46 / v47 slots were already taken by #316 (per-user dismiss) and #326 (FTS BM25 index). - `scripts/backfill_marketplace_rollup.py` — one-shot script to populate the new rollup tables from historic `usage_events` after a v48 deploy. - **Repo-committed Claude Code agents + skills under `.claude/`.** Four knowledge skills (`agnes-orchestrator`, `agnes-rbac`, `agnes-connectors`, `agnes-release-process`) auto-load into the main agent's context when their description matches the work or are invokable explicitly via `Skill()`. Four specialist subagents (`agnes-reviewer-rules`, `agnes-reviewer-rbac`, `agnes-reviewer-architecture`, `agnes-releaser`) wire into the Agent tool — reviewers fire in parallel at the end of PR work; the releaser handles pre-merge release-cut + post-merge tag / GitHub Release. `.gitignore` un-ignores `.claude/agents/` and `.claude/skills/` while keeping the rest of `.claude/` local-only. Source of truth for the rules these encode remains `CLAUDE.md` + `docs/RELEASING.md`. Design rationale + implementation plan: `docs/superpowers/specs/2026-05-15-agnes-agents-design.md` and `docs/superpowers/plans/2026-05-15-agnes-agents.md`. ## [0.54.20] — 2026-05-15 ### Added - **Corporate Memory — BM25 relevance ranking on knowledge search.** Replaces the `title ILIKE '%q%' OR content ILIKE '%q%'` ranked-by-insertion-order query in `KnowledgeRepository.search` with DuckDB's `fts` extension (BM25). Czech queries match across diacritics (`cesky` → `česky`) via `strip_accents=1` + `lower=1`. Schema v47 builds the initial index over `knowledge_items(title, content)`; per-mutation rebuild fires only when `title` / `content` change (status flips skip). The lifespan in `app/main.py` rebuilds once at boot as a safety net for restarts on v47. Result rows now carry a `bm25_score` column (always present — `None` on the ILIKE fallback for shape uniformity). When the `fts` extension can't be loaded (offline / sandboxed install) **or** the index is missing (migration soft-fail, concurrent `overwrite=1` rebuild's drop-then- create window), `search` and `count_items` transparently fall through to the pre-#121 ILIKE query — same result-set membership, ordering regresses to `updated_at DESC`. Closes #121. - **Corporate Memory — bulk-edit batch bar on the All Items tab.** Symmetric to the Review-tab bar shipped in #126; row checkboxes, "Select all" header, and the five bulk-edit actions (Move to category / Move to domain / Add tag / Remove tag / Set audience) now appear on `/corporate-memory-admin` All Items as well. Approve / Reject stay scoped to Review per #129's scope decision (status flips belong with the per-row actions or the keyboard workflow). Closes #129. ### Internal - **Schema v47** — adds DuckDB `fts` BM25 index over `knowledge_items(title, content)`. Auto-migrates on first boot; soft-fails to ILIKE if the extension repo is unreachable. Index is a snapshot — see `src/fts.py` for the on-mutation / lifespan rebuild contract. ## [0.54.19] — 2026-05-15 ### Changed - `connectors/jira/scripts/consistency_check.py` — `AUTO_FIX_THRESHOLD` bumped from 10 to 20. Auto-backfill now covers typical SLA-poller hiccups before escalating to ERROR. `WARNING_THRESHOLD` unchanged. ### Fixed - **`connectors/jira` — transient Jira API failure no longer wipes existing `remote_links` parquet rows.** Pre-fix, all three `fetch_remote_links` sites (`service.py`, `scripts/backfill.py`, `scripts/backfill_remote_links.py`) silently returned `[]` on 401/403/429/5xx or `httpx.RequestError`. Callers overlaid that `[]` onto cached issue JSON, and `transform_remote_links` interpreted the empty list as "issue legitimately has no remote links — delete its existing rows", so a transient Jira auth blip (or a webhook burst hitting Jira's rate limiter) permanently wiped remote-link history. Now: every fetch site raises `JiraFetchError` on non-200/non-404 status and `httpx.RequestError` (including the "service not configured" path — a webhook arriving while API creds are missing no longer surfaces as a silent wipe), overlay sites skip the `_remote_links` key on raise (leaving it ABSENT, not present-but-empty), and `transform_remote_links` returns `None` for absent / `null` keys (preserve existing rows) vs `[]` (legitimate empty — wipe). Both consumers (batch `transform_all` and incremental `transform_single_issue`) honor the new contract. End-to-end tests lock both halves: `test_incremental_preserves_remote_links_when_overlay_absent` + `test_incremental_wipes_remote_links_when_overlay_present_but_empty`. The bulk-backfill scripts retain their existing `Retry-After` sleep+retry loop for 429 (appropriate for non-interactive batch contexts); only the webhook hot path raises on 429. ### Internal - `CLAUDE.md` — `connectors/jira/transform.py` removed from the "Files NOT to modify" list. The `_remote_links` hardening required modifying `transform_remote_links` and `transform_all` to honor the new "overlay absent → preserve existing rows" contract; the module remains sensitive (touch only with end-to-end understanding of the JSON-overlay / parquet-rewrite pipeline) but is no longer off-limits. ## [0.54.18] — 2026-05-15 ### Added - "Curated Memory" now sits in the primary navigation next to Data Packages, visible to every authenticated user. - **Per-user Dismiss** for Curated Memory items — analysts can opt-out of approved items from their AI-agent bundle and gray them out on `/corporate-memory`. Schema v46 adds `knowledge_item_user_dismissed`; new endpoints `POST /api/memory/{id}/dismiss` and `DELETE` (idempotent). **Mandatory items can never be dismissed** — the governance hard rule is enforced at two layers (API rejects with 400, SQL filter exempts mandatory rows even if a stale dismissal exists). `GET /api/memory` gains `hide_dismissed=false` (default off — dismissed items still visible with a badge + Undismiss button) and per-item `dismissed_by_me` flag. `GET /api/memory/bundle` always excludes dismissed items. - **"My Upvotes" filter** on `/corporate-memory` replaces the old dead "My Rules" sentinel. Backed by a new `?upvoted_by_me=true` filter on `/api/memory` that subquerying against `knowledge_votes`. - **Inline tag typeahead** in the admin edit modal — focus the tag input to browse all existing tags as a dropdown, type to filter (case-insensitive), ↑/↓ + Enter to add as pill, type a fresh value to surface "+ Add new tag: ". Tags now render as removable pills (× to remove); Backspace on empty input pops the last pill. - **Bulk-edit modal pickers** for `/admin/corporate-memory` — Category, Audience, and Add tag get `