# Changelog

All notable changes to Armature are documented here.

## v0.9.4 — Package Adoption + Roles

Packages can now declare `adoptable: true` in their manifest. When a team
adopts an adoptable package, a team-scoped copy is created that references
the original (shared assets, no disk duplication). The package's
`requires_roles` auto-populate into a new `team_role_catalog` table so
team admins know which roles to assign.

**Schema**

- Migration 017: `adoptable` and `adopted_from` columns on `packages`.
  `team_role_catalog` table (team_id, role, source_package_id) with
  unique constraint. Both Postgres and SQLite dialects.

**Store + Models**

- `PackageRegistration`: `Adoptable bool`, `AdoptedFrom *string`.
- `PackageStore`: `ListAdoptable()`, `GetByAdoptedFrom()`.
- `TeamRoleCatalogEntry` model struct.
- `TeamStore`: `AddRoleToCatalog`, `ListRoleCatalog`,
  `RemoveRoleCatalogBySource`.

**Handlers**

- `POST /teams/:teamId/packages/:id/adopt` — adopt a global adoptable
  package into the team. Creates team-scoped registration, clones
  workflow if applicable, populates role catalog. Idempotent.
- `GET /teams/:teamId/packages/adoptable` — list available packages
  with adoption status per team.
- `DELETE /teams/:teamId/packages/:id/unadopt` — remove adopted package
  and clean up role catalog entries.
- `GET /teams/:teamId/roles/catalog` — list known roles for a team.

**Manifest Validation**

- `adoptable: true` parsed as `ManifestInfo.Adoptable`.
- Rejected on `library` and `test-runner` types.
- Persisted to DB on package install.

**Deprecation**

- `POST /teams/:teamId/workflows/:id/adopt` (`AdoptTeamWorkflow`) now
  returns `X-Deprecated` header and logs a deprecation warning.
  Use the package-level adoption endpoint instead.

**Tests**

- 11 new tests: 5 manifest validation, 3 role catalog store, 3 handler
  integration (adopt success, idempotent, non-adoptable rejection).

---

## v0.9.3 — Team User Roles

Promotes the team role system from a single-role-per-member model to a
many-to-many relationship, enabling users to hold multiple roles within
a team simultaneously.

**Schema**

- Migration 016: `team_user_roles` table (team_id, user_id, role) with
  unique constraint and compound index. Both Postgres and SQLite dialects.

**Store + Models**

- `TeamUserRole` model struct.
- 6 new `TeamStore` methods: `AddUserRole`, `RemoveUserRole`,
  `ListUserRoles`, `GetMemberRoles` (union of primary + additional),
  `HasRole`, `RemoveAllUserRoles`.
- Implementations for both Postgres and SQLite stores.

**Middleware**

- `RequireRole(teams, roles, stores)` — kernel middleware that checks
  whether the user holds at least one of the required roles (OR semantics).
  System admin bypass via permissions.

**Handlers**

- `GET /teams/:teamId/members/:memberId/roles` — list full role set.
- `POST /teams/:teamId/members/:memberId/roles` — assign additional role.
- `DELETE /teams/:teamId/members/:memberId/roles/:role` — remove role.
- `RemoveMember` handler now cleans up `team_user_roles` on member removal.

**Manifest**

- `requires_roles` field parsed from package manifests (advisory in v0.9.3;
  extensions gate via `teams.has_role()` in Starlark).

**Starlark SDK**

- New `teams` module wired into sandbox runner:
  - `teams.get_member_roles(team_id, user_id)` → list of strings.
  - `teams.has_role(team_id, user_id, role)` → True/False.

**Admin UI**

- Team-admin members page: removable badge chips for additional roles,
  "+ Role" dropdown for assignment.
- Fixed pre-existing SDK auto-unwrap bug in `loadRoles` / `loadMemberRoles`.

**Tests**

- 10 new tests: store CRUD (add, idempotent, has_role, remove, removeAll),
  middleware (allowed, denied), manifest parsing (valid, empty, invalid).

## v0.9.2 — Starlark Converter Consolidation + Snapshot Cleanup

Consolidates duplicated Go↔Starlark conversion code and snapshot
parsers into canonical locations, removing ~350 lines of copy-paste
across 8 files.

**Converter consolidation**

- New `sandbox/convert.go` with four exported functions:
  `GoToStarlark`, `StarlarkToGo`, `DictToMap`, `MapToDict`.
- Superset implementation handles all Go primitive types (nil, bool,
  int, int64, float64, string), containers (map, slice), and Starlark
  Tuple — covering every variant that previously existed.
- Deleted duplicate converters from `workflow/automated.go`,
  `handlers/starlark_helpers.go`, `handlers/workflow_hooks.go`,
  `sandbox/workflow_module.go`, `sandbox/realtime_module.go`,
  `sandbox/files_module.go`, `triggers/event.go`, `triggers/webhook.go`.
- SQL-specific converters in `db_module.go` (error-returning, `[]byte`
  handling) intentionally excluded — different semantics.

**Snapshot parser consolidation**

- New `models/snapshot.go` with `ParseSnapshotStages()` handling both
  wrapped `{"stages":[...]}` and legacy flat `[...]` formats.
- Deleted three identical parsers from `workflow/engine.go`,
  `handlers/workflow_instance_handlers.go`, and
  `handlers/workflow_assignment_handlers.go`.

**Snapshot format standardization**

- `workflow_packages.go` publish path now emits wrapped format,
  matching `workflows.go`. All snapshot creation is consistent.

**Tests:** 7 new converter tests with round-trip coverage. Existing
engine, handler, and workflow tests updated and passing.

---

## v0.9.1 — Server-Side Sub-Path Routing

Hardens multi-surface routing so full-page refreshes on sub-paths work
correctly server-side, with early auth optimization and back-button
resilience.

**Route consolidation**

- Root (`/s/:slug`) and catch-all (`/s/:slug/*path`) handlers now share
  a single `dispatch` function. The root handler injects `path="/"` and
  delegates, eliminating the separate code path.

**Early auth short-circuit**

- New `aggregateAccess()` function scans all surfaces to determine
  whether a package is all-public, all-authenticated, or mixed.
- All-authenticated packages redirect to login before surface matching,
  skipping `matchSurface()` + `evaluateAccess()` overhead.
- Mixed-access packages fall through to per-surface checks as before.

**SDK back-button fix**

- On boot, `history.replaceState()` seeds the initial history entry
  with `__SURFACE_PATH__` and `__SURFACE_PARAMS__`. This fixes
  back-button navigation from `sw.navigate()` to the initial
  server-rendered view.
- SDK version bumped to `0.9.1`.

**Tests: 13 new**

- 5 `aggregateAccess` unit tests (all-public, all-auth, mixed,
  default-access, single-public).
- 8 handler integration tests covering root refresh, sub-path static
  and param refresh, unauth redirect, public anonymous, mixed-access,
  non-matching sub-path, and early auth short-circuit.

**Modified files:**

- `server/pages/pages.go` — `aggregateAccess()`, route consolidation,
  early auth short-circuit in `RenderExtensionSurface`
- `server/pages/pages_surface_match_test.go` — 5 new unit tests
- `server/pages/pages_handler_test.go` — 8 new integration tests
- `src/js/sw/sdk/index.js` — `history.replaceState()` seed, version bump

## v0.9.0 — Multi-Surface Packages

Packages can now declare multiple surfaces — each with its own path, access
level, title, and layout. A single package can serve a public submission
form, an authenticated dashboard, and an admin settings page. The kernel
resolves incoming requests to the correct surface and enforces access
server-side.

**Manifest: `surfaces` array**

- Packages declare a `surfaces` array with entries like
  `{ "path": "/submit", "access": "public", "title": "Report a Bug" }`.
- Each surface has independent `path`, `access`, `title`, `layout`, and
  `nav` fields. Package-level `auth` and `layout` serve as defaults.
- Supported access levels: `public`, `authenticated`, `admin`, `group:{name}`.
- Packages without `surfaces` get auto-synthesis from legacy `auth`/`layout`
  fields — no existing packages break.

**Kernel: unified route tree**

- Surface handler and ext API handler share a single `/s/:slug` route tree
  via `RegisterExtensionRoutes`. The dispatcher checks the path prefix:
  `/api/*` → ext API handler with JWT auth; everything else → surface
  handler with per-surface access checks.
- `matchSurface()` resolves request paths against surface patterns with
  Gin-style `:param` support. Static segments preferred over params.
- `evaluateAccess()` checks access requirements per-surface, with login
  redirect for unauthenticated users and 403 for insufficient permissions.

**Frontend: `__SURFACE_PATH__` + `sw.navigate()`**

- `window.__SURFACE_PATH__` and `window.__SURFACE_PARAMS__` injected into
  every extension surface page. Packages use these to decide which view to
  render.
- `sw.navigate(path, params)` for SPA-style intra-package routing via
  `pushState`. Emits `surface.navigate` events. Handles back/forward via
  `popstate`.
- SDK version bumped to `0.9.0`.

**Navigation**

- `extensionNavItems` reads the `surfaces` array to find the nav entry
  (first `nav: true`, or root `/`).
- Fix: packages with `status: pending_review` no longer appear in the
  sidebar navigation.

**Validation**

- `ValidateManifest` validates `surfaces` entries: path required, must
  start with `/`, no duplicates, access level must be recognized.
- Empty `surfaces` array rejected. Non-array `surfaces` rejected.

**Tests: 22 new**

- 11 manifest validation tests (surfaces valid/invalid, auto-synthesis,
  group access, duplicate paths).
- 11 route matching tests (static paths, param extraction, specificity
  ordering, nav resolution).

**Modified files:**

- `server/handlers/package_validate.go` — surfaces validation + auto-synthesis
- `server/handlers/package_validate_test.go` — 11 new tests
- `server/main.go` — unified extension route registration
- `server/pages/pages.go` — matchSurface, evaluateAccess, findNavSurface,
  RegisterExtensionRoutes, nav status filter
- `server/pages/pages_surface_match_test.go` — 11 new tests
- `server/pages/templates/base.html` — surface path/params injection
- `src/js/sw/sdk/index.js` — sw.navigate, popstate, version bump
- `docs/PACKAGE-FORMAT.md` — surfaces field documentation
- `docs/MULTI-SURFACE-GUIDE.md` — developer guide

## v0.8.5 — Extension Composability

Extensions can now compose with each other through declared slots, UI
contributions, and cross-package function calls. This is the last kernel
feature before 1.0 — the platform now supports the "extensions extending
extensions" pattern.

**Manifest declarations**

- Surfaces declare named `slots` in their manifest (e.g., `toolbar-actions`,
  `note-footer`) with context documentation for extension authors.
- Extensions declare `contributes` entries targeting `{host}:{slot}` names
  (e.g., `notes:toolbar-actions`). Coupling is soft — install order doesn't
  matter.
- The kernel validates slot/contribution conventions at install time.

**Backend: `lib.require()` relaxation**

- `lib.require()` now works with any package that declares `exports`, not
  just `type: "library"` packages. A full package (with surfaces, settings,
  UI) can export callable functions for cross-package use.
- The existing `depends` and permission model is unchanged — the called
  function runs with the target package's permissions.

**Admin slots endpoint**

- `GET /api/v1/admin/slots` returns an aggregated map of all declared slots
  across installed packages with their contributors.
- Uninstalling a package that declares slots warns about orphaned
  contributions in other packages.

**SDK additions**

- `sw.slots.renderAll(name, context)` — convenience helper for host surfaces
  to render all components in a slot with error isolation.
- `sw.slots.declare(name, description)` — runtime slot declaration for
  discoverability and debugging.

**Documentation**

- `PACKAGE-FORMAT.md` — added `slots`, `contributes`, `depends` field docs.
- `EXTENSION-GUIDE.md` — new "Extension Composability" section with slot
  naming conventions, contribution patterns, and cross-package call examples.
- `STARLARK-REFERENCE.md` — updated `lib` module to reflect exports-based
  calling (not library-type-only).

**Modified files:**

- `server/sandbox/lib_module.go` — type check → exports check
- `server/handlers/extensions.go` — composability field validation
- `server/handlers/packages.go` — orphaned contribution warning
- `server/main.go` — admin slots route registration
- `src/js/sw/sdk/slots.js` — `renderAll()`, `declare()`, `declarations()`
- `docs/PACKAGE-FORMAT.md` — slots, contributes, depends
- `docs/EXTENSION-GUIDE.md` — composability section
- `docs/STARLARK-REFERENCE.md` — lib module update
- `docs/DESIGN-extension-composability.md` — status Draft → Implemented

**New files:**

- `server/handlers/admin_slots.go` — admin slot aggregation endpoint

## v0.8.4 — Documentation Refresh + Surface Sizing Fix

Eight versions of module additions (v0.7.5–v0.8.3) shipped without a
docs pass. This release brings the public-facing guides up to date and
fixes a CSS layout bug affecting all surfaces.

**Documentation refresh**

- `STARLARK-REFERENCE.md` — added `workspace` module section (5 builtins),
  `permissions` module section, and `settings.has_capability()` documentation.
- `EXTENSION-GUIDE.md` — added `capabilities` manifest block, `vector(N)`
  column type, `user_permissions` and `gate_permission` manifest fields,
  updated sandbox permissions list with v0.8.0+ additions (`files.read`,
  `files.write`, `workspace.manage`).
- `TUTORIAL-FIRST-EXTENSION.md` — reviewed for v0.7+ accuracy (no changes needed).

**Surface sizing fix**

All surfaces using the shell topbar had scroll content clipped at the
bottom by ~44px (the topbar height). Root cause: surface containers were
siblings of `#shell-topbar` inside `.surface-inner`, which used
`height: 100%` without flex layout — the surface div claimed the full
parent height, ignoring the topbar sibling.

- Fix: `.surface-inner` now uses `display: flex; flex-direction: column`
  so the topbar and surface share vertical space via flex layout.
- All surface containers (`.surface-docs`, `.surface-admin`,
  `.surface-settings`, `.surface-editor`, `.extension-surface`) changed
  from `height: 100%` to `flex: 1; min-height: 0`.
- Inline styles on `surface-team-admin` and `welcome-mount` templates
  updated to match.

**Modified files:**

- `server/pages/templates/base.html` — `.surface-inner` gains flex column layout
- `src/css/surfaces.css` — `.surface-docs`, `.surface-admin`, `.surface-settings`, `.surface-editor`
- `src/css/extension-surface.css` — `.extension-surface`
- `server/pages/templates/surfaces/team-admin.html` — inline style fix
- `server/pages/templates/surfaces/welcome.html` — inline style fix
- `docs/STARLARK-REFERENCE.md` — workspace, permissions, has_capability
- `docs/EXTENSION-GUIDE.md` — capabilities, vector, user_permissions, gate_permission

## v0.8.3 — Vector Column Type

Extensions can now declare vector columns and perform similarity search.
Three-tier progressive enhancement: native pgvector on Postgres, JSONB
fallback without pgvector, TEXT fallback on SQLite.

**Manifest: `db_tables` vector columns**

- Declare `"vector(N)"` as a column type (N = dimension, 1–4096).
- On Postgres with pgvector: native `vector(N)` type with auto-created
  HNSW index (`vector_cosine_ops`).
- On Postgres without pgvector: `JSONB` column.
- On SQLite: `TEXT` column (JSON-encoded float arrays).

**Starlark API**

- `db.query_similar(table, column, vector=[], limit=10, filters={}, metric="cosine")`
  — returns rows ordered by ascending cosine distance with injected `_distance` key.
- `db.insert()` now accepts list values (serialized as JSON strings) for
  vector column storage.

**Internal**

- Modified: `handlers/ext_db_schema.go` — `parseVectorDim`, `mapColType`
  gains `hasPgvector` parameter, HNSW index creation for vector columns.
- Modified: `sandbox/db_module.go` — `HasPgvector` in `DBModuleConfig`,
  list support in `starlarkToGoValue`, `dbQuerySimilar` with pgvector and
  fallback paths, `cosineDistance` helper.
- Modified: `sandbox/runner.go` — wire `HasPgvector` from capabilities.
- Modified: `handlers/extensions.go` — `SetCapabilities` on `ExtensionHandler`.
- Modified: `server/main.go` — wire capabilities to extension handler.
- Updated: `docs/STARLARK-REFERENCE.md` — vector similarity section.
- New tests: 5 schema tests + 8 db module tests (13 total).

## v0.8.2 — Capability Negotiation

Extensions declare environment requirements in their manifest. The kernel
validates at install time and exposes a runtime query for graceful degradation.

**Manifest: `capabilities` block**

- `capabilities.required` — array of capability names. Install is rejected
  (HTTP 422) if any are unavailable. Rollback deletes the package row.
- `capabilities.optional` — array of capability names. Install succeeds
  regardless; extensions query at runtime via `settings.has_capability()`.

**Detected capabilities:** `postgres`, `pgvector`, `object_storage`, `s3`,
`workspace`.

**Starlark API**

- `settings.has_capability(name)` — returns `True` or `False`. Always
  available (no permission required).

**Admin API**

- `GET /api/v1/admin/capabilities` — re-probes and returns current state.

**Internal**

- New: `handlers/capabilities.go` (detection, parsing, validation, admin handler).
- New: `handlers/capabilities_test.go` (14 tests).
- New: `sandbox/settings_module_test.go` (4 tests).
- Modified: `handlers/packages.go` — replaced stub `checkCapabilities` with
  real validation; `SetCapabilities` setter.
- Modified: `handlers/packages_bundled.go` — bundled packages with unmet
  required capabilities are skipped on startup.
- Modified: `sandbox/settings_module.go` — `has_capability` builtin.
- Modified: `sandbox/runner.go` — `capabilities` field + `SetCapabilities` setter.
- Modified: `server/main.go` — `DetectCapabilities` at startup, wired to
  runner and package handler, admin route registered.

---

## v0.8.1 — Workspace Module

New `workspace` sandbox module. Managed disk directories for extensions
that need a real filesystem (git, compilers, media tools).

**workspace module (permission: `workspace.manage`)**

- `workspace.create(name)` — create a workspace directory (idempotent). Returns absolute path. Enforces quota if `WORKSPACE_QUOTA_MB` > 0.
- `workspace.path(name)` — get the absolute path of an existing workspace. Returns None if not found.
- `workspace.list()` — list workspace names for this extension.
- `workspace.delete(name)` — recursively remove a workspace (idempotent).
- `workspace.usage(name)` — disk usage in bytes (10-second timeout on directory walk).

**Configuration**

- `WORKSPACE_ROOT` — mount point for extension workspaces (default `/data/workspaces`).
- `WORKSPACE_QUOTA_MB` — per-extension quota in MB (default `0` = unlimited).

**Internal**

- New file: `sandbox/workspace_module.go`.
- New permission constant: `ExtPermWorkspaceManage`.
- Config: `WorkspaceRoot`, `WorkspaceQuotaMB` fields.
- Runner wiring: `SetWorkspaceRoot()` setter, `buildModulesWithLibCtx` creates workspace module when permission granted.
- Startup: `main.go` creates workspace root directory if writable, graceful degradation if not.
- All directories scoped to `{WORKSPACE_ROOT}/{packageID}/{name}/`.
- Security: name regex (`^[a-z][a-z0-9_]{0,62}$`), `filepath.Clean` + prefix check, `filepath.EvalSymlinks` for symlink escape detection.
- 16 new tests (create, path, list, delete, usage, name validation, quota enforcement).

## v0.8.0 — Files Module

New `files` sandbox module. Bridges the existing ObjectStore (PVC/S3) into
the Starlark sandbox with extension-scoped key namespacing.

**files module (permissions: `files.read`, `files.write`)**

- `files.put(name, content, content_type, metadata)` — store a file with optional metadata companion. Accepts string or bytes content. 50 MB default limit (configurable via `EXT_FILES_MAX_SIZE`).
- `files.get(name)` — read a file. Returns dict with `content` (bytes), `content_type`, `size`, `metadata`. Returns None if not found.
- `files.meta(name)` — read metadata only (no content transfer).
- `files.list(prefix, limit)` — list files by prefix. Returns list of dicts. Filters out internal `_meta/` companions.
- `files.delete(name)` — delete a file and its metadata companion. Idempotent.
- `files.delete_prefix(prefix)` — delete all files under a prefix.
- `files.exists(name)` — check existence without reading.

**Internal**

- New file: `sandbox/files_module.go`.
- New permission constants: `ExtPermFilesRead`, `ExtPermFilesWrite`.
- `ObjectStore` interface gains `List(ctx, prefix, limit)` method; implemented for PVC and S3.
- Runner wiring: `SetObjectStore()` setter, `buildModulesWithLibCtx` creates files module when permission granted.
- All keys scoped to `ext/{packageID}/`. Metadata stored as companion JSON at `ext/{packageID}/_meta/{name}`.
- 16 new tests (15 files module + 1 PVC list).

## v0.7.12 — Concurrent Execution Primitive

New `batch` sandbox module. Enables extensions to parallelize arbitrary
Starlark callables — including frozen library exports from `lib.require()`.

**batch module (permission: `batch.exec`)**

- `batch.exec(callables, timeout=10)` — runs up to 8 zero-arg callables concurrently, each in its own `starlark.Thread` with independent step budget. Returns `(results, errors)` tuple with ordered results. Per-branch timeout (1–30s, default 10).
- Nested `batch.exec()` calls are prohibited (prevents exponential goroutine growth).
- `lib.require()` not available inside branches — load libraries before the batch call.
- `print()` output from branches is discarded.

**Internal**

- New file: `sandbox/batch_module.go`.
- New permission constant: `ExtPermBatchExec`.
- Runner wiring: `buildModulesWithLibCtx` creates batch module when permission granted.
- Design doc: `docs/DESIGN-batch-exec.md`.
- 12 new tests (parallel ordering, partial failure, timeout, cancellation, cap enforcement, nesting prevention, frozen sharing, permission gating).

## v0.7.11 — Query & HTTP Ergonomics

Four new Starlark sandbox builtins. No new permissions, no schema changes.

**db module (permission: `db.read`)**

- `db.count(table, filters={})` — returns integer count of matching rows.
- `db.aggregate(table, column, op, filters={})` — single-value aggregation. `op` ∈ {count, sum, avg, min, max}. Returns int, float, or None.
- `db.query_batch(queries)` — execute up to 10 query specs in a single call. Each spec supports the same parameters as `db.query`.

**http module (permission: `api.http`)**

- `http.batch(requests)` — concurrent HTTP dispatch of up to 10 requests. Individual failures return error response dicts (`status: 0`) rather than aborting the batch.

**Internal**

- Extracted `buildSelectQuery` helper from `dbQuery` for reuse by `db.query_batch`.
- 21 new tests (14 db, 7 http).

## v0.7.10 — Workflow Handoff + Assignment UI

Closes the three UX gaps found during v0.7.9: public→team handoff,
team inbox, and manual assignment. Also fixes dead system-admin bypass
in team middleware and enriches assignment API responses.

**Public Stage Handoff**

- `RenderWorkflow()` detects audience mismatch (team/system stage + unauthenticated visitor) and renders "Submitted Successfully" screen with reference ID instead of showing the team-gated form.

**API Response Enrichment**

- `ListByTeam` and `ListMine` handlers enrich assignment records with `workflow_name`, `stage_name`, `sla_breached` by joining instance → workflow → version snapshot. Results cached per-request to avoid repeated DB hits.
- `ListTeamInstances` returns `instanceView` with `workflow_name`, `stage_name`, `age_seconds`, `sla_breached`.

**Team Middleware Fix**

- `RequireTeamAdmin` / `RequireTeamMember` system-admin bypass was dead code (`c.Get("role")` never set by auth middleware). Fixed to resolve `PermSurfaceAdminAccess` via `resolveAndCachePerms`. Variadic `allStores` parameter preserves backward compatibility.

**Team Workflow Inbox**

- Enhanced `AssignmentsTab` in team-admin: "My Active" (claimed) and "Available" (unassigned) sections with claim/unclaim/release/work/complete actions, time-ago display, WS live updates.
- Manual "Assign" button on unassigned rows with team member dropdown. New `POST /api/v1/assignments/:id/assign` endpoint.

**Assignment Notifications**

- Engine calls `notifyAssignment()` on assignment creation — notifies specific user or all team members via notification system.

**SDK Gap Closure**

- Added `workflowAssignments` domain (claim/unclaim/complete/cancel/assign/mine)
- Added `teams.assignments`, `teams.workflowInstances`, `teams.cancelWorkflowInstance`
- Fixed dead `workflows.cancel` route referencing `/channels/`

**E2E Test**

- `ci/e2e-workflow-handoff.sh`: public form → team review → claim → complete. Verifies audience mismatch screen, assignment creation, full lifecycle.

---

## v0.7.9 — Workflow Independence Audit

Workflows proven independent of chat and all optional packages. Critical
rendering bugs fixed, dead chat UI removed, deferred test debt closed.

**RenderWorkflow Fix (critical)**

- `RenderWorkflow()` was a stub that never loaded instance/stage data — post-start page was broken. Now loads instance by token/ID, resolves current stage, populates all template fields.
- `WorkflowPageData` fields renamed: `ChannelID` → `EntryToken`, `ChannelTitle` → `WorkflowTitle`, `ChannelDescription` → `WorkflowDescription` (vestigial chat-era names)
- Stage modes aligned: template uses Go constants (`form`, `review`, `delegated`, `automated`) instead of legacy `form_only`/`form_chat`
- Dead chat UI removed: chat CSS, split layout, `sendMessage()`, fallback chat branch (~180 lines deleted from `workflow.html`)
- Landing page mode conditionals updated to match Go constants
- OpenAPI `stage_mode` enum corrected (4 occurrences)

**Bug Fixes (found during verification)**

- Entry token resolution: handler passed route param (instance ID) as entry token to JS. Fixed: resolve actual token from `inst.EntryToken` + `?token=` query param.
- Fieldset submit guard: `submitForm()` checked `FORM_TPL.fields` but not `.fieldsets` — progressive forms silently no-op'd. Fixed: accept either.
- Stage advance status check: JS checked `result.status === 'advanced'` but API returns `active`/`completed`. Fixed: on 200 OK with `active`, reload to render next stage.

**Deferred test coverage (carried from v0.7.6)**

- 17 new SQLite store tests: workflow CRUD, stages, instances, lifecycle (advance/complete/cancel/stale), team scope, API tokens, users, groups
- `InstallPackage` decomposed from 400-line monolith into 7 private methods: `receiveUpload`, `parseAndValidateArchive`, `extractPackageAssets`, `registerPackage`, `applySchemaAndPermissions`, `resolveDependencies`, `checkCapabilities`
- `ci/e2e-workflow-nochat.sh` — E2E test for workflow lifecycle without chat

**Discovered issues (deferred to v0.7.10)**

- Public→authenticated stage handoff: visitor sees auth-gated stage form instead of "submitted" screen
- Team member pickup UI: no surface for claiming workflow instances
- Assignment flow: no admin UI for manual instance assignment

**Tests:** 17 new store tests, 1 new E2E script

---

## v0.7.8 — Bug Fixes & Admin Gaps

- `StartBySlug` handler + `/api/v1/workflow-entry/:scope/:slug` route for landing page Start button
- Workflow delete guard: admin endpoint rejects team-scoped workflows (403)
- Package button cleanup: Delete hidden for bundled packages
- Package export: `fetch()` with auth token instead of `window.open()`
- Settings CSS: bottom padding fix for save button cutoff
- Shared `StageForm` component between admin and team-admin; public entry URL with copy button

---

## v0.7.7 — API Tokens + Extension Permissions

Personal access tokens (PATs) for programmatic API access, plus extension-declared
user permissions for backend RBAC enforcement.

**API Tokens (PATs)**

- Migration 015: `api_tokens` table (PG + SQLite) with SHA-256 hash, prefix, JSON permissions, expiry
- Token store interface + PG/SQLite implementations (Create, GetByHash, ListForUser, Revoke, CleanExpired, UpdateLastUsed)
- `POST /api/v1/auth/tokens` — create token (returns plaintext once), permissions validated as subset of user's
- `GET /api/v1/auth/tokens` — list my tokens; `DELETE /api/v1/auth/tokens/:id` — revoke
- `POST /api/v1/admin/tokens` — create token for any user (audit logged as `admin.token.create`)
- Auth middleware: `Bearer arm_pat_...` tokens validated alongside JWTs, user active check, fire-and-forget `last_used_at` update
- Permission scoping: PAT permissions used directly at request time (git model — retained until revoked)
- `auth.HashToken()` shared SHA-256 utility (replaces local `hashToken()` in auth.go)
- Settings UI: API Tokens tab with create form, permission checkboxes, copy-once display, revoke button
- Admin UI: "PAT" button on user rows creates tokens for any user
- `BootstrapPAT`: `ARMATURE_BOOTSTRAP_PAT=true` env var creates admin PAT at startup, writes to `/tmp/armature-admin-pat.txt`
- E2E smoke test: reads bootstrap PAT before falling back to login flow

**Extension-Declared User Permissions**

- Dynamic permission registry: `RegisterExtensionPermissions()` / `UnregisterExtensionPermissions()` with RWMutex
- `AllPermissionsWithExtensions()` returns kernel + extension permissions; `AllPermissionsGrouped()` for admin UI
- `user_permissions` manifest field: extensions declare user-facing permissions
- `gate_permission` manifest field: ext_api.go checks user permission before calling `on_request`
- `req["permissions"]` in Starlark request dict: user's effective permissions included for inline checks
- `permissions.check(user_id, perm)` Starlark module: read-only permission check, always available (no sandbox gate)
- Group UI: permissions grouped by source (Platform / package name) with section headings
- Boot-time scan: `RegisterAllExtensionUserPermissions()` populates registry from active packages
- Uninstall cleanup: `UnregisterExtensionPermissions()` called on package delete

**Tests:** 10 new tests (7 handler + 3 auth registry)

---

## v0.7.6 — Code Hygiene + Test Coverage

**Critical Fixes**
- Removed `channels` from `allowedViews` in `db_module.go` — `ext_view_channels` does not exist; `db.view("channels")` would crash
- Fixed 5 dead API routes in `workflow.html` — rewired to public workflow API (`/api/v1/public/workflows/`)
- Fixed `RenderWorkflow` handler to pass route `:id` param as entry token (was reading unset `channel_id`)

**Dead Code Removal**
- Deleted `SeedTestChannel()` from `database/testhelper.go` (inserted into nonexistent channels table)
- Removed `RunContext.ChannelID` from `sandbox/runner.go` (vestigial, unused)
- Removed `webhook.Payload.ChannelID` field (channels no longer exist — breaking webhook JSON change)
- Fixed stale comments in `storage.go`, `prometheus.go`, `workflow_module.go` referencing dead `/channels` paths

**Migration Hygiene**
- Added SQLite placeholder `013_cluster_registry.sql` (PG-only migration, aligns numbering)
- Renumbered SQLite `013_test_runner_type.sql` → `014` to match PG (compat rename in `migrate.go`)
- Documented missing migration 008 in both 009 files (merged into 007 during pre-1.0 consolidation)

**Test Coverage**
- 82 new workflow routing tests (`routing_test.go`): `ResolveNextStage`, `ResolveStageByName`, `ParseStageConfig`, `evaluateCondition` with all 10 operators
- 10 new middleware tests (`permissions_test.go`): `RequirePermission`, `RequireAdmin`, permission caching, `RateLimiter` (allow/deny/fail-open)

**Bug Fixes**
- Removed dead Admin "Storage" tab from System category (backend endpoint preserved for future Monitoring use)
- Fixed backup download: `sw.auth.token()` → `sw.auth._getToken()` — both "Download Backup" and server backup download now work

## v0.7.5 — Headless E2E + CI Gate

**CI Gating Redesign**
- `VERSION` and `scripts/*` no longer trigger frontend/backend tests — deploy-only (pipeline v0.18.0)
- `docs/*` changes now trigger build-and-deploy (docs are served in-app via Docs surface)
- Path gating comment block updated to reflect corrected model

**E2E Smoke Test**
- `ci/e2e-smoke-test.sh` — authenticates as admin, discovers surfaces, runs Playwright navigation test
- `ci/e2e-smoke-driver.js` — visits every surface, asserts shell topbar present, no JS console errors, home link works
- Screenshot-on-failure: full-page PNG + console log saved as CI artifacts
- Baseline screenshots captured for every surface (informational, not a gate)

**CI Pipeline Integration**
- `test-runners` stage re-enabled with broad trigger condition (BE/FE/packages/infra)
- New `e2e-smoke` stage: boots server via docker-compose, runs Playwright smoke test, failure blocks merge
- `docker-compose.ci.yml` gains `e2e-smoke` service (same Playwright v1.52.0 image)
- `build-and-deploy` now depends on both `test-runners` and `e2e-smoke`

**Documentation**
- `docs/DESIGN-storage-primitives.md` — v0.8.x storage primitives design (files module, workspace module, capability negotiation, vector columns)
- `docs/DESIGN-extension-composability.md` — v0.8.4 composability design (slots/contributes manifest fields, lib.require relaxation, SDK helpers)
- `ROADMAP.md` expanded through v1.0: v0.8.x storage primitives, v0.9.x reference extensions, v0.10.x sidecar tier, v1.0 gate criteria, design principles, full design decisions log

## v0.7.4 — Documentation + Deferred Surface Work

**Docs Category Grouping**
- Backend `Category` field on `docEntry` struct in `server/handlers/docs.go`
- Frontend sidebar groups docs by category with `.docs-category-heading` CSS
- Four categories: Getting Started, Platform, Extension Development, Operations
- 14 docs in ordered list (was 7 ordered + 3 auto-discovered)

**New Documentation (4 guides)**
- `PERMISSIONS-AND-GROUPS.md` — RBAC model, 7 permission slugs, system/custom groups, settings cascade, extension permissions
- `WORKFLOWS.md` — Entry modes, stage modes/types/audiences, signoff gates, SLA enforcement, branch rules, Starlark hooks
- `STARLARK-REFERENCE.md` — Sandbox constraints, 10 modules with function signatures and permission gates, example hook script
- `FRONTEND-JS-GUIDE.md` — Preact+htm runtime, 16 SDK modules with API reference, shell topbar patterns, CSS contract

**Extension Guide Updates**
- `config_section` manifest field documented: schema, backend discovery (`configSectionsForSurface()`), frontend `__CONFIG_SECTIONS__` contract, example component
- Starlark Sandbox API section replaced with pointer to new Starlark Reference

**Docs Content Refresh**
- GETTING-STARTED: `sb_data` → `armature_data` volume name
- ARCHITECTURE: `sb.register()`/`sb.ns()` → `sw` SDK references, shell topbar mention
- DEPLOYMENT: `sb_storage` → `armature_storage`, added `TLS_MODE` env var
- TUTORIAL-FIRST-EXTENSION: `--bg-2` → `--bg-secondary` CSS variable
- EXTENSION-CSS: self-hosted font notes on `--font` and `--mono`
- `docs.go`: added `AUDIT-` and `USABILITY-` prefix filters for auto-discovery

**Team Admin Workflows Split**
- `workflows.js` (722 lines) split into 3 ES modules:
  - `workflows.js` (~160 lines) — `WorkflowsSection` + `WorkflowsTab` + imports
  - `workflow-editor.js` (~240 lines) — `WorkflowEditor` + `StageForm`
  - `workflow-monitor.js` (~210 lines) — `AssignmentsTab` + `MonitorTab` + `SignoffPanel`
- External import contract unchanged (default export stays in `workflows.js`)

**Bug Fixes**
- Docs outline `scrollToHeading` now scrolls `.docs-content` container instead of `scrollIntoView`, preventing topbar from being pushed off-screen
- `--bg-2` (undefined CSS variable) replaced with `--bg-secondary` in `sw-shell.css` and `sw-primitives.css`

## v0.7.3 — Extension Shell Migration

**Shell Topbar Migration**
- Migrated Chat, Notes, and Schedules from legacy `sw.shell.Topbar` component to the v0.7.0 shell topbar contract (`sw.shell.topbar.setTitle/setSlot`)
- Eliminated double topbar (shell-injected + surface-owned) on all three extension surfaces
- Chat: reactive slot updates for thread title + People button when conversation changes
- Notes: slot content for + New Note, Import .md, and Graph toggle buttons
- Schedules: reactive slot with schedule count and + New Schedule button; removed legacy fallback branch

**Runner Test Updates**
- Added `shell-topbar` test suite to chat-runner, notes-runner, and schedules-runner
- Tests fetch surface JS and assert: no legacy `sw.shell.Topbar` reference, uses `sw.shell.topbar.setTitle/setSlot` API
- 6 new tests across 3 runners

**Package Versions**
- Chat surface v0.3.0, Notes surface v0.9.0, Schedules surface v0.2.0
- Chat runner v0.2.0, Notes runner v0.2.0, Schedules runner v0.2.0

**Roadmap**
- Headless E2E automation moved to v0.7.5 (independent from shell migration)

## v0.7.2 — Package Runners + CI Gate

**Package Runners (5)**
- Notes runner: `requires: ["notes"]`. 3 suites (crud, folders, tags-search), 12 tests
- Chat runner: `requires: ["chat", "chat-core"]`. 2 suites (conversations, messaging), 9 tests
- Schedules runner: `requires: ["schedules"]`. 1 suite (crud), 5 tests
- Workflow runner: `requires: ["content-approval"]`. 1 suite (lifecycle), 5 tests
- Renderer runner: `requires: ["mermaid-renderer"]`. 1 suite (contract), 4 tests

**Runner Result API**
- `POST /api/v1/admin/test-runners/results` — store structured run results
- `GET /api/v1/admin/test-runners/results` — retrieve latest results per runner
- In-memory store with 4 Go handler tests

**CI Integration**
- `test-runners` stage in Gitea CI pipeline
- Playwright driver launches headless browser, navigates to `/s/test-runners`, triggers run-all
- `wait-for-healthy.sh` polls `/healthz/ready` before test execution
- DinD networking fix: resolve container IP via `docker inspect` (port mapping not exposed to runner localhost)

## v0.7.1 — Surface Runner Framework

**`sw.testing` SDK Module**
- New kernel SDK module at `src/js/sw/sdk/testing.js`
- `sw.testing.suite(name, fn)` — register test suites with lifecycle hooks
- `sw.testing.run(name?)` — execute one or all suites, returns structured JSON
- Suite context: `s.test()`, `s.beforeAll/afterAll()`, `s.beforeEach/afterEach()`, `s.track()`, `s.skip()`
- Test context: `t.assert.ok/eq/neq/gt/match/throws/status/shape/arrayOf`, `t.warn()`, `t.skip()`
- Auto-cleanup: `track(type, id)` registers resources for LIFO deletion in afterAll
- Three result statuses: passed / failed / warned — warnings are never silent

**`test-runner` Manifest Type**
- New `"type": "test-runner"` in `ValidateManifest` — surface-like packages discovered by type
- Test-runner packages excluded from sidebar nav (not type "surface" or "full")
- Runner manifests support `"requires": [...]` — missing packages → clean skip

**ICD Runner Migration**
- Migrated from hand-rolled `T.test()`/`T.assert()` framework to `sw.testing.suite()`
- Stripped extension-dependent tests (channels, notes, personas, etc.) — those belong in v0.7.2 package runners
- Kernel-only suites: smoke, crud (admin, profile, notifications, teams, workflows, extensions, surfaces, packages), authz, security, providers, packaging, sdk
- Type changed to `"test-runner"`, standalone route removed, UI rendering delegated to registry

**SDK Runner Migration**
- Migrated from `T.dualTest()`/`T.domains` framework to `sw.testing.suite()`
- Dual-path validation preserved: SDK call + raw ICD fetch + verdict dispatch
- Stripped extension domains — kernel-only suites: misc, workflows, admin, packages, connections, dependencies, composition
- SHAPE_BUG verdict maps to `t.warn()`, SDK_BUG/ICD_BUG to `t.assert.ok(false)`

**Runner Registry Surface**
- New `test-runners` surface at `/s/test-runners` (admin-only)
- Discovers installed test-runner packages via admin packages API
- Dynamically loads each runner's JS to register suites
- Run All button, per-runner Run button, real-time results dashboard
- Suite/test results with pass/fail/warn/skip color coding, timing, error details
- Export Failures / Export Full Results buttons for JSON download
- `requires` checking with prominent skip display for missing dependencies
- Dark mode styling using kernel CSS variables

**Database Migration**
- SQLite migration 013: adds `test-runner` to packages.type CHECK constraint
- Postgres migration 014: same constraint update

## v0.7.0 — Shell Contract + Surface Audit + Rebrand

**Shell Infrastructure**
- Kernel-injected two-slot topbar for all surfaces (home, left slot, center slot, bell, user menu)
- `sw.shell.topbar` SDK API: `setLeft()`, `setSlot()`, `setTitle()`, `hide()`, `show()`
- `.sw-topbar__tabs` / `.sw-topbar__tab` CSS classes for consistent tab styling
- Shell topbar auto-mounts on extension surfaces via `#shell-topbar` div

**Backend WS Events**
- `package.changed` event broadcast on install/uninstall/enable/disable/update
- `auth.changed` event targeted to affected user on team/group membership changes
- `notification.all_read` event split from `notification.read` for cleaner badge sync
- `Hub.Broadcast()` method for untargeted all-client events

**User Menu + Bell Reactivity**
- User menu re-fetches surface list on `package.changed` and `auth.changed` events
- Notification bell syncs on `notification.read` and `notification.all_read` across tabs

**Surface Migrations**
- Settings: Pattern B (flat tabs in topbar, no sidebar, full-width content)
- Admin: Pattern C (category tabs in topbar, surface-owned sidebar below)
- Team Admin: Pattern B (flat tabs, no sidebar, Groups tab removed)
- Docs: Pattern A (shell topbar auto-renders, removed explicit Topbar import)
- All 4 surfaces now have notification bell and user menu via shell topbar

**Error Handling + Empty States**
- `.sw-inline-error` CSS primitive for inline error + retry pattern
- `.sw-empty-state` CSS primitive for guided empty states
- Admin Workflows, Packages, Groups: inline error on list fetch failure
- Admin Workflows, Groups: descriptive empty state guidance

**Announcement Global Dismiss**
- Announcement dismiss state persisted to localStorage keyed by content hash
- Dismissed once on any surface, dismissed everywhere

**Rebrand Assets**
- `favicon-light.svg` renamed to `wordmark.svg` (was a 520x80 wordmark, not an icon)
- New `favicon-light.svg`: actual square light-mode icon
- New `wordmark-dark.svg`, `wordmark-light.svg` for dark/light backgrounds
- New `favicon-light-32.png`, `favicon-light-256.png` raster icons
- Full icon library deployed to `src/icons/` (both b/e variants, animated SVGs)
- `manifest.json` description updated to "Self-hosted extension platform"
- Light-mode icon entries added to PWA manifest

**Bug Fixes**
- Docs: "On this page" outline links now scroll to headings (IDs were missing from rendered HTML)
- ICD security tier: tightened path traversal assertion (400/422, not 409), added `finally` cleanup
- Workflow demo: replaced silent catch with inline error + retry
- Team Admin: signoff panel shows display names instead of raw UUIDs
- Deleted `packages/hello-dashboard/` (dead package)
- Deleted `team-admin/groups.js` (37-line dead-end, no CRUD)

## v0.6.18 — CI Bundle Wiring

Wire `BUNDLED_PACKAGES` env var into the Gitea CI pipeline so each
environment gets the correct package set at boot.

### Changed

- **CI: dev deploys** — `BUNDLED_PACKAGES=*` (install all, matches docker-compose default).
- **CI: test deploys** — `BUNDLED_PACKAGES=notes,chat,chat-core` (core surfaces only).
- **CI: prod deploys** — `BUNDLED_PACKAGES=notes,chat,chat-core,mermaid-renderer,schedules`.
- **docker-compose.yml** — default `BUNDLED_PACKAGES` changed from empty to `*` (install all for local dev).

### Fixed

- **TestBundledInstall_DefaultAllowlist** — updated test assertions to match v0.6.17's empty default set (was still expecting `notes` in curated defaults).

## v0.6.17 — Bug Fixes & Welcome Logic

Fixes broken UI interactions (folder creation, team member add), dropdown
overflow, welcome surface auto-disable, and bare-install default behavior.

### Fixed

- **Notes "Add folder" button** — `prompt()` replaced with `sw.prompt()`
  so the dialog renders correctly in the extension iframe sandbox.
- **Admin "Add team members"** — user list API returns `{data:[…]}`; handler
  now unwraps the envelope (`Array.isArray(u) ? u : u.data`) so the user
  picker populates.
- **Package filter dropdown overflow** — removed `right:0` constraint on
  `.sw-dropdown__list`, added `min-width:max-content` and `overflow-x:hidden`
  so option labels ("Extension", "Workflow") render fully without a scrollbar.
- **Admin actions cell wrapping** — switched `.admin-actions-cell` from
  `white-space:nowrap` to flexbox with `flex-wrap:wrap; gap:4px` so buttons
  don't overflow on narrow viewports.

### Changed

- **Welcome surface auto-disable** — welcome page now redirects to `/` when
  any non-core extension surface is installed. Removed from the topbar
  navigation surface list so it never appears alongside real surfaces.
- **Zero default bundled packages** — `defaultBundledPackages` map is now
  empty. Fresh installs start bare; use `BUNDLED_PACKAGES` env var to control
  what gets auto-installed per environment (`*` for all, comma-separated list
  for selective).
- **Bundled filter logic** — `nil` (from `*`) means install all; empty map
  (default) means install nothing. Previous code treated both as "install all".
- **User menu conditional items** — Docs, Settings, and Team Admin menu
  entries only appear when those surfaces are actually enabled, not assumed.
- **SDK imperative host mount** — `ToastContainer` and `DialogStack` are now
  auto-mounted by the SDK boot sequence for extension surfaces that lack an
  AppShell, preventing missing toast/dialog hosts.

## v0.6.16 — Usability Survey Gate

Machine-auditable UI quality gate. Four new audit scripts, a structured survey
prompt, contrast and touch-target fixes, and Docker Hub documentation correction.

### Added

- **`scripts/generate-ui-inventory.sh`** — walks all kernel + package CSS,
  extracts every class selector with surface, line number, responsive breakpoints,
  spacing tokens, and font-size usage. Outputs `ui-inventory.json` (1567 entries).
- **`scripts/check-contrast.sh`** — parses `variables.css` dark/light token
  pairs, computes WCAG AA contrast ratios for 24 semantic text-on-background
  pairings per theme (48 total). Uses AA (4.5:1) for normal text, AA-lg (3.0:1)
  for large/bold text contexts.
- **`scripts/generate-coverage-matrix.sh`** — 12 kernel primitives × all surfaces
  markdown table. Flags any deprecated component usage (`.btn-primary`, etc.).
- **`scripts/audit-touch-targets.sh`** — static analysis for 44px minimum mobile
  touch targets on close buttons and interactive elements.
- **`docs/USABILITY-SURVEY.md`** — structured 8-section prompt (viewport, banners,
  responsive, styling, contrast, touch targets, focus indicators, component
  uniformity) with pass/fail criteria and file paths for automated execution.
- **Focus indicators** — `:focus-visible` styles on `.sw-btn`, `.sw-input`,
  `.sw-dropdown__trigger`, `.sw-menu__item`, `.sw-tabs__tab`.
- **Mobile touch targets** — `min-width/min-height: 44px` in `@media (max-width:
  768px)` for `.sw-banner__close`, `.sw-toast__close`, `.sw-dialog__close`,
  `.sw-drawer__close`, `.sw-tabs__arrow`, `.modal-close`, `.sw-tabs__tab`,
  `.sw-dropdown__option`.

### Fixed

- **WCAG contrast violations** — dark-mode accent darkened from `#6c9fff` to
  `#6493ed` (3.03:1 with white text), dark-mode success from `#22c55e` to
  `#1dab51` (3.00:1). Light-mode `--text-3` darkened from `#8b8da3` to `#787a92`.
  Light-mode `--success-light` and `--warning-light` darkened for badge contrast.
  All 48 pairings now pass.
- **Docker Hub references** — `docs/DEPLOYMENT.md` and `docs/DISTRIBUTION.md`
  corrected from `ghcr.io/armature/armature` to `gobha/armature` (Docker Hub).
  Builder image corrected to `gobha/armature-builder`. GitHub URL corrected to
  `github.com/gobha/armature`.

## v0.6.15 — User Display Audit

Every user-facing identity surface now shows human-readable names instead of
UUIDs, with the canonical fallback chain: `display_name → username → "Unknown"`.

### Added

- **`GET /api/v1/users/resolve?ids=...`** — batch endpoint returns identity
  records (username, display_name, handle, avatar_url) for up to 100 user IDs.
  Response keyed by ID for O(1) client lookups.
- **`sw.users` SDK module** — `resolve(id)`, `resolveMany(ids)`,
  `displayName(user)` with 60-second local cache. Surfaces use this instead
  of ad-hoc lookups or stale snapshots.
- **5 handler tests** for the resolve endpoint (single, multiple, missing,
  empty, no-param).

### Changed

- **Admin users list** — shows `display_name || username` as primary
  identifier, with username shown as secondary when display_name is set.
- **Admin teams/groups member lists** — replaced `username || user_id`
  with `display_name || username || 'Unknown'`.
- **Team-admin members** — dropdown and list now show display_name.
- **Chat participants** — resolved from users table via `sw.users.resolveMany()`
  instead of relying on creation-time snapshot. Message sender names, typing
  indicators, and participant sidebar all use resolved names.
- **Dashboard greeting** — added `|| 'Unknown'` terminal fallback.
- **Team activity log** — capitalized fallback to `'Unknown'`.

### Deprecated

- **`participants.display_name` column** in chat-core — column retained for
  backward compatibility but UI no longer relies on snapshot values. Comments
  added to `packages/chat-core/script.star` noting deprecation.

## v0.6.14 — Visual Polish

Systematic cleanup of stale values, self-hosted fonts, and rendering fixes.
Final visual pass before the v0.6.15 usability survey gate.

### Fixed

- **v0.6.13 spacing regressions** — added half-step tokens (`--sp-1h` 6px,
  `--sp-2h` 10px) and restored correct padding on `.sw-btn--sm`, `.sw-input`,
  `.sw-menu__item`, `.sw-dropdown__option`, `.sw-tabs__tab`.
- **Theme settings toggle** — showed resolved theme ("Dark") instead of stored
  mode ("System"). Changed `appearance.js` to read `sw.theme.mode`.
- **Notes surface scrollbar** — added `overflow: hidden` to `.surface-inner`
  in `base.html`, preventing spurious scrollbar at any scale.
- **Chat input clipped at high scale** — same `overflow: hidden` fix prevents
  zoomed content from overflowing the surface container.
- **User menu drift at scale > 100%** — `menu.js` now divides
  `getBoundingClientRect()` coords by the CSS zoom factor, fixing position
  for `position: fixed` menus inside a zoomed ancestor.
- **Undefined variables** — `--text-secondary` (login), `--text-muted`
  (user picker), `--text-1` (settings toggle) replaced with correct tokens.

### Changed

- **Stale fallback colors purged** — removed ~65 hex/rgba fallback values
  from `var()` calls across 9 kernel CSS files and 3 extension packages.
  Old gold theme color `#b38a4e` fully eliminated (9 instances).
- **Self-hosted fonts** — bundled DM Sans and JetBrains Mono woff2 files
  in `src/fonts/`. Replaced Google Fonts `@import` and login.html `<link>`
  with local `@font-face` declarations. Zero external font dependencies.
- **Consistent border-radius** — added `--radius-sm: 4px` token. Migrated
  ~60 hardcoded `border-radius` values across all kernel CSS and 12 extension
  packages to three tokens: `--radius-sm` (4px), `--radius` (8px),
  `--radius-lg` (12px).

### Updated

- `docs/EXTENSION-CSS.md` — added `--sp-1h`, `--sp-2h` half-step tokens
  and `--radius-sm` to the public CSS contract.

## v0.6.13 — Responsive & Spacing

Spacing token scale and tablet breakpoint. All kernel CSS and extension
packages migrated from hardcoded values to design tokens.

### Added

- **Spacing tokens** (`--sp-1` through `--sp-12`) — 4px-grid scale in
  `variables.css`. Nine stops: 4, 8, 12, 16, 20, 24, 32, 40, 48px.
  Numeric naming (`--sp-N`), rem-based for zoom/font-size respect.
- **Tablet breakpoint** (`max-width: 1024px`) — new responsive tier
  between mobile (768px) and desktop. Secondary workspace pane narrows
  to 360px, admin sidebar to 120px, settings/admin/editor navs shrink.
- **Breakpoint documentation** in `EXTENSION-CSS.md` — Mobile (768px),
  Tablet (1024px), Desktop (default).
- **Spacing guidelines** in `EXTENSION-CSS.md` — token table with
  computed pixel values and usage examples.

### Changed

- **8 kernel CSS files** migrated to spacing tokens — `sw-primitives.css`,
  `modals.css`, `surfaces.css`, `layout.css`, `sw-shell.css`,
  `primitives.css`, `user-menu.css`, `sw-login.css`. Hardcoded padding,
  margin, and gap values replaced with `var(--sp-N)`.
- **12 extension packages** migrated — chat, dashboard, editor,
  git-board, hello-dashboard, icd-test-runner, notes, schedules,
  sdk-test-runner, tasks, team-activity-log, workflow-demo.
- **Login hero breakpoint** normalized from 900px to 1024px (tablet).
- **Notes mobile breakpoint** normalized from 700px to 768px (standard).

## v0.6.12 — Extension CSS Isolation

Prefix enforcement prevents extension CSS from leaking into the kernel or
sibling extensions. All 12 in-tree packages migrated to `.ext-{slug}-*`
naming convention.

### Added

- **`data-ext` attribute** on extension mount container — enables scoped
  selectors like `[data-ext="chat"] .ext-chat-app`.
- **CSS linter** (`scripts/lint-package-css.sh`) — validates that the first
  class selector in every extension CSS rule starts with `.ext-{slug}`.
  Exempts `:root`, `@keyframes`, `@font-face`, `@media`, kernel `.sw-*`
  classes, and CodeMirror `.cm-*` classes.
- **Kernel CSS contract** (`docs/EXTENSION-CSS.md`) — documents stable
  public classes and CSS variables that extensions may reference. Everything
  else is internal kernel CSS.

### Changed

- **12 packages migrated** — all class selectors renamed to `.ext-{slug}-*`:
  chat, dashboard, editor, git-board, hello-dashboard, icd-test-runner,
  notes, schedules, sdk-test-runner, tasks, team-activity-log, workflow-demo.
  CSS and JS files updated in lockstep.
- **`icd-test-runner`** — ID selectors (`#extension-mount`) converted to
  class-based selectors with proper prefix.
- **`editor` cross-references** — compound selectors referencing notes
  classes updated to new `.ext-notes-*` names.
- **`chat` kernel overrides** — `.sw-dialog:has(...)` override scoped under
  `[data-ext="chat"]` instead of global.

## v0.6.11 — CSS Deduplication

One class per concept. The old `primitives.css` button, toast, popup-menu,
dropdown, and tabs systems are retired. `sw-primitives.css` is the single
source of truth for all Preact component styles.

### Changed

- **Buttons**: All 29 files migrated from `.btn-primary` / `.btn-small` /
  `.btn-danger` / `.btn-ghost` / `.btn-md` / `.btn-sm` to the BEM-style
  `.sw-btn .sw-btn--{variant} .sw-btn--{size}` system.
- **Toasts**: Old `.toast-container` / `.toast` CSS deleted. SDK's
  `sw.toast()` API already used `.sw-toast-*` classes — no JS changes.
- **Popup menus**: Old `.popup-menu` / `.popup-menu-item` CSS deleted
  (unused — `.sw-menu` is the active system).
- **Dropdown collision resolved**: Old `.sw-dropdown` (styled `<select>`)
  deleted from `primitives.css`. The `sw-primitives.css` custom dropdown
  component (`.sw-dropdown` with BEM sub-elements) is authoritative.
- **Tabs collision resolved**: Old `.sw-tabs` / `.sw-tab-btn` deleted from
  `primitives.css`. The `sw-primitives.css` scrollable tabs component
  (`.sw-tabs__tab`) is authoritative.
- **`.settings-section` collision resolved**: Removed duplicate definition
  from `modals.css`. The `surfaces.css` card-style definition is
  authoritative; `.settings-content .settings-section` override resets
  card styling for flat settings layouts.

### Added

- `.sw-btn--success` variant in `sw-primitives.css` (green action button).
- `--bg-active` CSS variable in both dark/light themes (`variables.css`).
- `scripts/audit-css-collisions.sh` — finds duplicate class selectors
  across kernel CSS files and outputs a JSON collision report.

### Fixed

- `packages/sdk-test-runner/css/main.css`: Wrong variable names
  (`--text3` → `--text-3`, `--text2` → `--text-2`, `--bg1`/`--bg2` →
  `--bg-raised`).
- `packages/icd-test-runner/css/main.css`: Replaced inline button fallback
  styles with kernel `.sw-btn` system.

### Removed

- Old button classes: `.btn-primary`, `.btn-small`, `.btn-danger`,
  `.btn-full`, `.btn-ghost`, `.btn-subtle`, `.btn-sm`, `.btn-md`.
- Old toast classes: `.toast-container`, `.toast`, `.toast.error/warning/success`.
- Old popup menu classes: `.popup-menu`, `.popup-menu-item`, `.popup-menu-*`.
- Old dropdown and tabs definitions from `primitives.css` that collided
  with `sw-primitives.css`.

## v0.6.10 — Viewport Foundation

Single layout model. Every surface renders inside one containment chain:
`body → shell → surface`. No dual systems. No transform hacks.

### Changed

- **CSS `zoom` replaces `transform: scale()`**: UI scale (80%–175%) now
  uses CSS `zoom` on `#surfaceInner` instead of `transform: scale()`.
  `zoom` reflows layout correctly — `getBoundingClientRect()` returns
  accurate values, eliminating the scale-correction hack in `menu.js`.
  Supported in all evergreen browsers (Firefox 126+, June 2024).
- **Single layout root**: `<body>` in `base.html` is the authoritative
  flex column layout. `.sw-shell` CSS demoted from viewport-level
  container (`height: 100vh`) to fill-parent (`height: 100%`).
  Safe-area insets moved from `.sw-shell` to `<body>`.
- **Banner single source of truth**: Template banners measure their own
  height via inline `<script>` and set `--banner-top-height` /
  `--banner-bottom-height` CSS variables. Removed `--banner-h: 28px`
  fixed variable. `ShellBanner` Preact component's `useEffect`
  measurement removed (dead code — no surface imports `AppShell`).
- **`sw-shell__banner` position**: Changed from `position: fixed` to
  `position: static` — template banners are in-flow elements.
- **`sw-shell__body` padding**: Removed `padding-top/bottom` for
  banner offsets — unnecessary with in-flow banners.
- **Extension surfaces `100vh` → `100%`**: `chat-app`, `chat-loading`,
  `surface-dashboard` now use `height: 100%` to inherit from the
  extension mount container (like Notes). Fixes overflow behind banners.
- **`100vh` → `100dvh` fallbacks**: All viewport-height declarations
  use `height: 100vh; height: 100dvh;` pattern for correct behavior on
  mobile browsers. Affects: `base.html`, `sw-login.css`,
  `workflow.html`, `workflow-landing.html`, `primitives.css`,
  `git-board/css/main.css`.
- **`sw.shell.getScale()` deprecated**: Returns `1` always — CSS `zoom`
  handles layout reflow without manual correction.

### Deprecated

- `src/js/sw/shell/app-shell.js`, `app.js`, `surface-viewport.js` —
  no surface imports these. Layout root is `<body>` in `base.html`.

## v0.6.9 — Session Lifetime Config

Admin-configurable session durations, "keep me logged in" opt-in, and
optional idle timeout. Completes the auth hardening started in v0.6.8.

### Added

- **Admin session settings**: `session.access_token_ttl` (default 15m,
  clamp 5m–60m) and `session.refresh_token_ttl` (default 7d, clamp
  1h–90d) stored in `global_settings`. New `LoadSessionConfig()` helper
  reads and clamps values with `parseDurationString()` supporting `m`,
  `h`, `d` suffixes.
- **"Keep me logged in" checkbox**: Login form opt-in. Checked = full
  `refresh_token_ttl`. Unchecked = capped at 24h. Cookie `max-age`
  tracks whichever lifetime was chosen. `keep_login` flag stored on
  refresh token row.
- **Config-driven token generation**: `generateTokens()` reads TTLs from
  `global_settings` instead of hardcoded `15*time.Minute` /
  `7*24*time.Hour`. Response includes `expires_in` and
  `refresh_expires_in` (seconds) so the client schedules refresh
  correctly.
- **Idle timeout (optional)**: Admin-toggleable (default off). Server
  checks `last_activity_at` on refresh-token row; rejects if gap exceeds
  `session.idle_timeout`. Client SDK pings `POST /api/v1/auth/activity`
  on click/keydown (debounced, max 1/min).
- **Admin Settings > Session section**: Dropdowns for access TTL, refresh
  TTL, and idle timeout with toggle.
- **7 new tests**: Duration parsing, clamping (low/high), defaults,
  invalid values, empty idle timeout.

### Changed

- `CreateRefreshToken` store method now accepts `keepLogin bool`
  parameter; both Postgres and SQLite implementations updated.
- `GetRefreshTokenInfo` returns `RefreshTokenInfo` struct with
  `UserID`, `KeepLogin`, `LastActivityAt` for idle-timeout decisions.
- SDK `auth.login()` accepts optional third `keepLogin` parameter;
  cookie max-age derived from server `refresh_expires_in` response.
- SDK boots activity tracking after successful auth boot.

## v0.6.8 — Cookie Fix + UI Hardening Roadmap

### Fixed

- **Session cookie max-age bug**: `arm_token` cookie was set to 15 min
  (matching access token) while refresh token lasted 7 days. Cookie now
  matches refresh token lifetime so Go SSR middleware can serve page
  shells while JS refreshes the access token client-side.

### Added

- **ROADMAP-UI.md**: Detailed UI hardening roadmap (v0.6.9–v0.6.15)
  covering session config, viewport foundation, CSS deduplication,
  extension CSS isolation, responsive layout, visual polish, and
  automated usability survey gate.

## v0.6.7 — Native mTLS

End-to-end mutual TLS without a reverse proxy. Targets systemd+podman
deployments where the Go binary terminates TLS itself.

### Added

- **`TLS_MODE` config**: Three values — `none` (default, plain HTTP), `server`
  (TLS, no client cert), `mtls` (mutual TLS, client cert required). Independent
  of `AUTH_MODE`. Combinations: `server`+`builtin` for HTTPS with password auth,
  `mtls`+`mtls` for full mTLS identity, `none`+`mtls` for proxy-terminated
  (existing behavior).
- **TLS server mode**: Binary calls `ListenAndServeTLS` directly when
  `TLS_MODE` is `server` or `mtls`. TLS 1.3 minimum, no cipher suite
  configuration. Config: `TLS_CERT`, `TLS_KEY`, `TLS_CA` path env vars.
- **`MTLSNativeProvider`**: New auth provider that reads
  `r.TLS.PeerCertificates[0]` directly — no header trust. `Subject.CommonName`
  becomes username, `sha256(cert.Raw)` becomes `external_id`. Auto-provisions
  users identically to the proxy provider. Selected when `AUTH_MODE=mtls` and
  `TLS_MODE=mtls`.
- **Shared mTLS helpers**: `ParseDN()`, `FingerprintCert()`, and
  `resolveOrProvision()` extracted to `mtls_helpers.go`. Both proxy and native
  providers consume the same user resolution logic.
- **Peer TLS config**: `BuildPeerTLSConfig()` constructs a `*tls.Config` for
  outbound node-to-node connections (forward-looking — cluster registry is
  currently DB-backed with no HTTP peer calls).
- **`armature-ca.sh`**: Shell wrapper around openssl for cert provisioning.
  Three commands: `init` (CA keypair), `issue-node` (365d, ServerAuth +
  ClientAuth EKU), `issue-user` (90d, ClientAuth only). All ECDSA P-256, PEM
  output.
- **12 new tests**: `FingerprintCert` determinism, native provider unit tests
  (nil TLS, empty certs, no CN, email extraction), TLS integration tests (no
  cert rejected, wrong CA rejected, expired cert rejected, valid cert accepted,
  peer certificate visibility).

### Changed

- **`MTLSProvider` → `MTLSProxyProvider`**: Renamed for clarity. Config type
  `MTLSConfig` → `MTLSProxyConfig`. Constructor `NewMTLSProvider` →
  `NewMTLSProxyProvider`. File `mtls.go` → `mtls_proxy.go`.

## v0.6.6 — Final Hardening

Final pass before public release. Security, correctness, and developer
experience. No new features — only fixes, validation, and cleanup.

### Added

- **`ValidateManifest()` gate**: Centralized manifest validation function
  (`package_validate.go`) called at both upload-install and bundled-install
  time. Catches malformed manifests early with clear error messages. 12 unit
  tests covering all type constraints and edge cases.
- **Extension dependency auto-activation**: Installing a package whose
  `dependencies` list an uninstalled library will auto-install that library
  from the bundled packages directory. If the dependency is not bundled, the
  error message lists exactly what's missing.
- **`OptionalAuth` middleware**: New page middleware for workflow visitor
  routes. Authenticates if a token is present, allows anonymous pass-through
  otherwise. Replaces the stale `AuthOrRedirect` TODO for Session routes.
- **Package signing schema reservation**: `signature` field accepted in
  manifest (no-op). `PACKAGE_VERIFY_SIGNATURES` env var logs warnings for
  unsigned packages when enabled. No cryptographic verification yet — schema
  slot reserved to prevent a breaking change later.
- **ICD/SDK runner v0.6.x coverage**: Smoke tier adds metrics, cluster,
  backups, docs, and OpenAPI JSON endpoints. SDK admin domain adds metrics,
  cluster, and backups dual-path tests.

### Fixed

- **OIDC nonce validation**: ID token nonce claim is now validated against
  the stored authorization request nonce. Previously the nonce was generated
  and stored but never checked on callback — a token replay/substitution
  vulnerability.

### Changed

- **Schema migration stub**: `RunSchemaMigrations()` no longer pretends to
  work. Replaced with a log-only function documenting that only additive
  schema changes are supported. Downgrade rejection preserved.
- **Session middleware TODO resolved**: `main.go` Session slot now uses
  `OptionalAuth` instead of `AuthOrRedirect`. OIDC nonce TODO and migration
  stub TODO also resolved.

## v0.6.5 — Renderer Pipeline + Docs Rewrite

Lifts block rendering to a kernel SDK primitive so all surfaces share one
markdown pipeline. Rewrites docs for external audience. Adds CONTRIBUTING
guide and extension tutorial.

### Added

- **`sw.renderers` SDK module**: Kernel-level renderer registry. Extensions
  register block renderers (fenced code blocks) and post renderers (DOM
  post-processing) once; all surfaces consume via `sw.markdown`.
- **`sw.markdown` SDK module**: Unified markdown rendering via `marked` v16
  (vendored). Lazy-loads `marked` + DOMPurify. Custom code hook delegates
  fenced blocks to `sw.renderers`. Wikilink tokenizer (`[[Page Name]]`)
  built in for all surfaces.
- **Browser extension script loader**: Server injects `<script>` tags for
  enabled browser-tier extensions into all pages. Extensions register via
  `sw:ready` event.
- **Chat markdown support**: Messages with `content_type: 'markdown'` render
  via `sw.markdown`. Existing `text` messages stay plain (no behavior change).
- **Mermaid architecture diagrams**: Six diagrams in `docs/ARCHITECTURE.md`:
  system overview, request flow, extension lifecycle, realtime events,
  settings cascade, cluster topology.
- **`CONTRIBUTING.md`**: Development setup, project structure, test commands,
  code conventions.
- **`docs/TUTORIAL-FIRST-EXTENSION.md`**: Step-by-step guide to building and
  installing a browser extension.
- **`docs/DESIGN-WORKFLOWS.md`**: Clean workflow system design document.
- **13 new renderers tests** (`src/js/__tests__/renderers.test.js`).

### Changed

- **Notes surface**: Hand-rolled markdown renderer replaced with
  `sw.markdown.renderSync()`. Post-renderers (mermaid, katex, etc.) now
  execute on preview. ~90 lines deleted.
- **Docs surface**: Hand-rolled markdown renderer replaced with
  `sw.markdown.renderSync()`. ~160 lines deleted.
- **4 renderer extensions** (mermaid, katex, csv-table, diff-viewer):
  Rewritten from dead `Extensions.register()` pattern to IIFE +
  `sw.renderers.register()` via `sw:ready` event. All rendering logic
  preserved.
- **Surface alias routes removed**: Six `/api/v1/admin/surfaces/*` backward
  compat routes deleted from `main.go`. SDK and ICD runner migrated to
  canonical `/api/v1/admin/packages/*` endpoints.
- **Docs rewritten for external audience**: All references to fork history,
  internal codenames, and pre-fork version numbers removed from docs,
  CHANGELOG, and ROADMAP.

### Removed

- `docs/DESIGN-WORKFLOW-REDESIGN-0.2.6.md` — replaced by `DESIGN-WORKFLOWS.md`.
- Hand-rolled markdown renderers in Notes and Docs surfaces.
- Six `/admin/surfaces/*` API alias routes.

---

## v0.6.4 — Admin Health/Metrics Tab + Cluster Merge

Structural consolidation: cluster dashboard merges into Admin as a Health tab.
Comprehensive metrics endpoint for runtime, DB, cluster, and extension stats.

### Added

- **Admin Health tab**: New "Health" section under Monitoring in the Admin
  surface. Auto-refreshing panels for runtime, database, cluster, and extension
  metrics with configurable poll interval (5/10/30/60s).
- **`GET /api/v1/admin/metrics`**: Single JSON endpoint returning all platform
  metrics — runtime (goroutines, heap, GC, uptime, FDs), DB pool (latency,
  active/idle/max, PG-only dead tuples + active backends), cluster nodes
  (when PG multi-node), and extension stats (Starlark exec/errors/duration,
  trigger fires, event bus publish/deliver counts).
- **Fattened heartbeat payload**: Cluster heartbeat JSONB now carries stack
  usage, GC CPU%, extensions loaded, Starlark execution counters, and trigger
  fire count — visible in per-node cluster cards.
- **Sandbox execution tracking**: `ExecPackage` and `CallEntryPoint` now
  increment atomic counters (exec, errors, duration) and the previously
  declared but unused `SandboxExecutionsTotal` Prometheus counter.
- **Event bus counters**: `Bus.PublishCount()` and `Bus.DeliverCount()` track
  cumulative publish/deliver operations.
- **Trigger fire counter**: `Engine.FireCount()` tracks cumulative trigger
  fires across webhook, event, and scheduled triggers.
- 4 new handler tests (metrics SQLite shape, cluster shape, extension counters).
- 1 new cluster test (fattened heartbeat payload).

### Changed

- **Health endpoint consolidation**: `/health` and `/api/v1/health` now use a
  shared `buildHealthResponse()` function returning identical JSON. Both now
  include `registration_enabled` when the database is connected.
- **Block renderer `requires` removed**: `mermaid-renderer`, `katex-renderer`,
  `csv-table`, and `diff-viewer` no longer require `["chat"]` — they activate
  on any surface (notes, docs, etc.).

### Removed

- **`cluster-dashboard` package**: Standalone surface package retired. Cluster
  node visibility is now in Admin > Monitoring > Health.

---

## v0.6.3 — Dead Code Sweep + Registry Fix

Hardening: fix broken registry install, add registry settings UI,
clean up dead code, narrow default bundle.

### Fixed

- **Registry install**: SDK was sending `{ url }` but the Go handler expects
  `{ download_url }`, causing every registry Install click to return 400.

### Added

- **Package Registry settings**: New "Package Registry" section in Admin >
  Settings with a URL input field for configuring the external registry.
- **`scripts/generate-registry.sh`**: Shell script that reads `.pkg` ZIPs,
  extracts manifests, and emits a `registry.json` for self-hosted discovery.
- **`docs/PACKAGE-REGISTRY.md`**: Documents registry JSON format, admin
  configuration, script usage, and self-hosting setup.

### Removed

- **Dead Go code**: Orphaned channel/message type comments from
  `interfaces.go`, unused `roleFilterType()` from `pages.go`, stale version
  comment from `main.go`.
- **Dead vendor JS**: `marked.min.js` (40KB) and `purify.min.js` (22KB) —
  unused vendor copies with zero production imports. Removed cache entries
  from `sw.js`.
- **`dev.html`**: 676-line standalone dev gallery, not referenced by anything.
- **Stale version comments**: Stripped legacy version annotations across
  ~140 files — standalone comments, inline suffixes, section headers, and
  config field docs. Reworded to describe features rather than reference
  obsolete internal version numbers.

### Changed

- **Default bundle narrowed** from 10 packages to 5: `notes`, `chat`,
  `chat-core`, `mermaid-renderer`, `schedules`. All other packages remain
  available via `BUNDLED_PACKAGES=*` or explicit list.

---

## v0.6.2 — Docs Polish + Dynamic OpenAPI

Docs surface polish and dynamic OpenAPI spec with extension route merging.

### Added

- **Dynamic OpenAPI spec**: `GET /api/docs/openapi.json` builds a merged
  spec at request time — kernel endpoints + auto-generated stubs for every
  extension `api_routes` entry. Extensions may optionally declare `api_schema`
  in `manifest.json` for richer path items (params, body, response examples).
- **`api_schema` manifest field**: Optional array in `manifest.json`. Malformed
  entries are logged and skipped — never blocks extension loading.
- **Tests**: 7 new handler tests (zero extensions, stubs, rich schema,
  multi-extension, malformed schema, disabled exclusion, required fields).

### Fixed

- **Dark mode contrast**: Replaced all hardcoded light-mode fallbacks in docs
  CSS with theme-aware CSS variables. Tables, code blocks, nav items, and
  headings now readable in dark mode.
- **Docs loading state**: Replaced borrowed `settings-placeholder` animation
  with docs-specific pulse skeleton. Added error state with retry button.
- **Docs routing**: Default route (`/docs`) now redirects to
  `/docs/GETTING-STARTED` instead of the blank `/:section` placeholder.
- **Docs scrolling**: Content area now scrolls correctly — full document
  visible without text overflowing viewport.
- **Docs icon**: Changed from 🧩 to 📖 in the user menu.
- **Docs duplicate menu entry**: Docs was appearing twice (once from surfaces
  API, once from hardcoded items). Added `'docs'` to `CORE_IDS` filter.
- **Swagger UI**: Updated to point at dynamic `/api/docs/openapi.json`
  endpoint. Static YAML preserved for backward compatibility.
- **OpenAPI tests on Postgres**: `openapiTestStores()` was hardcoded to
  `sqlite.NewStores()` — caused `pq: syntax error at or near ","` in CI.
  Now uses `database.IsSQLite()` check like all other handler tests.

### API

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/docs/openapi.json` | GET | Dynamic merged OpenAPI 3.0 spec |

---

## v0.6.1 — Backup/Restore + Documentation

Operational tooling for data safety and extension authoring.

### Added

- **Backup/Restore**: Full-instance backup as `.swb` ZIP archive containing
  core tables (JSONL), ext_data tables, and package assets. Stream to client
  or save server-side. Restore wipes and rebuilds from archive. Dialect-neutral
  (works across SQLite and Postgres).
- **Admin backup section**: New "Backup" tab under `/admin` with create,
  download, delete, and restore operations. Destructive restore requires
  confirmation dialog.
- **Documentation surface**: Builtin surface at `/docs` with sidebar navigation
  and client-side markdown rendering. Ships with 5 documents: Getting Started,
  Extension Guide, API Reference, Deployment, Package Format.
- **Docs API**: `GET /api/v1/docs` lists available documents, `GET /api/v1/docs/:name`
  returns raw markdown content. Authenticated (all users).
- **Tests**: 6 backup handler tests + E2E script (`ci/e2e-backup-test.sh`).

### API

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/v1/admin/backup` | POST | Create backup (stream or `?store=true`) |
| `/api/v1/admin/backups` | GET | List server-side backups |
| `/api/v1/admin/backups/:name` | GET | Download backup |
| `/api/v1/admin/backups/:name` | DELETE | Delete backup |
| `/api/v1/admin/restore` | POST | Restore from `.swb` upload |
| `/api/v1/docs` | GET | List documentation |
| `/api/v1/docs/:name` | GET | Get document content |

---

## v0.6.0 — Cluster Registry + HA

MVP convergence point. PG-backed cluster registry for horizontal scaling —
zero new infrastructure (no etcd/Consul/Redis).

### Added

- **Cluster registry**: `node_registry` UNLOGGED table with self-registration
  on startup (`INSERT ... ON CONFLICT DO UPDATE`), heartbeat tick (default 10s),
  stale sweep (default 30s threshold), and self-eviction (`os.Exit(1)` when
  swept by a peer — K8s restarts the process).
- **Cluster admin API**: `GET /api/v1/admin/cluster` returns all registered
  nodes with runtime stats (goroutines, heap, GC, uptime, ws_clients) in the
  standard `{data: [...]}` envelope.
- **Health endpoint cluster info**: `GET /health` and `GET /api/v1/health`
  now include `node_id` and `cluster: {size, peers, heartbeat_age_ms}` when
  running on Postgres with the registry active.
- **Cluster dashboard**: `cluster-dashboard` admin surface package renders one
  card per node with live runtime stats. Auto-refreshes every 10s. Stats are
  JSONB — new keys render automatically without schema migration.
- **Cluster config**: `CLUSTER_NODE_ID` (default hostname-PID),
  `CLUSTER_HEARTBEAT_INTERVAL` (default 10s), `CLUSTER_STALE_THRESHOLD`
  (default 30s), `CLUSTER_ENDPOINT` (Phase 2 mesh, auto-detect).
- **3-replica E2E test**: `docker-compose-e2e.yml` now runs 3 replicas.
  `ci/e2e-cluster-test.sh` verifies registration, stale sweep on stop, and
  re-registration on restart.
- **5 new tests**: 3 cluster registry unit tests (stats collection, start/stop
  lifecycle, node ID) + 2 handler tests (list nodes, empty response).
- **Curated bundle expanded**: `chat` and `cluster-dashboard` added to the
  default bundle (now 10 packages). Production deployments can override with
  `BUNDLED_PACKAGES=notes,chat,chat-core,cluster-dashboard` for a lean set.

### Changed

- SQLite deployments are unaffected — cluster store is nil, all cluster code
  is guarded behind `database.IsPostgres() && stores.Cluster != nil`.
  Cluster dashboard gracefully shows "0 nodes registered" on SQLite.

## v0.5.5 — Upgrade Testing

### Fixed

- **Bundled permission auto-grant on Postgres**: The `InstallBundledPackages`
  auto-grant SQL used SQLite-style `granted = 1` / `granted = 0` which fails
  on Postgres BOOLEAN columns. Now dialect-aware: `true`/`false` on Postgres,
  `1`/`0` on SQLite. This caused bundled extensions (notes, chat-core, etc.)
  to install with permissions not granted on Postgres deployments.
- **E2E test API field corrections**: Fixed `ci/e2e-chat-test.sh` login field
  (`"username"` → `"login"`), token extraction (`"token"` → `"access_token"`),
  and profile endpoint (`/api/v1/me` → `/api/v1/profile`).

### Added

- **Upgrade test harness**: `docker-compose-upgrade.yml` + `ci/e2e-upgrade-test.sh`
  orchestrate a full upgrade cycle: build old image, seed data (notes,
  conversations, messages, settings), stop old, start new, verify data integrity
  post-upgrade. Covers auth, notes, conversations, packages, and settings.
- **Rolling upgrade test**: `ci/e2e-upgrade-rolling.sh` tests rolling upgrade
  with 2 replicas sharing Postgres. Upgrades one replica at a time, verifies
  cross-replica reads during the mixed-version window.
- **11 new Go tests** in `upgrade_test.go`:
  - Schema edge cases: add index (idempotent), add column (idempotent),
    multi-column add, row preservation across migration.
  - Settings migration: global/team/user overrides preserved across package
    update, new keys get defaults, removed keys not deleted.
  - Package compatibility: bundled skip-if-present on restart, dormant status
    for unmet requires, enabled/type preserved across version bump.
  - Direct `MigrateExtTables` tests: new table creation, column add with row
    preservation, index idempotency.

## v0.5.4 — Package Updates

### Added

- **Package update API**: `POST /api/v1/admin/packages/:id/update` accepts a
  `.pkg` archive, validates semver version bump (rejects same/older), applies
  additive schema migration, merges settings, replaces assets, and re-syncs
  permissions/triggers/dependencies.
- **Package export API**: `GET /api/v1/admin/packages/:id/export` streams the
  installed package as a downloadable `.pkg` ZIP archive. Completes the manual
  rollback story: export before update, re-install old `.pkg` if needed.
- **Semver parsing**: `ParseSemver` / `Compare` in `handlers/semver.go` with
  prerelease support. Used by the update handler to enforce version bumps.
- **Additive schema migration**: `MigrateExtTables` diffs declared `db_tables`
  against existing ext_data schema. Adds new columns (`ALTER TABLE ADD COLUMN`)
  and new tables. No destructive changes — columns in DB but absent from
  manifest are preserved. `ListExtColumns` introspects via `PRAGMA table_info`
  (SQLite) or `information_schema` (Postgres).
- **Settings merge on update**: New manifest setting keys get their declared
  default value; existing admin-configured values are preserved.
- **Admin UI Update button**: Non-core package cards now show an Update button
  with a confirm dialog before upload.
- **14 new Go tests**: Version bump, 5 rejection cases (same/older/core/type/id
  mismatch), schema add column with data preservation, schema add table,
  settings merge, export with valid zip, default allowlist, wildcard allowlist.

### Changed

- **Curated default bundle**: Default bundled packages reduced from 23 to 8
  core packages (notes, chat-core, workflow-chat, dashboard, 4 demo workflows).
  `BUNDLED_PACKAGES=*` installs all; explicit comma-separated list for custom.
  Existing deployments are unaffected (install-once, skip-if-present).
- **`BUNDLED_PACKAGES` env var**: Empty value now installs curated defaults
  instead of all packages. Use `"*"` for previous install-all behavior.

## v0.5.3 — Chat Polish + Integration Testing

### Added

- **`db.query()` search_like**: New kernel-level `search_like` kwarg for text
  search. Accepts `{col: pattern}` dict, generates OR-joined `LIKE` (SQLite) /
  `ILIKE` (Postgres) clauses. Composes with existing `filters` via AND. Reusable
  by any package. 5 new sandbox tests.
- **Conversation search**: `GET /search?q=term` endpoint in `chat-core`. Searches
  conversation titles and message content, scoped to user's conversations.
  Returns `{conversations: [...], messages: [...]}`.
- **Search UI**: Debounced search input in chat sidebar. Results view replaces
  conversation list showing matched conversations and message snippets with
  section headers. Click result to navigate, clear button to dismiss.
- **`workflow-chat` library package**: `on_advance` hook that creates scoped
  group conversations when workflow stages require team collaboration. Adds all
  team members as participants, sends system message linking to the instance,
  enriches `stage_data` with `conversation_id`. Idempotent — skips if
  conversation already exists.
- **Multi-user E2E test infrastructure**: `docker-compose-e2e.yml` with 2
  replicas, Postgres, and nginx load balancer. `ci/e2e-chat-test.sh` shell
  script covering auth, conversation CRUD, message send/read, cross-replica
  consistency, search, and pagination. `ci/e2e-ws-listener.js` Node.js
  WebSocket client for realtime event testing.
- **Workflow hook tests**: 6 new Go tests for `parseOnAdvanceResult` covering
  None, nil, error, enriched stage_data, non-dict, and nested structure cases.

### Changed

- **Message pagination polish**: Scroll position now preserved when loading older
  messages — records `scrollHeight` before prepend, restores via
  `requestAnimationFrame`. Loading spinner shown at top of thread during fetch.
  "Load older messages" button hidden while loading.
- **chat-core** bumped to v0.2.0 (new search endpoint).
- **chat** bumped to v0.2.0 (search UI, pagination polish).

## v0.5.2 — Chat Surface

### Added

- **`chat` surface package**: Full messaging UI built on `chat-core` library.
  Route `/s/chat`, icon `💬`, depends on `chat-core`.
- **Conversation list sidebar**: Conversations sorted by recent activity, last
  message preview, relative timestamps, unread count badges. "New" button
  for conversation creation.
- **Message thread**: Paginated message history with cursor-based load-more on
  scroll-to-top. Participant avatars, display names, timestamps. Scroll-to-
  bottom on new messages.
- **Compose bar**: Auto-resizing textarea, Enter-to-send, Shift+Enter for
  newline. Typing indicator broadcast on keypress (3s debounce).
- **Participant sidebar**: Collapsible right panel with participant list, online
  status via presence hub, role badges. Add participant via `sw.ui.Dialog` +
  `sw.ui.UserPicker`. Remove participant with confirmation.
- **Create conversation dialog**: Group or Direct Message type selector. Multi-
  user picker with chip display for group, single picker for DM. Auto-title
  from participant names.
- **Typing indicators**: Realtime broadcast via `POST /typing/:id` Starlark
  endpoint. Frontend subscribes to `conversation:{id}` channel, shows
  "X is typing..." with 4s auto-expire. Handles multiple typers.
- **Read receipts**: Auto mark-read on conversation focus and new incoming
  messages. Unread counts in conversation list, cleared on selection.
- **Message editing**: Inline edit mode on own messages with textarea. Save on
  Enter, cancel on Escape. "(edited)" label on edited messages. Realtime
  propagation to other participants.
- **Message deletion**: Confirm dialog before delete. "This message was deleted"
  placeholder. Admin can delete any message. Realtime propagation.

### Fixed

- **Stale token login deadlock**: REST client's 401→refresh→retry loop caused
  a deadlock when the refresh endpoint itself returned 401 (stale DB). Auth
  endpoints (`/auth/login`, `/auth/refresh`, `/auth/register`) are now excluded
  from the retry loop. Defensive 8s timeout on `auth.boot()` ensures login
  page always renders even on unexpected hangs.
- **Bundled package permissions**: Bundled packages were installed with
  `pending_review` status and `granted=0` permissions, requiring manual admin
  approval. Bundled installer now auto-grants all declared permissions and
  sets status to `active` on install.
- **Creator display name**: `chat-core.create()` now accepts
  `creator_display_name` so the conversation creator shows a readable name
  in the participant sidebar instead of a raw UUID.
- **Chat dark mode theming**: Replaced non-existent CSS variable names with
  actual theme tokens from `variables.css`. Fixes invisible text and wrong
  backgrounds in dark mode.
- **Dialog dropdown clipping**: UserPicker autocomplete dropdown in dialogs
  (New Conversation, Add Participant) no longer clipped by `overflow: auto`
  on `.sw-dialog__body`.
- **User menu at scaled UI**: Menu primitive now divides anchor coordinates
  and viewport dimensions by `sw.shell.getScale()` to compensate for CSS
  `transform: scale()` on `#surfaceInner`. Fixes menu clipping at 110%+.

### Changed

- **Named Docker volume**: `docker-compose.yml` switched from bind mount
  (`./data:/data`) to named volume (`sb_data:/data`). Fixes volume mount
  failures when Docker daemon runs on a remote host (DinD, k8s pod).
  Deterministic wipe via `docker compose down -v`.

## v0.5.1 — Chat Core Library

### Added

- **`chat-core` library package**: Conversations, messages, participants, and
  read cursors as ext_data tables. Permissions: `db.write`, `realtime.publish`.
  Consumable by other packages via `lib.require("chat-core")`.
- **4 ext_data tables**: `conversations` (title, type, created_by, updated_at),
  `participants` (conversation_id, participant_id, type, display_name, role,
  joined_at), `messages` (conversation_id, participant_id, content, content_type,
  edited_at), `read_cursors` (conversation_id, participant_id,
  last_read_message_id).
- **6 exported Starlark functions**: `create()`, `send()`, `history()`,
  `add_participant()`, `remove_participant()`, `mark_read()`.
- **15 REST endpoints**: Full CRUD for conversations, messages, participants.
  Cursor-based paginated message history. Per-conversation unread counts.
- **Realtime events**: `message`, `message.edited`, `message.deleted`,
  `participant.added`, `participant.removed` published to
  `conversation:{id}` channels.
- **`db.query()` range parameters**: New `before` and `after` kwargs accept
  dicts of `{col: val}` generating `col < ?` / `col > ?` WHERE clauses.
  Enables cursor-based pagination and efficient unread counting for all
  extension packages. 7 new Go tests.

### Security

- **Invisible Unicode scanning gate** (GlassWorm / Trojan Source defense):
  `ScanSource()` detects variation selectors, bidi overrides, zero-width chars,
  tag characters, and other invisible Unicode across 10 ranges. `Verdict()`
  blocks GlassWorm payloads (>=10 variation selector / tag chars), Trojan Source
  (any bidi override), and excessive zero-width chars (>=6). Two enforcement
  points: extension install (422 rejection before files written) and Starlark
  execution (belt-and-suspenders). 23 new tests including GlassWorm payload
  simulation.

### Changed

- **Admin packages type filter**: Added `Library` option alongside Surface/Extension/Full/Workflow. Filter dropdown replaced button group with themed `Dropdown` primitive (keyboard nav, CSS-var theming, consistent with rest of admin UI).
- SDK version bumped from 0.5.0 to 0.5.1.

## v0.5.0 — Realtime Primitive + Dialog Audit + Permissions UI

### Added

- **Realtime Starlark module**: `realtime.publish(channel, event, data)` lets
  extensions publish events to WebSocket channels. Gated by the new
  `realtime.publish` permission. Payloads limited to 7KB (pg_notify safe).
  Reserved event prefixes (`system.`, `workflow.`, etc.) are blocked.
- **WS room protocol**: Clients send `room.subscribe` / `room.unsubscribe`
  messages to join/leave rooms. Intercepted in the WebSocket readPump before
  the bus publish gate (like ping). Per-connection cap of 100 rooms.
- **SDK realtime module**: `sw.realtime.subscribe(channel, [event], callback)`
  manages room join/leave over WebSocket and filters incoming `realtime.*`
  events by room. Returns unsubscribe handle. Auto re-joins all rooms on
  WebSocket reconnect. `sw.realtime.channels()` debug helper.
- **Admin permissions UI**: Packages page gains a "Permissions" button for
  packages with declared permissions. Inline drawer shows each permission with
  Grant/Revoke toggle, granted-by user ID, and a "Grant All" bulk action.
- **Status badges**: `pending_review` (amber) and `suspended` (red) badges on
  package rows. Enable toggle disabled with hint when `pending_review`.
- **SDK API client**: `permissions()`, `grantPerm()`, `revokePerm()`,
  `grantAllPerms()` methods added to `sw.api.admin.packages`.
- **`starlarkToGoVal()` helper**: Converts Starlark values to Go `any` for
  JSON marshaling (reverse of existing `goValToStarlark`).
- **8 new Go tests**: Route table cases for `realtime.*` and `room.*`,
  realtime module publish (happy path, empty data, missing args, reserved
  prefix, payload too large), `starlarkToGoVal` type conversion.

### Changed

- **Dialog audit**: 5 bare `confirm()` calls migrated to `await sw.confirm()`
  with `{ destructive: true }` styling in tasks, schedules, notes (×2), and
  editor packages. Package archives rebuilt.
- SDK version bumped from 0.2.3 to 0.5.0.
- Event route table: `realtime.` → DirToClient, `room.subscribe` /
  `room.unsubscribe` → DirFromClient.
- Runner gains `bus` field and `SetBus()` method; wired in main.go.

## v0.4.7 — Note Graph

### Added

- **Graph API**: `GET /graph` endpoint returns all non-archived notes as nodes
  and resolved wikilinks as edges in a single payload. Nodes include id, title,
  folder_id, and tags. Edges include source, target, and link text.
- **Graph view**: Canvas-based force-directed graph visualization replaces the
  editor pane when "Graph" button is clicked in the topbar. Force simulation
  uses repulsion (all pairs), attraction (edges), and center gravity with
  velocity damping. Supports zoom (scroll wheel) and pan (mouse drag).
- **Click focus**: Single-click a node to dim unconnected nodes/edges to 15%
  opacity; clicked node and direct neighbors stay at full brightness. Edges
  between focused nodes highlighted. Click empty space to reset.
- **Shift+click chain**: Hold shift and click a second node to union both
  neighborhoods — trace thought paths across two hops.
- **Double-click open**: Double-click a graph node to navigate to that note in
  the editor. Graph view closes, editor opens with the selected note.
- **Hover tooltips**: Hovering a node shows title, tag pills, and connection
  count in an HTML overlay (no additional API calls).
- **Folder coloring**: Nodes colored by folder using a 10-color palette.
  Unfiled notes shown in gray.
- **Tag filter integration**: Active tag filter dims non-matching nodes to 30%
  opacity, stacking with click focus.
- **Orphan highlighting**: Notes with zero connections shown with dashed stroke.
  "Hide orphans" checkbox in toolbar to toggle visibility.

### Changed

- Notes package version bumped from 0.7.0 to 0.8.0.
- Topbar gains a "Graph" toggle button alongside "New Note" and "Import .md".


## v0.4.6 — Sidebar Restructure

### Added

- **Sidebar tabs**: Two-tab header ("Notes" / "Outline") replaces the static
  sidebar header. Notes tab contains folder tree, tag filter, search, and note
  list. Outline tab shows the document heading tree for the active note.
  Outline tab disabled (grayed) when no note is selected.
- **Sidebar outline**: Document outline relocated from editor-side panel into
  the sidebar's Outline tab. Nested heading hierarchy (H1→H2→H3) with depth
  indentation via `padding-left`. Click heading scrolls to it in both preview
  and CM6 editor modes.
- **Active heading tracking**: Scroll listener (RAF-throttled) highlights the
  current heading in the sidebar outline based on scroll position. Uses
  `getBoundingClientRect()` in preview/split mode and CM6 viewport line in
  edit mode.

### Changed

- Notes package version bumped from 0.6.0 to 0.7.0.
- `EditorPane` exposes headings, scroll function, and active heading index to
  parent via callback props (`onHeadingsChange`, `scrollToHeadingRef`,
  `onActiveHeadingChange`).
- Sidebar auto-resets to Notes tab when active note is deleted.

### Removed

- Old `DocumentOutline` component and `.doc-outline` CSS styles (replaced by
  sidebar outline).

## v0.4.5 — Editor Modes + Document Outline

### Added

- **Tri-state view mode**: Notes open in rendered (read-only) mode by default.
  Toolbar button cycles through Edit (CM6) and Split (side-by-side) modes.
  Replaces the old binary preview toggle.
- **Split view**: Side-by-side CM6 editor + rendered preview using CSS grid.
  CM6 instance preserved when toggling between edit and split modes.
- **Synced scroll**: In split mode, scrolling the CM6 editor proportionally
  scrolls the preview pane via linear ratio mapping on `scrollDOM`.
- **View mode persistence**: User's preferred mode saved to localStorage
  (`sw.storage.local('notes')`). New `editor_mode` manifest setting with
  admin-level default (rendered / edit / split).
- **Document outline**: Collapsible TOC panel adjacent to the editor body.
  `parseHeadings()` extracts heading level, text, and line number (skips
  fenced code blocks). Click heading to scroll — uses CM6
  `EditorView.scrollIntoView()` in edit/split mode, DOM `scrollIntoView()`
  in rendered mode. Updates on body change with 300ms debounce.

### Changed

- Notes package version bumped from 0.5.0 to 0.6.0.
- Editor body wrapped in `.notes-editor__content` flex container to
  accommodate the outline panel.
- Wikilink click handling active in both rendered and split modes.
- Mobile: split view collapses to single column, outline panel hidden.

---

## v0.4.4 — Rich Editor + Import/Export

### Added

- **CodeMirror 6 integration**: Notes editor now uses the vendored CM6 bundle
  (`CM.noteEditor()`) for rich markdown editing with syntax highlighting,
  wikilink autocomplete (`[[` triggers note title completion), and inline
  preview decorations for headings, code blocks, and blockquotes.
- **CM6 dynamic loading**: Bundle loaded via `<script>` tag at boot time.
  Falls back to plain textarea if CM6 is unavailable.
- **Export as .md**: Export button in editor header downloads the note as a
  Markdown file with YAML frontmatter (title, tags, created date).
- **Import .md**: Import button in topbar allows uploading `.md`/`.markdown`/`.txt`
  files. Parses YAML frontmatter for title and tags, creates note via API.
- **CSS for CM6**: Editor fills the container with `max-height: none` override
  and proper scroller padding.

### Changed

- Notes package version bumped from 0.4.0 to 0.5.0.
- `EditorPane` component refactored to support CM6 with mutable refs for
  title/body (required for CM6 callback closures).
- Ctrl/Cmd+S save works in both CM6 and textarea fallback modes.

---

## v0.4.3 — Backlinks + Wikilinks

### Added

- **Links table** (`ext_notes_links`): source_id, target_id, link_text columns
  with indexes on source_id and target_id for bidirectional lookup.
- **Wikilink extraction**: `_extract_wikilinks()` parses `[[...]]` patterns
  using `split("[[")` + `find("]]")` (Starlark has no regex or while loops).
  Returns deduplicated list of link text strings.
- **Link sync on save**: `_sync_links()` called from `_create_note` and
  `_update_note` when body changes. Follows the tags delete-all + reinsert
  pattern. Resolves link text to note IDs via case-insensitive title matching.
  Unresolved links stored with empty `target_id`.
- **Cascade delete**: Hard-deleting a note removes both outgoing links
  (source_id matches) and incoming backlinks (target_id matches).
- **Link API endpoints**: `GET /links/:note_id` returns outgoing links.
  `GET /backlinks/:note_id` returns incoming links enriched with source note
  titles to avoid extra frontend round-trips.
- **Wikilink preview rendering**: `[[Note Title]]` syntax renders as clickable
  accent-colored links in the markdown preview. Unresolved wikilinks styled
  in danger/red with dashed underline.
- **Wikilink click navigation**: Clicking a resolved wikilink navigates to the
  target note. Clicking an unresolved wikilink creates the note and navigates
  to it, then re-saves the source note so the link resolves to blue.
- **Backlinks panel**: Collapsible panel below the editor showing all notes
  that link to the current note. Each backlink displays source title and link
  text. Click to navigate. Hidden when no backlinks exist.
- **Updated stats**: `/stats` response now includes `links` count.

### Data model

| Table | Columns |
|-------|---------|
| `ext_notes_links` | source_id, target_id, link_text |

### Notes package version

0.3.0 → 0.4.0

---

## v0.4.2 — Tags + Search

### Added

- **Tags table** (`ext_notes_tags`): note_id, tag columns with indexes on
  both for bidirectional lookup. Many-to-many relationship via join table.
- **Tag CRUD API**: 3 new Starlark handlers — list all unique tags, get tags
  for a note, replace tags for a note (delete-all + reinsert). Tags are
  normalized to lowercase, trimmed, and deduplicated on save.
- **Tags in list/get/search**: `_list_notes` batch-fetches all tags in a
  single query and attaches a `tags` array to each note item. `_get_note`
  includes tags. `_search_notes` now matches search terms against tags.
- **Tag cascade delete**: Hard-deleting a note also removes its tag rows.
- **Tag input in editor**: `TagInput` component with removable pills, text
  input (comma or Enter to add), and autocomplete dropdown populated from
  all existing tags.
- **Tag pills on note cards**: `NoteCard` renders up to 3 tag pills with
  "+N" overflow indicator for notes with many tags.
- **Tag filter in sidebar**: `TagFilter` component displays all unique tags
  as clickable pills. Clicking toggles client-side filtering of the note
  list by that tag.
- **Drag-and-drop note moves**: `NoteCard` is draggable (HTML5 drag/drop).
  `FolderNode`, "All Notes", and "Unfiled" items are drop targets. On drop,
  calls the existing move-note API.
- **Folder context menu**: Replaced `prompt()` hack with a proper right-click
  popup menu showing "Add subfolder", "Rename", and "Delete" actions. Positioned
  at click coordinates, dismissed on outside click.
- **Updated stats**: `/stats` response now includes `tags` count (unique).

### Data model

| Table | Columns |
|-------|---------|
| `ext_notes_tags` | note_id, tag |

### Notes package version

Bumped from 0.2.0 → 0.3.0. 15 API routes (12 existing + 3 new).

## v0.4.1 — Folders + Navigation Tree

### Added

- **Folders table** (`ext_notes_folders`): name, parent_id, creator_id,
  sort_order. Indexed on parent_id and creator_id. Nested hierarchy via
  parent_id foreign key (empty string = root).
- **Folder CRUD API**: 5 new Starlark handlers — list, create, update, delete
  folders plus a dedicated move-note endpoint (`POST /notes/move`).
- **Navigation tree**: `FolderTree` + `FolderNode` Preact components in the
  sidebar. Flat API response built into a nested tree client-side via `useMemo`.
  Expand/collapse toggles, depth-based indentation, inline rename via
  right-click context menu.
- **Folder filtering**: Clicking a folder filters notes by `folder_id` query
  param. "All Notes" clears the filter. "Unfiled" shows notes with empty
  folder_id (client-side filter).
- **Editor folder select**: Dropdown in the editor header moves notes between
  folders. Shows nested hierarchy with `└` prefix indentation.
- **Folder-aware note creation**: New notes inherit the active folder's ID.
- **Delete cascade**: Deleting a folder orphans its notes (moves to Unfiled)
  and reparents child folders to the deleted folder's parent.
- **Updated stats**: `/stats` response now includes `unfiled` and `folders`
  counts.

### Data model

| Table | Columns |
|-------|---------|
| `ext_notes_folders` | name, parent_id, creator_id, sort_order |

### Notes package version

Bumped from 0.1.0 → 0.2.0. 12 API routes (7 existing + 5 new).

## v0.4.0 — Notes Surface

### Added

- **Notes surface package** (`packages/notes/`): Obsidian-style markdown notes
  rebuilt as a standard installable `.pkg` archive. Type `full`, tier `starlark`.
  Zero kernel changes — proves the extension stack end-to-end.
- **Starlark backend** (`script.star`): 7 API routes — CRUD, search, stats.
  Note bodies stored in `ext_notes_notes` TEXT columns. List endpoint returns
  lightweight projections (no body); full content fetched on select.
- **Markdown editor**: Preact+htm frontend with sidebar note list, monospace
  textarea, and toggle-able live preview. Inline markdown renderer (~100 lines)
  covers headings, bold, italic, code blocks, links, lists, blockquotes, and HR.
- **Auto-save**: Debounced save (1s) with dirty/saved status indicator.
  `Ctrl/Cmd+S` for force save. Tab key inserts two spaces.
- **Pin & archive**: Pin important notes to the top of the list. Archive
  (soft-delete) via the delete button; hard delete with `?hard=1` query param.
- **Search**: Case-insensitive full-text search across title and body content.
  Search bar in sidebar filters the note list in real-time.
- **Theme support**: All styles use CSS variables — light and dark themes work
  out of the box. Responsive layout collapses to vertical on narrow viewports.

### Data model

| Table | Columns |
|-------|---------|
| `ext_notes_notes` | title, body, folder_id, creator_id, updated_at, pinned, archived |

Indexes on `folder_id`, `creator_id`, `pinned`, `updated_at`.

### Planned (v0.4.1–v0.4.4)

- Folders + navigation tree
- Tags + enhanced search
- Backlinks + `[[wikilinks]]`
- Rich editor (CodeMirror 6) + markdown import/export

## v0.3.8 — Distribution

### Added

- **Bundled packages**: Production Docker image now ships with 12 pre-built
  `.pkg` archives (4 workflows, 3 surfaces, 1 full, 1 library, 2 test runners,
  1 extension). Auto-installed on first boot via `InstallBundledPackages()`.
  Install-once, skip-if-present — admin uninstalls are respected on restart.
- **Package allowlist** (`BUNDLED_PACKAGES`): Comma-separated list of package
  IDs to install. Empty (default) installs all. Useful for Helm/K8s where
  different environments need different packages.
- **Skip bundled** (`SKIP_BUNDLED_PACKAGES=true`): Disables auto-install
  entirely. Intended for production until post-MVP packages are ready.
- **Builder image** (`Dockerfile.builder`): Pre-caches Go modules, Node
  dependencies, and vendor lib tarballs for faster custom builds.
- **Distribution docs** (`docs/DISTRIBUTION.md`): Quick start, bundled
  packages, builder image usage, custom build guide, production deployment
  reference.
- **Migration 012**: Adds `bundled` to `packages.source` CHECK constraint
  (both Postgres and SQLite).
- **K8s manifest**: `SKIP_BUNDLED_PACKAGES` and `BUNDLED_PACKAGES` env vars
  added to `k8s/armature.yaml`.
- **Tests**: 6 handler tests (fresh install, skip existing, missing dir, empty
  dir, dormant handling, allowlist filtering).

### Environment defaults

| Environment | `SKIP_BUNDLED_PACKAGES` | `BUNDLED_PACKAGES` |
|-------------|------------------------|--------------------|
| Dev (compose) | `false` | (empty = all) |
| Test (K8s) | `false` | (empty = all) |
| Prod (K8s) | `true` | — |

## v0.3.7 — Package Audit

### Added

- **Dormant status**: New `dormant` value in `packages.status` CHECK constraint
  (both Postgres and SQLite). Installer auto-detects packages with unmet
  `requires` entries and sets `status=dormant, enabled=false`. Enable endpoint
  returns 409 for dormant packages.
- **Generalized requires check**: Any unmet requirement triggers dormant status,
  not just specific values. Known capabilities checked against a whitelist;
  anything unknown is unmet.
- **Admin UI indicators**: Dormant badge, disabled Enable button with tooltip,
  Dormant stat card in packages admin view.
- **Tests**: 4 new handler tests covering dormant status, enable blocking,
  surface exclusion, and admin list inclusion.

### Fixed

- **Manifest fixes**: Fixed 6 chat-extension manifests (`"name"` → `"title"`)
  that were blocking install. Added explicit `"type": "surface"` to
  hello-dashboard, icd-test-runner, sdk-test-runner.
- **Dashboard + Editor tagged dormant**: Both depend on removed `sw.*`
  imperative SDK (`sw.tabs`, `sw.chat`, `sw.layout`, etc.) — render blank.
  Tagged with `requires: ["legacy-sdk"]` so installer auto-sets dormant.
- **Dependency check**: Relaxed library dependency validation to allow
  `pending_review` libraries (gitea-client declares permissions). Only
  `suspended`/`dormant` libraries blocked.

### Package Audit Results

- **7 working surfaces**: schedules, tasks, team-activity-log, git-board,
  hello-dashboard, icd-test-runner, sdk-test-runner
- **8 dormant**: 6 chat-dependent (csv-table, diff-viewer, js-sandbox,
  katex-renderer, mermaid-renderer, regex-tester) + 2 legacy-sdk (dashboard,
  editor)
- **1 library**: gitea-client (standalone, active)

## v0.3.6 — Example Workflows + Interactive Demo

### Added

- **Bug Report Triage** (`packages/bug-report-triage`): Public entry, progressive
  fieldsets, severity-based branch routing, SLA timer (3600s on critical fix).
- **Employee Onboarding** (`packages/employee-onboarding`): Starlark automated
  stages (`db.insert`, `notifications.send`), manager signoff gate with
  `required_role`, rejection reroute. `script.star` with `on_provision` and
  `on_welcome` hooks. Creates `provisions` and `onboarding_log` ext_data tables.
- **Content Approval** (`packages/content-approval`): Multi-party signoff (quorum
  of 2), rejection reroute creating a review cycle loop.
- **Webhook Notifier** (`packages/webhook-notifier`): Starlark `http.post` with
  `connections.get` fallback, delivery logging to `webhook_log` ext_data table.
- **Workflow Demo** surface (`packages/workflow-demo`): Interactive walkthrough
  with workflow cards, stage flow diagrams, Starlark viewer, API curl examples,
  and "Try It" buttons. Route: `/s/workflow-demo`.
- **Engine context fix**: `started_by` added to automated stage Starlark context
  dict, enabling hooks to reference the workflow initiator.
- **SLA package installer**: `sla_seconds` field added to `workflowPkgStage`
  struct and wired in both install and export paths.
- **Snapshot format fix**: `parseSnapshotStages` helper handles both wrapped
  (`{"stages":[...]}`) and legacy flat array snapshot formats in the engine,
  fixing `corrupt version snapshot` errors when starting published workflows.
- **Workflow adoption**: `POST /teams/:teamId/workflows/:id/adopt` claims a
  global (team_id=NULL) workflow for a team. `GET /teams/:teamId/workflows/available`
  lists adoptable workflows. `TeamID` added to `WorkflowPatch` model.
- **Tests**: 3 new engine tests (automated context, SLA install, manifest
  roundtrip). Total: 142 handler tests passing.
- Per-package `README.md` files for all 5 new packages.

### Fixed

- **Demo surface**: Added `sw.userMenu` to topbar. Fixed workflow install
  detection (was reading wrong response key). CSS uses theme variables for
  dark mode support.
- **Admin teams tab**: Extracted `.data` from paginated response — was stuck
  at loading because `setTeams` received the envelope object, not the array.
- **Team-admin workflows**: Added "Adopt Global" button to claim package-installed
  workflows into a team scope. Workflow list now unwraps paginated responses.

## v0.3.5 — Settings Audit + ICD + Tests + Clone

### Added

- **Clone endpoint**: `POST /api/v1/workflows/:id/clone` deep-copies a workflow
  and all its stages. Creates an inactive draft with "Copy of" prefix. Handles
  slug collisions with auto-suffix. Team-scoped mirror at
  `/api/v1/teams/:teamId/workflows/:id/clone`.
- **Integration tests**: 7 new engine-level tests in `workflow_engine_test.go`
  covering full lifecycle, branch routing, public entry, signoff validation gate,
  signoff rejection, cancel-clears-assignments, and error cases. Total: 28 tests.
- **Staleness timeout UI**: `staleness_timeout_hours` input added to workflow
  editor in team-admin settings (was backend-only since v0.3.3).
- **Branch rules UI**: Collapsible JSON textarea in stage form for configuring
  conditional routing rules (was backend-only since v0.3.2).

### Changed

- **ICD (OpenAPI spec)**: Fixed stale `Workflow` schema (added `description`,
  `branding`, `is_active`, `on_complete`, `retention`, `staleness_timeout_hours`).
  Fixed stale `Stage` schema (removed `history_mode`/`transition_rules`, updated
  `stage_mode` enum to `[form, review, delegated, automated]`, added `audience`,
  `stage_type`, `starlark_hook`, `branch_rules`, `stage_config`, `sla_seconds`,
  `auto_transition`, `assignment_team_id`, `surface_pkg_id`).
  Added `WorkflowInstance`, `WorkflowAssignment`, `WorkflowSignoff` schemas.
  Added ~20 new endpoint paths: instances, assignments, signoffs, public entry,
  clone, and team roles. Added tags: Workflow Instances, Workflow Assignments,
  Workflow Signoffs, Public Workflows.

## v0.3.4 — Team Roles + Multi-party Validation

### Added

- **Custom team roles**: Removed `CHECK (role IN ('admin','member'))` constraint
  from `team_members` (both Postgres and SQLite). Custom roles are stored in
  `teams.settings["roles"]` as a JSON array. Builtins `admin` and `member` are
  always present.
- **Team roles API**: `GET /api/v1/teams/:teamId/roles` returns configured roles.
  `PUT /api/v1/teams/:teamId/roles` replaces the roles array (builtins enforced).
- **Role-based stage assignment**: `stage_config.required_role` restricts which
  team members can claim an assignment. `CheckClaimRole` verifies the user's team
  membership role before allowing claim; rolls back on mismatch (403).
- **Multi-party sign-off**: New `workflow_signoffs` table with
  `UNIQUE(instance_id, stage, user_id)` preventing double-signing.
  `StageConfig.validation` supports `required_approvals`, `required_role`, and
  `reject_action` (cancel or reroute to named stage).
- **Validation gate**: `advanceInternal` blocks stage advancement until the
  required number of approvals is met. Rejections trigger cancel or reroute
  based on `reject_action`.
- **`Engine.SubmitSignoff`**: Records approval/rejection, enforces signoff role
  if configured, emits `workflow.signoff` event.
- **Signoff HTTP API**: `POST /api/v1/instances/:iid/signoffs` (submit),
  `GET /api/v1/instances/:iid/signoffs` (list current stage signoffs).
  Team-scoped mirrors at `/api/v1/teams/:teamId/instances/:iid/signoffs`.
- **New events**: `workflow.signoff`, `workflow.rejected`.
- **Frontend — Members page**: Dynamic role selects populated from team roles API.
  "Manage Roles" panel for adding/removing custom roles inline.
- **Frontend — Stage editor**: "Required Role (claim)" dropdown and "Multi-party
  Validation" section (required approvals, signoff role, on reject action) appear
  when a team is selected for the stage.
- **Frontend — Monitor tab**: Signoff panel with approve/reject buttons, comment
  field, and approval progress display.
- **3 new store tests**: CreateSignoff (with UNIQUE constraint check),
  ListSignoffs (ordering), CountSignoffs (decision filter). 20 total, all passing.
- **Design doc**: `docs/DESIGN-EXTENSION-LIFECYCLE.md` — permanent vs PoC
  packages, graduation criteria, explicit install model.
- **Design doc**: `docs/DESIGN-TRIGGER-COMPOSITION.md` — triggers/schedules can
  start workflows, workflows emit events, no circular invocation.

### Changed

- `addMemberRequest` and `updateMemberRequest` binding relaxed from
  `oneof=admin member` to `required,min=1,max=50` with application-level
  validation against the team's configured roles.
- `TruncateAll` test helper updated to include `workflow_signoffs`.

---

## v0.3.3 — Public Entry + Background Jobs

### Added

- **Public workflow entry**: Unauthenticated routes for anonymous workflow
  participation. `POST /api/v1/public/workflows/:id/start` creates an instance
  with `started_by = "public:<uuid>"` and returns an entry token.
  `GET .../resume/:token` and `POST .../advance/:token` allow continuation.
  Only stages with `audience = "public"` can be advanced anonymously.
- **SLA scanner**: Background goroutine (5-minute interval) checks active
  instances against per-stage `sla_seconds`. Fires `workflow.sla_breach` event
  on first breach, marks `sla_breached: true` in instance metadata (idempotent).
- **Staleness sweep**: New `staleness_timeout_hours` column on `workflows`.
  Scanner marks instances as `stale` when `updated_at` exceeds the threshold,
  cancels open assignments, fires `workflow.stale` event.
- **New store methods**: `ListActiveInstances()`, `MarkInstanceStale()`.
- **New events**: `workflow.sla_breach`, `workflow.stale`.
- **3 new store tests**: MarkStale, ListActive, StalenessTimeoutHours round-trip.

---

## v0.3.2 — Workflow Engine

### Added

- **Stage execution engine**: `server/workflow/engine.go` — `Start`, `Advance`
  (with `branch_rules` conditional routing), and `Cancel` operations. Merges
  `stage_data` across stages, creates assignments, emits lifecycle events.
- **Automated stage handler**: `server/workflow/automated.go` — fires Starlark
  hook, auto-advances on success, cycle guard (max 10 consecutive automated
  stages).
- **Instance HTTP API**: Start, GetInstance, Advance, Cancel, ListInstances.
  Team-scoped mirrors under `/api/v1/teams/:teamId/workflows/`.
- **Assignment HTTP API**: Claim, Unclaim, Complete, Cancel, ListByTeam,
  ListMine. Claimer identity verification on unclaim/complete.
- **Starlark module expansion**: `workflow.get_instance()`,
  `workflow.list_instances()` (read-only).
- **14 store tests** covering all 15 v0.3.1 methods (instance + assignment
  CRUD). All passing.

---

## v0.3.1 — Instance Assignment

### Added

- **`workflow_instances` table**: Tracks workflow execution state —
  `workflow_version`, `current_stage`, `stage_data`, `status`, `entry_token`.
- **`workflow_assignments` table**: Per-stage queue for instance assignments —
  `instance_id`, `stage`, `team_id`, `assigned_to`, `status`, `review_data`.
  Optimistic claim locking via status transition.
- **15 new store methods**: Full CRUD for instances (Create, Get, GetByToken,
  Update, List, AdvanceStage, Complete, Cancel) and assignments (Create, Claim,
  Unclaim, Complete, Cancel, ListByTeam, ListByInstance, ListByUser).
- **New events**: `workflow.started`, `workflow.cancelled`, `workflow.error`.

---

## v0.3.0 — Workflow Schema Redesign

### Changed

- **`workflow_stages` schema modernized**: Dropped chat-era columns (`persona_id`,
  `history_mode`). Renamed `transition_rules` → `stage_config`. Updated
  `stage_mode` CHECK from `(form_only, form_chat, review, custom)` to
  `(form, review, delegated, automated)`.
- **New stage fields**: Added `audience` (team | public | system), `stage_type`
  (simple | dynamic | automated), `starlark_hook` (package_id:entry_point),
  and `branch_rules` (JSONB array of routing conditions).
- **Routing engine**: `ResolveNextStage` now reads `branch_rules` (flat array)
  instead of `transition_rules.conditions` (nested object). Cleaner separation
  between routing rules and stage config.
- **Hook handler**: `FireOnAdvanceHook` reads from `stage_config` instead of
  `transition_rules`. Renamed `channelID` parameter to `instanceID`.
- **Stage CRUD validation**: New fields validated on create/update. `delegated`
  mode requires `surface_pkg_id`. `dynamic`/`automated` stage_type requires
  `starlark_hook`.
- **Package export/import**: Updated to use new field names. Workflow packages
  now include `audience`, `stage_type`, `starlark_hook`, `branch_rules`,
  `stage_config`.
- **Starlark workflow module**: Stage dicts now include `audience` and
  `stage_type` keys. Removed `history_mode` and `persona_id`.
- **Frontend stage editor**: Both admin and team-admin surfaces updated with
  new mode values, audience selector, stage type selector, and conditional
  Starlark hook input. Fixed pre-existing bug in admin workflows.js where
  `STAGE_MODES` still contained `chat_only`.

### Removed

- `persona_id` column from `workflow_stages` (personas are extension concerns)
- `history_mode` column from `workflow_stages` (chat-era context management)
- `StageModeCustom`, `StageModeFormOnly`, `StageModeFormChat` Go constants

---

## v0.2.9 — Builtin Extension Retirement

### Changed

- **6 builtin extensions → standard packages**: csv-table, diff-viewer,
  js-sandbox, katex-renderer, mermaid-renderer, and regex-tester moved from
  `extensions/builtin/` to `packages/` with standard `js/` layout. Each
  manifest now declares `"requires": ["chat"]` and `"type": "extension"`.
  Built via `build.sh` like all other packages — no auto-install.

### Removed

- **`SeedBuiltinPackages` seeder**: Deleted `seed_packages.go` (function,
  `builtinManifest` struct, `buildFullManifest` helper) and the startup call
  in `main.go`. Extensions are no longer auto-seeded into the DB.
- **`extensions/builtin/` directory**: Removed from repo and Dockerfile COPY.
- **Seed tests**: Removed 8 `TestSeed_*` functions and `makeSeedDir` helper
  from `extension_test.go` (~220 lines).
- **Stale comments**: Removed `SeedBuiltinPackages` references from
  `package_iface.go` and `trigger_sync.go`.

---

## v0.2.8 — Team Admin Settings Audit (Pass 1)

### Removed

- **Dead `HasPrivateProviderRequirement`**: Store method checked team settings
  for `require_private_providers` (BYOK vestige). Removed from interface,
  PostgreSQL, and SQLite implementations. No callers existed.
- **Dead `UserRole` field on `TeamMember`**: Joined `users.role` column
  (deprecated in v0.2.0 RBAC migration). Removed from model, `ListMembers`
  and `GetMember` queries in both stores. Frontend never consumed it.
- **Dead `allow_team_providers` policy**: Removed from `PolicyDefaults`, both
  test seed data blocks, and ICD test assertions. No handler or UI read it.
- **Dead personas in workflow stage UI**: Removed `sw.api.teams.personas()`
  call (endpoint doesn't exist), personas state, persona dropdown in
  StageForm, and persona badge in stage list.
- **Dead `history_mode` in stage UI**: Removed history mode selector and state
  from StageForm. Backend column retained for v0.3.x schema migration.
- **Dead ICD tests**: Removed assertions for `/teams/:teamId/personas`,
  `/teams/:teamId/providers`, and `/teams/:teamId/models` — endpoints no
  longer exist in the kernel.
- **Stale `chat_only` stage mode**: Frontend `STAGE_MODES` updated from
  `['chat_only', 'form_only', 'form_chat', 'review']` to
  `['form_only', 'form_chat', 'review', 'custom']` matching the backend
  CHECK constraint.
- **Stale comments**: Removed references to deleted `team_providers.go`,
  `personas.go`, and `apiconfigs.go` files.

---

## v0.2.7 — User Settings Audit

### Changed

- **localStorage namespace**: Renamed `cs-appearance` key to `sb-appearance`
  across appearance settings, base template, and workflow template. One-time
  migration preserves existing user preferences.
- **Policy-gating tests**: Replaced stale `allow_user_byok` / `allow_user_personas`
  assertions with `allow_registration` check (the only policy still in admin UI).

### Removed

- **Dead BYOK nav section** in user settings: empty `BYOK_ITEMS` array,
  `byokEnabled` state, "BYOK Enabled" footer badge, and the nav group
  that rendered an empty section.
- **Dead personas gate**: `personasEnabled` state and `.filter()` on
  NAV_ITEMS for a `gate` property no items have. `auth.permissions.changed`
  listener removed (existed solely for BYOK + personas state).
- **Dead Message Font Size**: Slider, `msgFont` state, and `--msg-font`
  CSS variable application from appearance section and base template.
- **Dead policy defaults**: `allow_user_byok` and `allow_user_personas`
  removed from `PolicyDefaults`, profile bootstrap, permissions handler,
  and `PublicSettings`. Test seed data cleaned.

---

## v0.2.6 — Admin Settings Audit

### Changed

- **Roadmap reorder**: Extension Lifecycle moved from v0.2.6 to v0.3.x
  (Workflows series). Settings audit milestones renumbered: v0.2.7→v0.2.6,
  v0.2.8→v0.2.7, v0.2.9→v0.2.8. Added v0.2.9 for builtin extension
  retirement (chat-centric extensions dormant until chat surface ships).

### Removed

- **Dead admin section categories** in `sectionCategory()`: AI (providers,
  models, personas, roles, knowledgeBases, memory), routing (health, routing,
  capabilities), and channels. Default category changed from `ai` to `system`.
- **Dead PublicSettings fields**: `system_prompt`/`has_admin_prompt`,
  `retention_ttl_days`, `paste_to_file_chars`, `allow_user_personas` policy
  — all chat-era with no frontend consumers.
- **Dead PolicyDefaults**: `allow_raw_model_access`, `default_model`.
- **Dead policy lookups**: `allow_raw_model_access` and `kb_direct_access`
  from profile bootstrap and permissions handlers.
- **Dead test seed data**: `model_roles` global setting, `allow_raw_model_access`
  policy from test helper.
- **Dead CORE_IDS entry**: Removed `chat` from packages admin page.

---

## [Unreleased] — v0.2.5

### Added

- **Welcome surface**: New core surface shown as fallback. Detects whether
  extensions exist (shows "Set Default Surface") vs truly empty install
  (shows "Go to Packages"). Topbar + UserMenu included.
- **User default surface**: Users can set a personal landing page in
  Settings > General, overriding the admin-configured global default.
- **UserMenu in admin topbar**: Replaced Back button with UserMenu,
  providing consistent surface navigation and eliminating infinite loops.
- **Package manifest icons**: Added emoji icons to hello-dashboard,
  icd-test-runner, sdk-test-runner, and team-activity-log manifests.
- **PoC documentation**: README.md for tasks and schedules packages
  documenting Proof of Concept status and graduation criteria.

### Changed

- **Default surface resolution**: Priority chain is now
  user preference → global config → first extension → `/welcome`.
  User preference read from JWT cookie on unauthenticated `/` route.
  Correctly resolves `type: "full"` extension packages (not just `type: "surface"`).
- **User settings General section**: Replaced dead chat fields (Default
  Model, System Prompt, Max Tokens, Temperature, Show Thinking) with
  a Default Surface dropdown.
- **Admin settings**: Removed dead chat sections (System Prompt, Default
  Model, Policies, Web Search, Auto-Compaction, Memory Extraction).
- **Roadmap restructured**: Workflows → v0.3.x (includes team roles),
  Notes → v0.4.0, MVP → v0.5.0. Added settings audit milestones
  (v0.2.7 admin, v0.2.8 user, v0.2.9 team-admin pass 1).
- Updated bus doc examples and test labels from `chat.*` to `workflow.*`.
- Renamed `channel-` prefixed test paths to `test-` in storage tests.
- Updated doc comments throughout to reflect current surface architecture.

### Removed

- **~500 lines of dead CSS**: Orphaned classes for removed legacy UI
  components (chat, channel, project, notes, editor-chat, sidebar,
  router-picker) from `layout.css` and `surfaces.css`.
- **Dead Go types**: `Grant` struct (persona-era), `CompositeModelKey` func,
  comment-only stubs for NoteGraph, ProjectChannel, etc.
- **Dead event code**: `chat.typing.*` / `channel.typing.*` condition in WS
  subscriber, empty Chat/Channel event route table sections.
- **Dead test helpers**: `seed_helpers.go` (SeedTestMessage, SeedTestMessages,
  SeedTestCursor — all callerless).
- **Stale template refs**: CSS link tags for non-existent `sw-chat-pane.css`,
  `sw-notes-pane.css`, `chat.css`. Orphaned `chat-pane.html` component.
- **Vestigial guards**: `"chat"` surface checks in `IsSurfaceEnabled()` and
  `DisablePackage()` (chat is no longer a surface).
- **Dead settings UI**: Chat defaults from user settings, System Prompt /
  Default Model / Policies / Web Search / Compaction / Memory from admin.
- Unused `fmt.Sprintf` references in team stores.

---

## [Unreleased] — v0.2.4

### Added

- **`sw.shell.Topbar`**: Standard navigation bar component for surfaces.
  Composes title + extension slot + NotificationBell + UserMenu into a
  consistent 44px bar. Surfaces use `<${sw.shell.Topbar} title="...">` with
  children rendered in the extension slot. Graceful fallback if unavailable.
- **Schedules surface** (`packages/schedules/`): New package wrapping the
  kernel `/api/v1/schedules` API. Table view with cron badge + human-readable
  preview, next fire time, enable/disable toggle, manual run, and execution
  logs panel. Create/edit dialog with live cron-to-english preview.
- **Manifest `icon` field**: Packages can declare an emoji icon in
  `manifest.json` (`"icon": "⏰"`). Served via the surfaces API `icon` field.
  Rendered in the UserMenu flyout next to each surface name.

### Changed

- **UserMenu**: Surface list now driven entirely by the `/api/v1/surfaces`
  API. Removed hardcoded surface links; menu is now fully dynamic.
  Core surfaces (Admin, Settings, Team Admin, Workflow) filtered from the
  API list and handled as dedicated menu items with RBAC gating.
- **Tasks surface**: Replaced custom `.tasks-header` with `sw.shell.Topbar`.
  View tabs and create button rendered in the extension slot.

### Fixed

- **`sw.isAdmin` RBAC regression**: `isAdmin()` in `can.js` was checking the
  deprecated `user.role === 'admin'` field instead of the v0.2.0 RBAC grant
  `surface.admin.access`. Admin menu item and admin-gated features now appear
  correctly for users in the Admins group.

---

## [Unreleased] — v0.2.2

### Added

- **Event bus subscriptions**: Extensions declare event triggers in manifest
  (`"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]`).
  Wired via `bus.Subscribe()` on startup. Handlers fire asynchronously.
- **Webhook triggers**: Inbound HTTP at `/api/v1/hooks/:package_id/:slug`.
  HMAC-SHA256 verification via `X-Armature-Signature` header. Synchronous
  Starlark handler can return custom HTTP status and body.
- **Scheduled tasks**: User-created cron-scheduled Starlark scripts with
  restricted sandbox (no raw HTTP, no DB table creation, connections-only
  outbound). Runs as creator identity with RBAC scoping. Admin-created tasks
  can opt into system context. Creator deactivation auto-pauses schedule.
- **Schedule templates**: Extensions ship pre-built schedule templates in
  manifest (`schedule_templates` array) with configurable params and default
  cron expressions.
- `triggers.register` extension permission — required for event/webhook triggers
- `triggers` table — extension-declared event and webhook trigger definitions
- `scheduled_tasks` table — user-created cron tasks with script, template,
  and identity fields
- `trigger_logs` table — unified execution audit log for both tiers
- `TriggerStore` + `ScheduledTaskStore` interfaces (postgres + sqlite)
- Trigger engine (`server/triggers/`) — orchestrates event subscriptions,
  webhook resolution, and cron scheduling via `robfig/cron/v3`
- `SyncManifestTriggers()` — declarative sync of event/webhook triggers from
  manifest. Hooked into seed, admin install, and package install flows.
- Admin trigger API: `GET/PUT/DELETE /admin/triggers`, `/admin/triggers/:id/logs`,
  `/admin/packages/:id/triggers`
- Admin schedule API: `GET /admin/schedules`, enable/disable/delete
- User schedule API: full CRUD at `/api/v1/schedules`, manual run, execution logs
- `trigger.fired` and `trigger.error` event bus labels (DirLocal) for observability
- OpenAPI spec: Trigger, ScheduledTask, TriggerLog schemas + all new endpoints

---

## [v0.2.1] — 2026-03-26

### Added

- **Default surface routing**: `/` redirects to configurable default surface.
  Fallback chain: configured default → first enabled extension surface → `/admin`.
  First installed extension surface auto-becomes default. Admin can change via
  Settings > Default Surface dropdown.
- `default_surface` global config key (JSON `{"id": "slug"}`)
- Admin settings UI: Default Surface dropdown (extension surfaces only)
- **ICD (API contract)**: Full OpenAPI 3.0.3 spec covering all 160 kernel
  endpoints. 22 tag groups (System, Auth, Profile, Workflows, Packages,
  Connections, Teams, Groups, Extensions, and Admin subsections). Reusable
  component schemas for User, Team, Group, Workflow, Package, Extension, etc.
  Served at `/api/docs` (Swagger UI) and `/api/docs/openapi.yaml`.

### Changed

- `disabledRedirect()` now redirects to `/admin` instead of `/` to prevent
  redirect loops when the default surface is disabled
- Disabled extension surfaces (`/s/:slug`) redirect to `/admin` instead of `/`

### Fixed

- **Gin route param conflict** causing backend startup hang: team-scoped
  package settings routes used `:pkgId` while sibling routes used `:id`.
  Gin's radix tree entered an infinite loop on the conflicting param names.
  Unified to `:id` across all `/teams/:teamId/packages/` routes.
- Docker entrypoint: increased health check timeout (10s → 60s), added stale
  process cleanup and crash detection to prevent zombie backends on restart

---

## [v0.2.0] — 2026-03-26

### Added

- **Full RBAC**: all authorization flows through group membership and permission
  grants. No magic roles, no implicit group membership, no special-casing.
- `surface.admin.access` permission — any group can grant admin panel access
- Admins system group seeded with all platform permissions
- Everyone system group — all users explicitly added on creation
- `EnsureEveryoneGroup()`, `AddToAdminsGroup()`, `RemoveFromAdminsGroup()` helpers
- `SeedAdminsGroupMember()`, `SeedEveryoneGroupMember()` test helpers
- System groups re-seeded after `TruncateAll` in test helper
- OIDC `isIdPAdmin()` — maps IdP role claims to Admins group membership
- **Settings cascade**: three-tier resolution (global → team → user) with
  `user_overridable` flag per manifest setting key. Admins can lock settings
  that team admins and users cannot override.
- `package_team_settings` table for team-scoped package setting overrides
- Team admin API: `GET/PUT/DELETE /api/v1/teams/:teamId/packages/:pkgId/settings`
- `RunContext.TeamID` for team-aware Starlark settings resolution
- `store.ResolveSettings()` / `store.FilterOverridableKeys()` pure functions
- `store.ParseSettingsSchema()` extracts `user_overridable` from manifests

### Changed

- `RequireAdmin()` / `RequireAdminPage()` check `surface.admin.access` grant
- `RequirePermission()` no longer bypasses for admin role
- `ResolvePermissions()` unions explicit group memberships only (no implicit Everyone)
- All user creation paths (builtin, OIDC, mTLS, admin, bootstrap, seed) add to
  Everyone group. Admin users added to Admins group.
- JWT claims no longer include `role` field
- Login response no longer includes `role` in user object
- Profile endpoint no longer returns `role`
- Profile bootstrap resolves permissions from groups (no admin shortcut)
- Middleware auth cache tracks `isActive` only (no role)
- Admin create user accepts `is_admin` bool (not role string)
- Admin update role endpoint accepts `is_admin` bool, manages Admins group directly
- Demotion/deletion safeguards check Admins group member count
- Notifications `RoleFallbackHandler` queries Admins group members
- OIDC syncs Admins group on login (no role column writes)
- Kernel permissions: 6 → 7 (added `surface.admin.access`)
- Admin users UI: role dropdown removed, admin managed through groups
- Starlark `settings.get()` uses cascade resolver instead of naive merge
- User settings save (`POST /extensions/:id/settings`) strips non-overridable keys

### Removed

- **`users.role` column** — dropped from schema, model, JWT, all handlers
- `UserRoleAdmin`, `UserRoleUser` constants
- `CountByRole()` store method
- `DefaultRole` config for OIDC and mTLS providers
- `SyncAdminsGroupMembership()` (replaced by `AddToAdminsGroup`/`RemoveFromAdminsGroup`)
- OIDC `resolveRole()` (replaced by `isIdPAdmin()`)
- **Token budgets** from groups: columns, `ResolveTokenBudget()`, all store/handler/UI
- **Allowed models** from groups: column, `ResolveModelAllowlist()`, UI
- Admin groups UI: Token Budgets section, Allowed Models section

### Migration notes

- 001_core.sql (both dialects): removed `role` column from users table
- 002_teams.sql (both dialects): added Admins group seed, removed
  `token_budget_daily`, `token_budget_monthly`, `allowed_models` columns
- No new migration files — edited in place per pre-MVP policy

---

## [v0.1.0] — 2026-03-26

Initial release. Pure extension platform — kernel provides auth, RBAC,
storage, and the Starlark sandbox. Everything else is a package.

### Removed

- **AI/Chat system**: providers, model catalog, routing policies, personas,
  channels, messages, completion streaming, tool loop, compaction, memory,
  knowledge bases, notes, workspaces, projects, folders, files, export/import
- **Task scheduler**: entire scheduler package, task store, task handlers.
  Tasks will be rebuilt as a Starlark extension with three trigger primitives
  (time, webhook, event)
- **Session system**: channel-based anonymous sessions. Workflow instances
  will get new storage in v0.2.0
- **Health accumulator**: provider health windows, tool health tracking.
  Replaced with kernel-only Prune (ws_tickets, rate_limit_counters, presence)
- **15 Go packages**: tools, compaction, extraction, roles, mentions,
  notelinks, export, memory, knowledge, providers, routing, capabilities,
  filters, retention, workspace
- **29 handler files**, 6 test files, ~44K lines total

### Fixed

- CI deploy: k8s resource quantity vars (`BE_MEMORY_REQUEST` → `MEMORY_REQUEST`)
  aligned with CI workflow outputs — `envsubst` was producing empty strings
- CI deploy: image var (`BE_IMAGE` → `IMAGE`) — caused `InvalidImageName` in pods
- CI rollout: deployment name (`switchboard` → `armature-be`) — rollout
  verification was looking for wrong deployment name
- Nginx BASE_PATH: regex cache-header locations intercepted static asset
  requests before alias could strip the sub-path prefix — moved inside alias block
- Post-login blank page: dead Go template references (`surface-chat`,
  `surface-notes`, `surface-projects`) caused html/template to silently
  produce Content-Length: 0 responses
- Login branding: "Chat Armature" → "Armature", updated tagline
  and feature pills to reflect platform pivot

### Changed

- Go module: `armature`
- VERSION: `0.1.0`
- Default DB name: `armature`
- Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables
- Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files)
- Stage modes: `chat_only` removed, `custom` added
- Task output modes: `channel|note|webhook` → `notification|webhook|log`
- Kernel permissions: 16 → 6 (`extension.use`, `extension.install`,
  `workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`)
- Everyone group seed: `["extension.use","workflow.submit"]`
- Global settings seed: site name "Armature"
- Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours,
  ProviderAutoDisableThreshold, ExtractionConcurrency, etc.)
- Health stores rewritten: kernel-only Prune for stale tickets, counters, presence
- Maintenance goroutine replaces scheduler for background cleanup

### Retained

- Identity & auth (builtin, mTLS, OIDC)
- Teams, groups, permissions
- Package system (surfaces, extensions, libraries, workflows)
- Starlark sandbox with capability-gated modules
- Extension connections & dependencies
- Workflow definitions, stages, versions
- Notifications & preferences
- Audit log
- Object storage (PVC, S3)
- WebSocket hub & presence
- Multi-replica HA (ws_tickets, rate_limit_counters)
- Frontend shell (preact+htm, SDK, vendor libs)
