# Changelog

All notable changes to Switchboard Core are documented here.

## 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/switchboard.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 were
  removed in Phase 0 fork.
- **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 remove references to gutted surfaces.

### Removed

- **~500 lines of dead CSS**: Orphaned classes for gutted chat, channel,
  project, notes, editor-chat, sidebar, and router-picker features 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 Chat, Notes, Projects links (gutted in Phase 0).
  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-Switchboard-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

Forked from chat-switchboard v0.38.5. Gutted to a pure extension platform.

### 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` → `switchboard-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 Switchboard" → "Switchboard Core", updated tagline
  and feature pills to reflect platform pivot

### Changed

- Module renamed: `chat-switchboard` → `switchboard-core`
- VERSION: `0.1.0`
- Default DB name: `switchboard_core`
- 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 "Switchboard Core"
- 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)
