# 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 `` 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 `