@terreno/agent-plugin-loop 0.0.0-stage → 57.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,2124 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ All `@terreno/*` packages (`api`, `test`, `ui`, `rtk`, `admin-backend`,
6
+ `admin-frontend`, `admin-spa`, `ai`, `announcements`, `api-health`, `comms`,
7
+ `feature-flags`, `jobs`, `mcp`, `syncdb`) and the unscoped `create-terreno-app` CLI are versioned in lockstep
8
+ and published at the same version.
9
+
10
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
11
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
12
+
13
+ Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/) as one
14
+ file per feature. `bun run changelog:assemble <version>` folds those files into a
15
+ dated section below when cutting a release.
16
+
17
+ Upgrade notes for consumer action live in [`mcp-server/src/docs/upgrades/`](mcp-server/src/docs/upgrades/). Fetch a range with the MCP tool `terreno_get_upgrade_guide`. Format: [`mcp-server/src/docs/upgrades/README.md`](mcp-server/src/docs/upgrades/README.md). The [`58.0.0` stub](mcp-server/src/docs/upgrades/58.0.0.md) is for the next major (not yet a changelog section).
18
+
19
+ ## [Unreleased]
20
+
21
+ Unreleased changes live in [`changelog/unreleased/`](changelog/unreleased/). Add one Markdown file per feature (see that directory's README) instead of editing this section.
22
+
23
+ ## [57.21.0] - 2026-11-01
24
+
25
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.21.0.md`](mcp-server/src/docs/upgrades/57.21.0.md).
26
+
27
+ ### Breaking
28
+
29
+ - The `terreno-scan` plugin is now `terreno-loop`, and its skills are renamed from `terreno-scan-*` to `terreno-loop-*`. The resident loop `terreno-scan-loop` is now `terreno-loop-run`: `/terreno-loop:run` in Claude Code, `/terreno-loop-run` in Cursor, `$terreno-loop-run` in Codex. Claude Code moves existing installs over through the marketplace `renames` entry. In Cursor and Codex, uninstall `terreno-scan` and install `terreno-loop`. Campaign state stays at `.terreno/scan/<goal>.json`, so campaigns already in progress resume.
30
+
31
+ ### Added
32
+
33
+ - `@terreno/admin-spa` serves an MCP OAuth consent screen at `/console/oauth/consent`, so a backend with no product frontend can set `mcpOAuth.consentUrl` to `${issuer}/console/oauth/consent`. Any signed-in user can approve or deny a request with their Better Auth session; signed-out users sign in and return through the login screen's new `returnTo` parameter, which accepts only in-app paths.
34
+ - Claude Code plugins now publish to npm with every release, as `@terreno/agent-plugin` and `@terreno/agent-plugin-loop`. A new `terreno-releases` marketplace installs them at the latest stable release. To pin one release, add `TerrenoLabs/terreno#<version>` instead. See [Install agent skills](docs/how-to/install-agent-skills.md#pick-a-channel).
35
+ - The generated Claude plugin manifest no longer pins `version`. Before this change, plugins installed from the `TerrenoLabs/terreno` marketplace stayed on the `2.12.0` copy, because their version never changed. They now update on every commit. Run `/plugin update terreno@terreno-plugins` once to leave `2.12.0`.
36
+ - The `terreno-planning` lifecycle plugin (2.13.0) plans e2e tests and manual regression cases in every IP:
37
+
38
+ - Grow requires `## E2E tests` and `## Regression cases` sections (either may be `None — <reason>`), read from the e2e guide at `docs/testing/e2e.md` or a `## Testing conventions` override in AGENTS.md or CLAUDE.md.
39
+ - Regression cases stay minimal: most features add none. A case is only for what a developer or QA must check by hand, such as push notifications, and Roast fails one that repeats an existing case.
40
+ - Pick writes each planned spec in the task that delivers it, and Roast runs it; a missing or failing row is `FAIL`.
41
+ - Brew gates on every planned spec and merges regression cases into `docs/regression/<area>.md` with the next free ID.
42
+ - Taste re-checks both sections after behaviour-changing fixes and renumbers regression ID collisions with `master`.
43
+ - `bun run check:lifecycle-skills` enforces these phrases in every stage.
44
+ - New `backfill-regression` skill proposes manual regression cases one area at a time and writes only the sets the human approves.
45
+ - A consumer opts out of manual regression suites with `Regression suites | none` in its `## Testing conventions` table.
46
+ - The `terreno-planning` lifecycle plugin (2.14.0) grows an end-user guide with every feature:
47
+
48
+ - Every IP has a `## User guide` section listing the guide pages it adds, updates, or retires, or `None — <reason>`. Pages live in `docs/user-guide/<area>/<feature>.md`, one feature per page, with `title`, `audience`, `platforms`, and `questions` frontmatter that chat agents use to answer "how do I…" questions.
49
+ - Pick writes each page in the task that ships the feature, Roast checks it against the shipped screens, Brew gates on it and the index, and Taste keeps it current after fixes.
50
+ - A repo opts out with `| User guide | none — <reason> |` in a `## User guide conventions` table in AGENTS.md or CLAUDE.md.
51
+ - New `backfill-user-guide` skill drafts pages for features that shipped without one, one area at a time, and writes only the pages the human approves.
52
+ - `bun run check:user-guide` validates the pages and index (CI `repo-policies`); `--uncovered <app dir>` lists screens no page names.
53
+ - Terreno's own guide covers the example app's main features.
54
+ - `@terreno/api`: opt-in `mcpRequestLog` on `TerrenoApp` records one metadata-only `McpRequestLog` row per MCP tool call (tool, user, credential, service token or OAuth client, duration, outcome, error class; never arguments, results, or error text), expiring after `retentionDays` (default 30).
55
+ - `@terreno/api`: opt-in `mcpToolSettings` on `TerrenoApp` registers every MCP tool in `McpToolSetting` at startup; tools an admin switches off are hidden from `tools/list` and rejected by `tools/call`.
56
+ - `@terreno/api`: deleting an `McpOAuthToken` row revokes its whole connection (token family) and keeps the rows. New `McpOAuthToken.revokeAllForUser` and `McpServiceToken.revokeAllForUser`.
57
+ - `@terreno/admin-backend`: `mcpAdminModels()` adds MCP admin screens (service tokens, OAuth clients, OAuth connections, request log, tool switches), `mcpAdminStatements` for RBAC, and `mcpAdminScripts()` adds `mcp-sign-out-everywhere`. See [Manage MCP from the admin panel](docs/how-to/manage-mcp-in-admin.md).
58
+ - Docs: [Expose an MCP server safely](docs/how-to/expose-mcp-server-safely.md) (OAuth, service tokens, Cloudflare Tunnel, Access) and the `bootstrap-mcp-server` skill for a single-binary MCP server with the admin console embedded.
59
+ - Every workspace package has a real-process smoke test and an e2e page:
60
+
61
+ - Each package's `e2e/smoke.ts` (or `smoke.spec.ts` for browser packages) runs its built output through its public entry: a server on a real port, the compiled binary, or a page in Chromium. `bun run --cwd <package> smoke` runs one; `bun run smoke` runs them all and prints each package's time. The CircleCI `smoke` job runs them on every package change.
62
+ - `@terreno/test` adds the smoke helpers `smokeMongoUri()`, `spawnUntilReady()`, `spawnExampleBackend()`, `runBin()`, and `freePort()`.
63
+ - The [E2E testing guide](docs/testing/e2e.md) explains the test tiers and links one page per package under `docs/testing/e2e/`.
64
+ - Manual regression cases live in `docs/regression/<area>.md`, one file per area, and `bun run check:regression-suites` validates them.
65
+ - example-frontend's smoke serves a web export that `bun run --cwd example-frontend export:smoke` builds once with a placeholder API URL; the smoke swaps in a `freePort()` API URL as it serves the bundle, so the 30 s smoke no longer rebuilds the export. `spawnUntilReady()` takes `stopGraceMs`, and `spawnExampleBackend()` stops example-backend after 0.5 s instead of 5 s.
66
+ - Every workspace package has a `typecheck` script that runs `tsc` over source and test files through a new `tsconfig.typecheck.json`. Root `bun run lint` ends with `bun run typecheck`, and CircleCI runs it in a new `typecheck` job. Test files that had type errors were fixed.
67
+
68
+ ### Changed
69
+
70
+ - `@terreno/api`: deleting an `McpOAuthToken` row with document or query `deleteOne` now revokes that row's whole token family (the connection) and keeps the rows instead of removing one pair. `deleteMany` is unchanged.
71
+ - `@terreno/ui`: new `theme.field` tokens (`background`, `backgroundDisabled`, `border`, `borderFocus`, `borderError`, `borderDisabled`) style form inputs. `TextField`, `SelectField`, `DateTimeField`, `HeightField`, and `OtpField` read them instead of `surface.base` and `border.dark`. Defaults equal the old colors, but overriding `surface` or `border` no longer recolors those fields; use `setTheme({field: ...})`.
72
+
73
+ ## [57.20.0] - 2026-10-30
74
+
75
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.20.0.md`](mcp-server/src/docs/upgrades/57.20.0.md).
76
+
77
+ ### Breaking
78
+
79
+ - `@terreno/api`: the app `User` claim guard moves before the Better Auth user is created, and
80
+ legacy users can prove ownership with their password. 57.17.0 refused to link an unverified
81
+ Better Auth user to an existing `User` by email, but only after the Better Auth user existed, so
82
+ a later email verification could still hand that user the `User`. Now a Better Auth user claims
83
+ an unlinked `User` only when the email is verified (by the provider), or when the sign-up (or
84
+ email sign-in) password verifies against that `User`'s passport-local-mongoose hash. A refused
85
+ sign-up answers 409 with `code: "VERIFY_EMAIL_TO_USE_ACCOUNT"` ("Verify your email to use this
86
+ account"; OAuth callbacks redirect with that `error`) and creates no Better Auth user, account,
87
+ or verification email. A `User` bound to another Better Auth account is refused before
88
+ creation too, with `code: "ACCOUNT_ALREADY_EXISTS"`. `syncBetterAuthUser` throws `APIError`
89
+ 409 with the matching title, and never inserts a duplicate `User`. Emails match ignoring case.
90
+ A refusal reveals that an app `User` has that email (an accepted account-existence signal).
91
+ 57.17.0's rules still hold: a verified-email link removes a legacy password the `User` never
92
+ proved and bumps `tokenEpoch`, and `trustEmail` seeds skip the checks.
93
+ - `BetterAuthApp` registered without a `userModel` now adopts `TerrenoApp`'s before it creates
94
+ Better Auth (new `adoptUserModel`, called by `TerrenoApp.build()`), so the guard runs whenever
95
+ TerrenoApp's session middleware links app `User`s. The legacy hash check does not touch
96
+ passport's `limitAttempts` counters.
97
+ - With a user model, the `terreno-app-user-sync` plugin syncs the app `User` as each Better Auth
98
+ session is created, not only on the first authenticated request, and the OAuth provider id
99
+ fills `oauthProvider`. A password-proven link that fails to save fails the sign-in with 500
100
+ `APP_USER_LINK_FAILED`. `syncBetterAuthUser`'s options gain `password`;
101
+ `seedBetterAuthUser` passes the seed password beside `trustEmail`.
102
+ - `accountLinking: {mode: "never"}` (or `options.account.accountLinking.enabled: false`) now
103
+ refuses `POST {basePath}/link-social` with 403 `LINKING_DISABLED` instead of failing at the
104
+ provider callback.
105
+ - Migration for JWT apps: users who sign up with their existing password, or sign in with a
106
+ provider that verified their email, link at once. Users with no password hash (GitHub JWT
107
+ sign-in) reset their password through JWT `POST /auth/forgotPassword` first, or sign in with a
108
+ verified provider, so keep the JWT routes mounted during the move. See
109
+ `docs/explanation/account-linking.md`.
110
+
111
+ ### Added
112
+
113
+ - `AccountSecurityApp` adds access recovery routes under `/users/:id/security`: `revoke-sessions` deletes every Better Auth session (bearer clients get 401 on their next request; cookie sessions can survive up to Better Auth's 5-minute cookie cache) and bumps `tokenEpoch` when the User model has it; `send-password-reset` mails a reset link (501 without `sendMail` or `publicAppUrl`, 409 for a user with no Better Auth account); `disable` sets `User.disabled` and revokes sessions, and `enable` clears it (both need `isDisabledPlugin`). Each change is audited.
114
+ - `POST /users/:id/security/issue-sign-in-link` creates a Better Auth account for a legacy User with no Better Auth link and no password, links it by `betterAuthId`, and returns a single-use set-password link `{url, expiresAt}` for the administrator to hand over. The email stays unverified. A link that was never used can be issued again (the earlier one stops working). It needs the new RBAC statement `user:issueSignInLink`, which only `superadmin` holds by default (`admin: true` without RBAC).
115
+ - Security route actions can be marked `platformOnly` (`disable`, `enable`, and `issue-sign-in-link` are) and can name a `requiredPermission`. `mountSecurityRoutes(router, authorize, {includePlatformOnly})` leaves platform-only actions out unless asked, so org-admin prefixes never expose them.
116
+ - The security summary reports `emailVerified` and `legacyPassword`, and `AccountSecurityApp` takes a `publicAppUrl` option (defaulting to the registered `BetterAuthApp`'s).
117
+ - The example backend's User applies `isDisabledPlugin`, so its admin and security screens can disable accounts.
118
+ - `AccountSecurityApp` (`@terreno/api`) adds platform account-recovery routes: `GET /users/:id/security` summarizes a user's linked providers, passkeys, 2FA state and remaining backup codes, active sessions, last sign-in, and available actions without exposing any secret, and `POST /users/:id/security/reset-two-factor` turns a locked-out user's 2FA off and forgets their trusted devices. Access needs the new RBAC statement `user:manageSecurity` (held by `superadmin` and `operator`; an existing `operator` role gains it once on the next seed, and a later revocation sticks through the new `RbacRole.appliedGrants`) and the user's own session (JWT or Better Auth session; MCP tokens and any other credential get 403). Acting on yourself, or on someone holding a permission you lack (such as a superadmin), is refused with 403. The plugin refuses to start without `AuditApp` unless you pass `audit: false`. See `docs/reference/account-security.md`.
119
+ - `AuditEvent.source` accepts `security`. `recordSecurityAction` writes one security event per recovery action that changes something, and `onSecurityAction` lets in-process listeners react to it.
120
+ - `mountSecurityRoutes(router, authorize)` mounts the same handlers under another prefix with its own authorizer, and `TerrenoApp.getAccessControl()` returns the app's RBAC access control.
121
+ - `DELETE /users/:id/security/passkeys/:passkeyId` removes one of a user's passkeys (another user's passkey id is a 404), and `DELETE /users/:id/security/accounts/:providerId` unlinks a sign-in provider. The password can't be unlinked. Either action that would leave no sign-in method is refused with 409 unless the user's email is verified, in which case a password-reset link is mailed first (and also afterwards, if a concurrent removal took the last other method). Unlinking doesn't end sessions started with that provider. Both actions are audited and appear in the summary's `actions` when they apply. A custom action that reuses an action id now throws at construction.
122
+ - The example backend registers `AccountSecurityApp`.
123
+ - `MemberSecurityPanel` in `@terreno/admin-frontend` shows a user's sign-in security (providers, password, passkeys, two-factor, sessions, last sign-in, disabled and email-verified state) and runs the recovery actions the server lists. Actions render from the summary's `actions`, so new server actions get a button without a frontend change; destructive ones confirm first, results and server error titles show inline, and a returned sign-in link is shown once with a copy button. It calls `/orgs/:orgId/members/:userId/security` or, without `orgId`, `/users/:id/security` through the host-bound `adminRequest`.
124
+ - `OrgMembersScreen` rows get a **Security** button that opens the panel for active members, and a **Reactivate** button for suspended memberships.
125
+ - `useMemberSecurityApi`, `memberSecurityPath`, and `resolveSecurityActionRequest` are exported for custom security screens.
126
+ - Each entry of the account security summary's `actions` now carries its route: `method` (`POST` or `DELETE`) and `path` relative to the user's security base, with `:param` placeholders such as `/passkeys/:passkeyId`. Clients can run any listed action, including custom ones, from the summary alone.
127
+ - `UserSecurityScreen` in `@terreno/admin-frontend` is the platform staff page for one user's sign-in security: a header with the user's name and email from their admin record, then `MemberSecurityPanel` against `/users/:id/security`, so operators and superadmins can sign a user out everywhere, send a password reset, disable or enable the account, and run every other recovery action the server lists.
128
+ - `UserSecurityLink` and `userSecurityRoute` add a **Security** button to an admin user record (for example in `AdminModelForm` `footerContent`) that opens `{routeBase}/users/:id/security`.
129
+ - The example app mounts the screen at `/admin/users/[id]/security` and links it from the admin `User` record, both only for staff holding `user:manageSecurity`; the link is hidden on the viewer's own record.
130
+ - `@terreno/api`: `BetterAuthConfig` turns auth features on without hand-built plugins:
131
+ `accountLinking: {mode: "verified-email" | "explicit" | "never", trustedProviders?}`,
132
+ `twoFactor: {issuer, backupCodes?, trustDeviceDays?}`, `passkey: {rpID, rpName, origin}`,
133
+ `signUp: {mode: "open" | "closed"}`, `lastLoginMethod`, `multiSession: {maximumSessions?}`,
134
+ `oauthProxy: {productionURL}`, and `nativeApps` (the `@better-auth/expo` server plugin).
135
+ Every feature is off until set; with none set the plugin list stays
136
+ `bearer`, `terreno-methods`, then `config.plugins`. Feature plugins go before
137
+ `config.plugins`, and `options` still overrides what they set.
138
+ - `signUp: {mode: "closed"}` stops new accounts through email and password and OAuth: email
139
+ sign-up answers 400, and a social or generic OAuth sign-in for an unknown email fails with
140
+ `signup_disabled`, even with `requestSignUp: true`. Other sign-in plugins added through
141
+ `plugins` (magic link, email OTP, phone number, anonymous) need their own `disableSignUp`.
142
+ - `passkey` config is checked at startup: each `origin` must be a bare http(s) origin on
143
+ `rpID` or a subdomain of it.
144
+ - `appleOAuth` also accepts `{clientId, teamId, keyId, privateKey, appBundleIdentifier?}` and
145
+ signs Apple's ES256 client secret at startup (valid 180 days).
146
+ - `GET {basePath}/terreno/methods` now reports real `twoFactor`, `passkey`, and
147
+ `multiSession` flags.
148
+ - With `rateLimit` on, `{basePath}/two-factor/*`, `/passkey/*`, `/link-social`,
149
+ `/unlink-account`, `/multi-session/*`, `/oauth-proxy-callback`, and
150
+ `/expo-authorization-proxy` use the `auth` bucket.
151
+ - New dependencies: `@better-auth/passkey` (catalog, api and rtk), `@better-auth/expo` (api),
152
+ `react-native-qrcode-svg` (ui), and `otpauth` (example-frontend dev).
153
+ - `@terreno/api`: every Better Auth server now answers `GET {basePath}/terreno/methods`
154
+ (public, no session) with `{emailPassword, signUp, providers, passkey, twoFactor,
155
+ accountLinking, multiSession}`. `providers` lists the configured Google, GitHub, and Apple
156
+ providers and any `genericOAuth` provider ids. `createBetterAuth` adds the `terreno-methods`
157
+ plugin after `bearer()`, before `config.plugins`.
158
+ - `@terreno/rtk`: `useAuthMethods(authClient)` returns `{methods, isLoading, error}` from that
159
+ endpoint through the client's `$fetch`, so bearer and cookie modes both work.
160
+ - example-frontend: the login screen shows only the social providers the server reports, and
161
+ shows the mock OIDC button when the server lists `mock-oidc`.
162
+ `EXPO_PUBLIC_MOCK_OIDC_SIGN_IN` is no longer read.
163
+ - `@terreno/api` serves `POST {basePath}/terreno/set-password` (`{newPassword}`) so a signed-in user without a password, such as one who signed up with a provider, can add one. It needs a fresh session (403 `SESSION_NOT_FRESH` otherwise) read from the database, refuses with 409 `PASSWORD_ALREADY_SET` when a password exists, follows Better Auth's length rules, and sits in the `auth` rate-limit bucket. With a `userModel`, an app User linked by `betterAuthId` that already has a JWT password gets the new one too, and its `tokenEpoch` is bumped; a social-only account does not gain a JWT password. Two concurrent sets leave one password.
164
+ - A successful set records the new `password-set` security event, which emails the user "A password was added to your account" (template `securityPasswordSet`) when security notifications are on.
165
+ - Two-factor, trusted-device, and passkey sign-in now work for bearer-mode web clients that send no cookies (`createBetterAuthClient({webAuth: "bearer"})`). With `twoFactor` or `passkey` configured, `@terreno/api` copies Better Auth's signed challenge cookies into the `X-Terreno-Auth-Challenge` header for clients that send it and turns the header back into cookies on `/two-factor/*`, `/passkey/*`, and `/sign-in/*`. rtk's bearer mode stores the header beside the bearer token (`${storagePrefix}_auth_challenge`), sends it on those calls, never stores the dead token of a sign-in waiting on a second factor, drops the 2FA challenge after a successful verify and pending challenges on sign-out, and keeps the trusted-device value until it expires. Cookie-mode and native clients are unchanged.
166
+ - create-terreno-app: generated apps get the auth suite. `backend/src/utils/betterAuthConfig.ts` turns on `accountLinking` (`AUTH_ACCOUNT_LINKING`, default `verified-email`), `twoFactor` (issuer = the app's display name, 30 trusted days), `passkey` (origin and rpID from `FRONTEND_URL`, or `PASSKEY_RP_ID`; off in production while `FRONTEND_URL` is a loopback URL), `lastLoginMethod`, `multiSession`, `nativeApps`, and `signUp` (`AUTH_SIGN_UP`, default `open`); `getWebOrigins` now includes the `FRONTEND_URL` origin. `server.ts` registers `AuditApp` and `AccountSecurityApp`; the generated User schema now applies `rbacUserPlugin` (`roles`, default `member`), so the seeded admin's `operator` role is saved and reaches the `/users/:id/security` recovery routes; and the seed keeps sign-up open on its own instance. On the frontend, `lib/betterAuth.ts` passes the matching client `features` and exports a `rememberedAccounts` store that keeps only the sign-in method (`rememberIdentity: false`). `app/login.tsx` renders `TerrenoLoginScreen`, and a new `app/settings/security.tsx` renders `TerrenoAccountSecurityScreen`, opened from the Profile tab's **Security** button. The Profile tab's "New Password" field is gone: it sent `password` to `PATCH /auth/me`, which refuses password fields since 57.17.0. Change passwords on the Security screen. The "Current password" field that an email change needs stays. Generated apps set no `sendMail`, so forgot-password, the `send-password-reset` and `issue-sign-in-link` recovery actions (501), and security notification emails do nothing until the app passes `@terreno/comms` `sendMail` and `renderAuthMail` to the Better Auth config. To move an existing generated app over, copy those files (and `models/user.ts`, `types/models/userTypes.ts`) from a freshly generated app and add `FRONTEND_URL` to the backend env.
167
+ - example-backend turns on every Better Auth feature: account linking (`AUTH_ACCOUNT_LINKING`, default `verified-email`), sign-up mode (`AUTH_SIGN_UP`, default `open`), two-factor (issuer "Terreno Example"), passkeys (origin from `FRONTEND_URL`, rpID from `PASSKEY_RP_ID` or the `FRONTEND_URL` host; off in production while `FRONTEND_URL` is a loopback URL), last-used method, multi-session, and native sign-in. `GET /api/auth/terreno/methods` reports them. Seeded users (`seedBetterAuthUserInProcess` and the e2e setup) get a verified Better Auth email, and seeding still works with `AUTH_SIGN_UP=closed`. See [Auth features in example-backend](docs/how-to/configure-better-auth.md#auth-features-in-example-backend).
168
+ - The e2e mock OIDC provider can sign in any email, verified or not, per browser context with `setMockOidcIdentity(context, {email, emailVerified})` (or a `login_hint`). `playwright.config.ts` lists the cross-site bearer specs in `CROSS_SITE_SPEC_FILES`, and the new `e2e/authMethods.spec.ts` checks the discovery endpoint there.
169
+ - `ListDetailLayout`, `ListDetailIndex`, and `useListDetail` in `@terreno/ui`: a route-driven list/detail layout for an Expo Router group's `_layout.tsx`, like a mail inbox. Phones push the detail route; when the layout is at least 768px wide (tablets, wide browsers, inside a sidebar), the list stays in a left pane and the detail opens beside it. The open item lives in the URL, so deep links and back work, and it stays open across rotation and resize. See `docs/how-to/build-list-detail-screens.md`. `SplitPage` is unchanged; its docs now point new screens to `ListDetailLayout`.
170
+ - `Box` `accessibilityState.selected`. A clickable row sends it as `aria-current="true"` on web; a selectable `accessibilityRole` (`tab`, `option`, `row`, ...) sends `aria-selected`.
171
+ - The demo inbox exports each fixture message (`generateStaticParams`), and the Netlify demo deploy rewrites any other path to `index.html`, so `/demo/list-detail/2` and `/demo/sidebar-navigation/mail/2` load on refresh.
172
+ - The example todo browser shows a spinner while a deep-linked todo is still syncing, and “This todo does not exist” only after sync is idle and the row is absent.
173
+ - A clickable `Box` with `selected` sends only `aria-current` on web. `selected` is left off the host `accessibilityState`, so react-native-web does not also set `aria-selected` on the button.
174
+ - The hosted Terreno MCP server rate limits JSON-RPC calls per client IP (default 120 requests per 60 seconds). Excess calls get HTTP 429 with a JSON-RPC error naming the limit and retry window. Configure with `TERRENO_MCP_RATE_LIMIT_MAX` (`0` disables), `TERRENO_MCP_RATE_LIMIT_WINDOW_MS`, and `TERRENO_MCP_TRUST_PROXY_HOPS`.
175
+ - `@terreno/api` MCP OAuth accepts `grant_type=client_credentials` for a confidential client created with `McpOAuthClient.issueConfidential`. The helper binds the client to a user, stores the secret as a SHA-256 hash, and returns the secret once. The token endpoint accepts `client_secret_basic` or `client_secret_post`, whichever method the client was created with, and returns an `mcpat_` access token with no refresh token. The token acts as that user (`credential: "oauth"`). Revoking the client ends every token it holds, and a revoked client is rejected at `/oauth/authorize` and `/oauth/token`. An authorization-code exchange, a refresh, or a client_credentials grant that loses a race with that revoke is discarded. `deleteOne`, `findOneAndDelete`, and `deleteMany` do the same. A query `deleteOne` or `findOneAndDelete` whose filter matches more than one client pins the delete to one document and revokes that client's tokens. Dynamic client registration stays public-only. Discovery advertises `client_credentials`, `client_secret_basic`, and `client_secret_post`.
176
+ - `@terreno/api`: with `oauthHandoff`, a bearer-mode web client on another site can link a
177
+ social or generic OAuth provider to the signed-in user. `POST {basePath}/oauth-handoff/link-intent`
178
+ (bearer- or cookie-authenticated, `{provider, callbackURL, errorCallbackURL?}`) returns
179
+ `{token, startURL}`: a single-use, three-minute link token, stored hashed and bound to the
180
+ caller's session. Both URLs must be absolute pages on one of `oauthHandoff.webOrigins` that
181
+ `trustedOrigins` allows (400 otherwise); `callbackPaths` do not apply, since no token travels
182
+ on a link's return. The intent answers 403 `LINKING_DISABLED` when account linking is off. The web app posts the token
183
+ from a hidden form to `POST {basePath}/oauth-handoff/link-start`, which refuses a missing, `null`, or
184
+ foreign `Origin` with 403 (so web origins must not serve `Referrer-Policy: no-referrer` or
185
+ `same-origin`), then uses the token up (400 with no
186
+ redirect when it is unknown, used, or expired), sets only the OAuth state cookie, first-party on the
187
+ API's origin, and redirects to the provider. The provider callback links the account and
188
+ redirects to `callbackURL` with no one-time token, since no session is created. A session
189
+ revoked after the intent comes back with `error=session_expired`. Both routes are in the auth
190
+ rate-limit bucket, and Better Auth's limiter allows three of each per ten seconds per IP.
191
+ - `@terreno/rtk`: new `linkSocialWithHandoff({client, provider, callbackURL, errorCallbackURL?})`.
192
+ On web in bearer mode it posts the intent and submits the hidden form; in cookie mode and on
193
+ native it calls `client.linkSocial`. It resolves `{error}` (`null` once the browser is on its
194
+ way to the provider). New type `BetterAuthLinkClient`.
195
+ - Org-admins can run account recovery on members of their organization. When the app registers `AccountSecurityApp`, `OrgsApp` mounts the security routes at `/orgs/:orgId/members/:userId/security/*` (`TerrenoApp` passes the registered plugin; pass `accountSecurity` to a hand-registered `OrgsApp`). An org-admin may act on any active member of the org, even one in other orgs. Suspended members and non-members answer `404`; platform staff (`superadmin`, `operator`) and `admin: true` targets answer `403`; disabled orgs answer `403`. Platform staff holding `user:manageSecurity` pass the org checks. Every event records the org.
196
+ - New org-only action `suspend-membership` replaces `disable` under the org prefix: it suspends the membership in that org only, so the user still signs in and keeps their other orgs. `disable`, `enable`, and `issue-sign-in-link` stay platform-only. Security route actions can be marked `orgOnly`, mounted only with `mountSecurityRoutes(..., {includeOrgOnly: true})`. `mountSecurityRoutes` also takes `pathPrefix` (such as `/:orgId/members`) so a prefix's params appear in OpenAPI.
197
+ - `OrgsApp` takes `orgRecovery: {minMembershipDays}` (default `0`) to refuse org-admin recovery on members who joined too recently.
198
+ - The recovery routes' "target holds permissions you lack" check resolves each user in their own context: outside any organization on the platform routes, and under each user's own membership in `:orgId` on the org routes (authorizers return it as `orgScope`). A target is never judged by the acting org-admin's membership grants.
199
+ - `OrgsApp` throws `OrgsApp accountSecurity is not registered` when handed an `AccountSecurityApp` that is not registered (`AccountSecurityApp.isRegistered()`), so the org routes never run without the audit check, access control, or user model.
200
+ - `suspend-membership` keeps the last-org-admin rule under concurrency (suspend, recount, undo), so two org-admins suspending each other never leave the org without one.
201
+ - `oauthProxy` on the production server itself (`productionURL` on the `baseURL` origin) now sets oauth-proxy's `currentURL`, so that server's own sign-ins, including server-side starts such as `GET {basePath}/oauth-handoff/start` and requests on another host name, skip the proxy and keep Better Auth's state-cookie check. Its callback still forwards previews' returns.
202
+ - The OAuth one-time-token handoff also runs when oauth-proxy finishes a preview's sign-in (`GET {basePath}/callback/:id/oauth-proxy`, and the deprecated `GET {basePath}/oauth-proxy-callback`), so a bearer-mode web app on a preview gets `#ott=<token>` after the stable deploy forwards the provider's return.
203
+ - example-backend turns on `oauthProxy` from `OAUTH_PROXY_PRODUCTION_URL`, reads the Sign in with Apple key form (`APPLE_CLIENT_ID`, `APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_PRIVATE_KEY`; `APPLE_CLIENT_SECRET` still works), and trusts `https://appleid.apple.com` when Apple is configured.
204
+ - The example deploy sends every preview's social sign-in through the stable backend's registered callbacks. `STABLE_BACKEND_URL` and `STABLE_FRONTEND_URL` (defaulting to today's Cloud Run and Netlify hosts) set the stable `BETTER_AUTH_URL`, `FRONTEND_URL`, `OAUTH_PROXY_PRODUCTION_URL`, and, when both are subdomains of one domain, `PASSKEY_RP_ID`. Stable mounts the `GOOGLE_*`, `GITHUB_*` and `APPLE_*` secrets that have a version; previews mount only the `*_CLIENT_ID` ones and configure providers with the placeholder secret `OAUTH_PROXY_PREVIEW_CLIENT_SECRET`, since stable exchanges every code. `STABLE_PASSKEY_RP_ID` overrides the derived rpID, terraform creates the secret containers, and the stable web build defaults to the same untagged Cloud Run URL as the deploy. New guide: [Set up social login](docs/how-to/set-up-social-login.md).
205
+ - `OAUTH_CALLBACK_TAG=pr-<n>` makes that example preview own the registered OAuth callbacks: it mounts every provider secret and exchanges codes itself, other previews proxy through it, and stable gets only client IDs. It lets the providers be tried before stable runs a release with this setup. See [Register on a preview first](docs/how-to/set-up-social-login.md#register-on-a-preview-first).
206
+ - New `set-up-social-login` skill (planning plugin): the order of work for Google, GitHub, and Sign in with Apple in a Terreno app, from server config and callback URLs to secrets, preview-first registration, and the `/terreno/methods` check. [Set up social login](docs/how-to/set-up-social-login.md) now covers the whole setup end to end: setting the gcloud project, choosing same-site hosts, the server config, a callback URL table for local dev, previews, and stable, and how to check the result.
207
+ - **Provisional consent signing.** Staff can record consent on a user's behalf with
208
+ `POST /consent-responses/provisional` and a required note. Pass `accessControl` to
209
+ `ConsentApp` and grant the new `consentResponse:signOnBehalf` RBAC action (the built-in
210
+ `admin` role gets it on the next role seed); without RBAC only `user.admin` may sign. A
211
+ provisional signature counts as consent but leaves the form in the user's consent gate,
212
+ which now shows "Signed on your behalf by <name> on <date>". The user's own answer always
213
+ wins.
214
+ - `getConsentStatuses({userId})` (`@terreno/api`) and `GET /consent-responses/status` return
215
+ each form's `pending`, `provisional`, `signed`, or `declined` status. `POST
216
+ /consent-responses/:id/void` voids a provisional signature and keeps the record. With
217
+ `AuditApp`, signing, voiding, and status reads write audit events; audit events gain a
218
+ `read` operation and a `viewed` verb.
219
+ - `ProvisionalConsentSigner` and `useProvisionalConsent` (`@terreno/ui`) list a user's forms,
220
+ sign with a note, print, and void. `printConsentHtml` prints consent HTML on native and web,
221
+ and `buildConsentPdfHtml` renders a "Provisional signature" section.
222
+ - `ConsentResponseViewer` and `ConsentHistory` (`@terreno/admin-frontend`) show provisional
223
+ signatures and gain Print buttons, and their PDFs include the provisional signature.
224
+ - `@terreno/rtk`: `createRememberedAccountsStore({rememberIdentity})` remembers who signed in on
225
+ this device and how, for the login screen's "last used" badge and "Continue as" cards. It stores
226
+ `{userId, method, lastUsedAt}` in SecureStore on native and localStorage on web, keeps the newest
227
+ 5, re-reads storage before each change (and follows `storage` events on web) so a stale tab
228
+ cannot bring back a forgotten account, and resets on unreadable data. `rememberIdentity` is
229
+ required: pass `false` to keep only the method. With `true`, the name, a masked email
230
+ (`j***@n***.io`) and an `https://` avatar are stored too, and switching back to `false` scrubs
231
+ them on the next load. `useRememberedAccounts(store)` returns `{accounts, lastUsedMethod,
232
+ isHydrated, forget, clear}`, and `toLoginAccountOptions()` maps accounts to `@terreno/ui`
233
+ `LoginScreen`'s `rememberedAccounts`.
234
+ - `@terreno/rtk`: `createBetterAuthClient({features})` adds the Better Auth client plugins for the
235
+ server's auth features: `twoFactor` (`twoFactorClient`, with `onTwoFactorRedirect`), `passkey`
236
+ (`passkeyClient` from `@better-auth/passkey/client`), `multiSession`, `lastLoginMethod` and
237
+ `oneTimeToken`. Each enabled feature's namespace is typed without casts (`client.twoFactor.enable`,
238
+ `client.passkey.addPasskey`, `client.multiSession.listDeviceSessions`, ...), and the plugins run
239
+ after the Expo plugin on native. Without `features` (or with a config whose type does not name
240
+ them) the client and its type are unchanged. In web bearer mode `multiSession` lists no
241
+ sessions, and 2FA and passkey challenges still need cookies. New types:
242
+ `BetterAuthClientFeatures`, `BetterAuthClientWithFeatures`, `BetterAuthClientFor`,
243
+ `BetterAuthHandoffClient`.
244
+ `BetterAuthOAuthProvider` now accepts any generic OAuth provider id, the OAuth handoff helpers
245
+ accept clients with features, and `BetterAuthClientInterface` accepts the stricter session types
246
+ those clients return, so they work with `generateBetterAuthSlice`, which now stores a missing
247
+ user `image` as `null`.
248
+ - `AccountSecurityApp` emails the affected user about every security action, admin and
249
+ self-service.
250
+ - Each email says who acted ("you" or "an administrator", never the admin's name), when, and how
251
+ to get help.
252
+ - It names the org only with `notifications.includeOrgName: true` (default `false`), and never
253
+ includes tokens, links, passkey names, or other secrets.
254
+ - Mail goes out only when `sendMail` is configured. `notifications: {enabled: false}` turns it
255
+ off, and `notifications.sendMail` supplies a mailer outside `TerrenoApp`.
256
+ - `notifications.throttleMinutes` (default 10) sends at most one email per user, template,
257
+ origin (self or admin), and org per window, so an admin action is never hidden behind the
258
+ user's own change.
259
+ - A failed delivery is logged and never fails the action.
260
+ - `send-password-reset` sends no extra email, because its reset mail is the notice.
261
+ `issue-sign-in-link` tells the user a link was created, without the link.
262
+ - The newest registered `AccountSecurityApp` owns the emails, so two apps in one process never
263
+ email twice. `unsubscribeNotifications()` stops them.
264
+ - `createBetterAuth` records the user's own security changes as security actions with
265
+ `origin: "self"`, written as `AuditEvent`s when `AuditApp` is registered.
266
+ - Recorded: a provider linked or unlinked, a passkey added or removed, two-factor turned on or
267
+ off, a password changed or reset with a link, and other sessions signed out.
268
+ - Two-factor changes are read from the database, not the cookie-cached session.
269
+ - The new action ids are `password-reset` and `other-sessions-revoked`.
270
+ - The emails render with a built-in renderer. `BetterAuthConfig.renderAuthMail` is not used for
271
+ them; opt in to a custom renderer with `notifications.render`. `BetterAuthConfig.authMailTemplates`
272
+ accepts the new `security*` ids as overrides.
273
+ - `@terreno/comms` `renderAuthMail` renders the `security*` template ids (subject, text, and HTML),
274
+ so it can be passed as `notifications.render`. It takes optional `data`, `token` is now optional,
275
+ and HTML bodies get HTML-escaped values.
276
+ - `@terreno/rtk` adds `TerrenoAccountSecurityScreen`, a drop-in account security screen, and `useAccountSecurity`, the hook behind it. It renders `@terreno/ui`'s `AccountSecurityScreen` with the cards the server's `/terreno/methods` turns on: linked providers (linking through the OAuth handoff in bearer mode), the password (change, or set a first one), passkeys (web), two-factor (enrolment returns the backup codes to the card), and signed-in sessions ("Sign out everywhere else" calls `revokeOtherSessions`). Every Better Auth `{error}` becomes the calling card's error, each change reloads only the affected section, the screen never asks the server to remove a user's only sign-in method, `SESSION_NOT_FRESH` reads "Sign in again to manage this.", and a credential row without a password switches the password card to set mode.
277
+ - `useAuthMethods` returns `refetch()` for a Try again button and takes `{skip: true}` for callers that already have the methods.
278
+ - A link's `?error=`, an error code, or a provider id that matches an `Object.prototype` key (`__proto__`, `constructor`, `toString`) reads as the generic link error or goes unlabelled, instead of crashing the screen with a non-string value.
279
+ - `@terreno/rtk` adds `TerrenoLoginScreen`, a drop-in login screen, and `useLoginController`, the hook behind it. They read the server's `/terreno/methods` and skip the screen when a session is live. They offer "Continue as" cards for this device's signed-in sessions (`multiSession`, not in bearer mode) and its remembered accounts, a session card hiding the same user's remembered card. They badge the last used method from the remembered-accounts store, offer passkeys (button and web autofill), and sign in socially through the OAuth handoff in bearer mode or `signIn.social` otherwise. The two-factor step runs inline unless `onTwoFactorRequired` routes to the app's own screen. Each finished sign-in is recorded in the remembered-accounts store.
280
+ - `getLoginFeedback` turns server refusals into login guidance: `account_not_linked` worded for the linking mode, `VERIFY_EMAIL_TO_USE_ACCOUNT` as "verify your email", and `signup_disabled` (and the ID-token "signup disabled") as "ask your administrator for access", with the sign-up link hidden. Text from a URL is never shown.
281
+ - `getBetterAuthClientWebAuth(client)` reports whether a client is in web bearer mode and the token it holds. `recordRememberedSignIn` records a sign-in finished on an app's own screen.
282
+ - `@terreno/ui` `LoginScreen` takes `initialValues`, which prefill form fields; a new object later replaces those fields and keeps what the user typed in the others. With `fields={[]}` it leaves out the form block (fields, Log In, forgot-password, and the email dividers), and `TerrenoLoginScreen` does that when the server has email and password off. `OAuthButtons` takes `dividerPosition: "none"`.
283
+ - `TerrenoLoginScreen` takes `onStepChange`, called with each step the screen moves to, so an app that already holds a session can leave a login route the screen could not skip.
284
+ - A remembered method or a provider id that matches an `Object.prototype` key (`__proto__`, `constructor`) is named from the id ("Proto", "Constructor") instead of an inherited value.
285
+ - `@terreno/ui` adds `LinkedAccountsCard` and `PasswordCard`, the first account security cards.
286
+ `LinkedAccountsCard` shows one row per linked account (so two Google accounts are each
287
+ unlinkable) with its email and linked date, and a Link row for each offered provider without
288
+ one. Unlink asks for confirmation and calls `onUnlink(providerId, account)`; pass `account.id`
289
+ (Better Auth's account row id) to `unlinkAccount({accountId})`. The acting row shows a spinner
290
+ while `onLink` or `onUnlink` runs, and Unlink is disabled with an explanation when it would
291
+ remove the user's last sign-in method (the `credential` password account and
292
+ `otherMethodCount` count; `canUnlinkLast` opts out). `PasswordCard` shows "Change password"
293
+ (current, new, confirm, optional "Sign out of other devices" sent as `revokeOtherSessions`) or
294
+ "Set a password" for provider-only users, with inline length and confirmation errors, focus on
295
+ the first invalid field plus an alert summary, Enter to submit, and a promise-based `onSubmit`.
296
+ Also exported: the `LinkedAccount` (`linkedAt` accepts a `Date` or ISO string),
297
+ `LinkableProvider` and `PasswordCardSubmission` types, `CREDENTIAL_PROVIDER_ID`, and
298
+ `hasPasswordCredential(accounts)`.
299
+ A provider id that matches an `Object.prototype` key (`__proto__`, `constructor`) is labelled
300
+ with its id instead of an inherited value.
301
+ - `@terreno/ui` adds `AccountSecurityScreen`, a security settings screen composed from the account
302
+ security cards. Pass each card's props as one object (`linkedAccounts`, `password`, `passkeys`,
303
+ `twoFactor`, `sessions`); a card renders only when its object is given, so a feature the server
304
+ has off hides its card. Cards appear in a fixed order (sign-in methods, then passkeys, two-factor
305
+ and sessions) in one centered column up to `maxWidth` (720px by default), under a "Security"
306
+ heading with optional `description` and `header`. `isLoading` shows a spinner, `error` shows a
307
+ page-level alert with Try again (`onRetry`), and `children` add app-specific content after the
308
+ cards. The screen wraps itself in a scrolling `Page`; pass `withPage={false}` to place it inside
309
+ your own `Page` or `ScrollView`. Also exported: the `AccountSecurityScreenProps` type.
310
+
311
+ `Heading` gains an optional `level` (1–6): with it, the heading renders `role="heading"` and
312
+ `aria-level` (an `<h1>`–`<h6>` on web, the header accessibility role on native); without it,
313
+ output is unchanged. The account security cards' titles are now level 2 headings and the screen
314
+ title level 1. Below the desktop floor (`isSupportedDesktopWidth`), `Card` and
315
+ `AccountSecurityScreen` draw edge to edge.
316
+ - `@terreno/ui` adds `FormContainer`: a real `<form>` on web (`display: contents`, `noValidate`, page reload always prevented) and a `Box` on native. `ref.requestSubmit()` submits through the form element on web so password managers see a real submit and offer "Save password"; `username` adds a hidden `autocomplete="username"` input for change-password forms. Chrome no longer logs "Password field is not contained in a form" on `LoginScreen`, `SignUpScreen`, `PasswordCard`, the `TwoFactorSettingsCard` password step, `GPTChat`'s API key modal, the admin configuration screen, or the example app's reset-password, profile, and admin password forms.
317
+ - `TextField` sends `autoComplete` to native too: React Native's `autoComplete` (Android's autofill hint) and the matching iOS `textContentType` (`username`, `emailAddress`, `password`, `newPassword`, `oneTimeCode`, `none`), so iOS and Android offer saved passwords, strong new passwords, and one-time codes. It also accepts `"one-time-code"`.
318
+ - `LoginScreen` and `SignUpScreen` default the email field to `autoComplete="username"`, name each input by its field label, and handle Enter: it moves to the next field and submits from the last one once, only when the submit button is enabled. `PasswordCard` now moves from current to new to confirm on Enter and submits from confirm. `OtpField` gains `onEnter`, and `TwoFactorChallengeScreen` submits a complete code on Enter.
319
+ - `PasswordCard` and `TwoFactorSettingsCard` take a `username`, which `useAccountSecurity` fills with the session user's email.
320
+ - `LoginScreen` (`@terreno/ui`) gains optional props for returning users: `lastUsedMethod` badges the
321
+ method this device used last and moves a provider or passkey above the form; `currentSessions` and
322
+ `rememberedAccounts` render one-tap "Continue" cards (`onContinue`, `onForget`); `onPasskeyPress`
323
+ adds a "Sign in with a passkey" button and `passkeyAutofill` asks web browsers to offer saved
324
+ passkeys in the email field; `notice` shows guidance such as an `account_not_linked` hint; and
325
+ `genericProviders` renders providers without a branded button (for example a generic OIDC
326
+ provider). `OAuthButtons` accepts `genericProviders`, `lastUsed`, and `dividerPosition`. Without the
327
+ new props the screen renders exactly as before. A method or provider id that matches an
328
+ `Object.prototype` key (`__proto__`, `constructor`) is named by its id, and the new
329
+ `getSocialProviderName(providerId)` returns a branded provider's name or `undefined`.
330
+ - `@terreno/ui` adds `PasskeysCard` and `SessionsCard` to the account security cards.
331
+ `PasskeysCard` lists passkeys with their added and last-used dates and a "Synced" badge for
332
+ backed-up ones (an unnamed passkey shows `authenticatorName`, for example from
333
+ `getAuthenticatorName(aaguid)`, else "Unnamed passkey"), and adds (with an optional name, `onAdd(name?)`), renames (`onRename(id, name)`)
334
+ and removes (confirmed, `onRemove(passkey)`) them; `supported={false}` hides Add and explains that
335
+ the device can't create passkeys. `SessionsCard` lists sessions with the browser and OS parsed
336
+ from the user agent, location and IP when known, "Signed in" (`createdAt`) and "Last refreshed"
337
+ (`updatedAt`, which Better Auth moves only once per `session.updateAge`) as Luxon relative times
338
+ that recompute every minute (inject `now` for deterministic output). The current session, matched by `currentSessionId` or
339
+ `currentSessionToken`, comes first, is marked "This device", and can't be revoked on its own;
340
+ other sessions have Sign out (`onRevoke(session)`), and "Sign out everywhere else"
341
+ (`onRevokeOthers()`) asks for confirmation and is disabled when no other session exists. Better
342
+ Auth's `listUserPasskeys()` and `listSessions()` items fit the new `AccountPasskey` and
343
+ `AccountSession` types as is. Also exported: `parseUserAgent` and `describeUserAgent`, a small
344
+ dependency-free user agent parser that also recognizes React Native app clients. The account
345
+ security docs and examples now unwrap Better Auth's `{data, error}` results and throw, since the
346
+ client does not reject and the cards only show errors from rejected callbacks.
347
+ - `@terreno/ui` adds `OtpField` and `TwoFactorChallengeScreen`. `OtpField` is a one-time code input
348
+ with a cell per digit: paste or one-time-code autofill fills every cell, backspace steps back a
349
+ cell, each cell is labelled "Digit k of N", and `onComplete(code)` fires when the code is full.
350
+ `TwoFactorChallengeScreen` is the second sign-in step: an authenticator code that submits itself
351
+ once complete, a toggle to a backup code, an optional "Don't ask again on this device for N days"
352
+ checkbox (`trustDeviceDays`), an alert error that clears the code, and
353
+ `onSubmit({code, kind: "totp" | "backup", trustDevice})`. `TextField` accepts
354
+ `accessibilityLabel` to set its input's accessible name.
355
+ - `@terreno/ui` adds `TwoFactorSettingsCard` to the account security cards. Turning on two-factor
356
+ authentication walks through four steps: confirm the password (`onStartEnroll(password)` resolves
357
+ `{totpURI, backupCodes}`, narrowed from the TOTP branch of Better Auth's `twoFactor.enable()`
358
+ data, which is `{method: "otp"}` or `{method: "totp", totpURI, backupCodes}`), scan a QR code drawn with
359
+ `react-native-qrcode-svg` or copy the setup key under it, enter a code in `OtpField`
360
+ (`onVerify(code)`), then save the backup codes with Copy and Download (a `.txt` file on web) or
361
+ Share (native), with Finish gated on "I saved these backup codes". Once on, the card offers new
362
+ backup codes (`onRegenerateBackupCodes(password)`, shown with the same gate) and Turn off
363
+ (`onDisable(password)`, after a destructive confirm), and warns when `backupCodesRemaining` drops
364
+ below 3. Better Auth requires the password unless its two-factor plugin sets `allowPasswordless`,
365
+ so `hasPassword={false}` disables the card's actions and asks an OAuth-only user to set a
366
+ password first, and
367
+ `requirePassword={false}` skips the password steps on a passwordless server. Also exported:
368
+ `parseTotpSecret(totpURI)`, `parseTotpDigits(totpURI)` (the code length defaults to it) and the
369
+ `TwoFactorEnrollment` type. Step changes move focus to the step on web and are announced on native.
370
+
371
+ ### Changed
372
+
373
+ - `@terreno/admin-frontend` renders a nested (non-array) object field that has `items` in its admin config as a titled group with one control per subfield, instead of a JSON text box. Groups recurse to any depth and also render inside array-of-subdocument items. `Mixed`, `Map`, and objects without `items` keep the JSON editor, and a field `widget` still takes precedence. Subfield test IDs use the dotted path (`admin-field-tasks.enabled`, group `admin-field-group-tasks`); inside array items, nested groups are prefixed with the array key and item index (`admin-field-servers.0.media.port`). New array items and create-mode nested objects start from subfield defaults and leave out system fields and enums, refs, dates, and `Mixed` subfields that have no default. Save still sends the whole object for the field.
374
+ - The example app's login route is now `TerrenoLoginScreen` with `rememberIdentity: true`. It shows a badge on the last used method and "Continue as" cards after sign-out, offers a passkey button on web, and runs the two-factor challenge inline. Its Better Auth client turns on the `multiSession`, `passkey` and `twoFactor` features. The mock OIDC provider's button is now the login screen's generic provider button, `login-screen-oauth-mock-oidc`, which replaces `login-mock-oidc-button`. The root layout leaves a signed-in user on `/login`, so the screen can record a returning social sign-in before it moves on. The example's `logout` stops syncdb before it dispatches rtk's `logout()`, and the profile screen shows the sign-out in progress on its button, and the error when a sign-out could not reach the server. The root layout shows its loading spinner only while no user is loaded, so a session refresh or a sign-out keeps the current screen.
375
+ - e2e: `loginAs` answers the two-factor challenge for a user with a `totpSecret`. `crossSiteChallenges.spec.ts` now runs (its `test.fixme` is removed). New cross-site specs `crossSiteLogin.spec.ts`, `two-factor.spec.ts` and `account-linking.spec.ts` cover the last used method, "Continue as", passkey sign-in, TOTP and backup codes, and email and mock-OAuth sign-ins that end up in one account. `E2E_ACCOUNT_LINKING=explicit` starts the backend in explicit linking mode for that spec's explicit-mode case.
376
+ - The example app has a Security screen at `/settings/security` (`TerrenoAccountSecurityScreen`), opened from the profile's **Security** button (`profile-security-link`). It links and unlinks providers, changes the password through Better Auth's `changePassword`, adds and removes passkeys, turns two-factor on with backup codes, and signs out other sessions. The profile's "New Password" field, which sent `password` to `PATCH /auth/me` (refused since 57.17.0), is gone; the "Current password" field an email change needs stays.
377
+ - e2e: `account-security.spec.ts` joins the cross-site bearer run. It links the mock OIDC provider and unlinks it (the password still signs in), turns on two-factor and signs in with a saved backup code, adds and removes a passkey through the virtual authenticator, signs out a second browser's session (which then gets 401), and changes the password (the old one is refused, the new one signs in). `profile.spec.ts` opens Security from the profile in the cookie-mode run.
378
+ - The hosted Terreno MCP server moves to `https://mcp.terreno.app/mcp`. The README, docs, and `create-terreno-app` now use it. `https://mcp.terreno.flourish.health/mcp` keeps serving for at least one more release; update `.cursor/mcp.json`, `.mcp.json`, and `claude mcp add` registrations as described in `docs/how-to/set-up-terreno-mcp.md`.
379
+ - Card, MarkdownEditor, the admin shell, and the admin filter drawer switch to the narrow layout below the desktop floor (1024pt) instead of 768pt. Admin home and the example sync debugger stack their side-by-side panes below that floor. Demo headers and stories read the live window width instead of a one-time viewport check. `isSupportedDesktopWidth` reports that floor. `ADMIN_FILTER_MOBILE_BREAKPOINT` is 1024.
380
+ - `useLoginController` maps a cancelled sign-in (`AUTH_CANCELLED`, or a message of "auth cancelled"
381
+ / "authentication cancelled") to an info notice. A wrong email or password
382
+ (`INVALID_EMAIL_OR_PASSWORD`, `INVALID_PASSWORD`, `INVALID_EMAIL`, or those messages on a response)
383
+ is `passwordError` on the login screen instead of the error banner. Continuing a remembered email
384
+ account that has a usable address sets `continuingAccount` and opens the password step.
385
+ Canceling that step, or canceling two-factor, clears `continuingAccount` and sets the form
386
+ password back to empty so the next account does not keep the previous password.
387
+ `account_not_linked` notices name the original method and, when that method is known, offer
388
+ "Sign in with {method}". The two-factor failure message tells the user to enter the code their
389
+ app shows now. `TerrenoLoginScreen` passes the email that started the challenge as
390
+ `accountLabel`. Pass `initialTrustDevice={false}` to start "Trust this device" unchecked;
391
+ otherwise the inline challenge keeps the authenticator default (checked).
392
+ - `@terreno/rtk` `generateBetterAuthSlice`: `logout()` no longer clears the session before the server answers. The session stays in the state until `authClient.signOut()` finishes (`signOutTimeoutMs`, default 10 seconds), and then it is cleared. Meanwhile the new `isSigningOut` state (`selectBetterAuthIsSigningOut`) is true; `isLoading` is left alone, so a navigator gated on it keeps its screen. Before, a login screen that opened at once could find the still-live session and sign the user straight back in. A sign-out that fails or times out checks the session first. A session that is still live, or a cookie or native session the server cannot be asked about, stays signed in, and `error` is set to `SIGN_OUT_FAILED_MESSAGE` ("Couldn't sign out. Check your connection and try again."). A web bearer client, which drops its token before the request leaves, ends signed out. The global `auth/logout` action (a refused session) now ends signed out even when the sign-out request fails. With `apisToReset` (57.17.0), `logout()` purges the cache when the session clears, not when it is dispatched: a purge at dispatch refetched every mounted screen mid-sign-out, and a refetch that could not reach the server answered 401 and signed the user out even though the sign-out failed. A failed sign-out keeps both the session and the cache. The global `auth/logout` action still purges at once (only the first of several in flight) and again when the session clears. A sign-in for another user that lands while a sign-out is in flight is kept; a sign-out that started signed out asks `getSession` first, so a restore of the session it revoked is cleared. Show `selectBetterAuthIsSigningOut` on the sign-out control and `selectBetterAuthError` after a failed sign-out.
393
+ - `@terreno/ui`: `SyncStatusBanner` no longer flashes "Offline" while the sync socket first connects or during brief drops. The badge appears only after `isOnline` stays false for `offlineGraceMs` (default 3000 ms) and hides as soon as the client reconnects. Pass `offlineGraceMs={0}` to show it immediately, as before.
394
+ - `LoginScreen` matches the login redesign. Method pills sit above the email form. Returning users
395
+ see a continue-as list, with other methods behind "Sign in another way". Edit removes a remembered
396
+ account from this device; a signed-in session cannot be removed there. A remembered email opens a
397
+ password step (`continuingAccount`). A wrong password is `passwordError` on the password field.
398
+ `error` is a banner, and one `notice` (info, warning, or error) replaces it. The submit button
399
+ reads "Sign in". `TwoFactorChallengeScreen` titles the authenticator step "Enter your code" and
400
+ the backup step "Use a backup code", checks "Trust this device" by default for an authenticator
401
+ code (`initialTrustDevice={false}` starts it unchecked), and shows the `error` string it was
402
+ given. `OAuthButtons` `appearance="pill"` and
403
+ `SocialLoginButton` `variant="outline"` render the same neutral pill.
404
+
405
+ ### Deprecated
406
+
407
+ - `installTerrenoDevConsoleLogger` moves to `@terreno/syncdb` (also `@terreno/syncdb/devConsoleLogger`) and takes `{baseUrl}`. The `@terreno/rtk` export is a deprecated wrapper that keeps working with no arguments.
408
+ - `terreno-mcp-local` adds `get_client_state`, which inspects either the RTK Query store or a SyncDB client and returns a `layer` field (`rtk`, `syncdb`, `syncdb+rtk`, or `none`). `get_rtk_state` is now a deprecated alias for one release and returns the same wrapped response. `terreno state` uses the new tool.
409
+
410
+ ### Fixed
411
+
412
+ - `@terreno/admin-frontend`: an admin record no longer stays on its loading spinner when the organization switcher settles on a default organization after the record's request started. `useAdminApi`'s `useListQuery` and `useReadQuery` now carry the organization in the query argument, so the switch fetches the new organization's entry instead of waiting on one that was never requested. A read-only user record's `roles` field also no longer requests `GET /rbac/roles`, which staff without `rbac:read` (such as an operator viewing a user) were refused with 403; it lists the assigned role names instead.
413
+ - The admin user security header clears the previous user's name and email while the next user's record loads, so a recovery action is not confirmed under the wrong identity.
414
+ - `@terreno/api` answers a malformed JSON request body with 400 `Malformed request body` instead of 500 `Internal server error`. `apiErrorMiddleware` now converts body-parser client errors with the new `bodyParserErrorToAPIError` helper and keeps the parser's status: an oversized body returns 413 `Request body too large`, and an unsupported charset returns 415 `Unsupported request body encoding`. The parser's message is in `detail`. These errors are logged at `warn` and are not sent to Sentry.
415
+ - `@terreno/api`: a Better Auth password reset no longer gives a social-only account a JWT password. `onPasswordReset` now copies the new password to the app User's passport hash only when that User already has one (the same rule as `terreno/set-password` and `change-password`), so an account that signed in only with a provider cannot then sign in at `POST /auth/login`, which has no second factor. A dual-enrolled account still gets its JWT password replaced, and every matching app User's `tokenEpoch` is still bumped, so outstanding JWT refresh tokens and MCP credentials stop working after any reset.
416
+ - Terreno's Better Auth password reset sync to the app User's JWT password now also reads the new password from the parsed body of the Better Auth endpoint being served, so direct `auth.api.resetPassword` calls and the email-OTP and phone-number resets (which send `password`) update the passport hash and bump `tokenEpoch` the same way `POST {basePath}/reset-password` does.
417
+ - OpenAPI specs and model metadata from `@terreno/api` no longer mark a field as required when
418
+ its Mongoose `required` is a function. Such a field is only required on some documents, so
419
+ generated SDKs now type it as optional (for example `ConsentResponse.provisionalNote` and
420
+ `GptHistory.text`). Saving still enforces the condition.
421
+ - Apps generated by `create-terreno-app` now send web console errors and warnings to the backend, so `read_logs` and `last_error` with the `browser` source see them. The generated `frontend/app/_layout.tsx` calls `installTerrenoDevConsoleLogger({baseUrl})` from `@terreno/syncdb` at startup, and the generated `backend/.env` sets `NODE_ENV=development` so `TerrenoApp` registers `POST /__terreno/browser-logs`. In an existing app, add both lines; see `docs/how-to/set-up-terreno-mcp.md`.
422
+ - `create-terreno-app` MCP configs now start `terreno-mcp-local` with `bunx -p @terreno/mcp terreno-mcp-local`. The old `bunx terreno-mcp-local` entry failed in generated apps because no npm package has that name. Generated apps also get a root `.mcp.json` for Claude Code with the same servers as `.cursor/mcp.json`. In an existing app, add `"-p", "@terreno/mcp"` before `"terreno-mcp-local"` in the `terreno-local` args; see `docs/how-to/set-up-terreno-mcp.md`.
423
+ - `installTerrenoDevConsoleLogger` no longer logs `Requiring unknown module "<id>"` on the first lazy component load on web (for example `ConsentNavigator` after login). Metro reports the missing split-bundle module to `ErrorUtils` while Expo's web `import()` probes it, and Expo then catches the rethrown error and loads the bundle. The logger now records an `ErrorUtils` error only after the previous handler consumes it. An error that is rethrown and never caught is still logged by the window listeners, and unhandled rejections now include the `Error` stack.
424
+ - `@terreno/api`: with `BetterAuthApp` registered, the JWT recovery routes no longer give an account without a JWT password one. `POST /auth/forgotPassword` mails no legacy reset link to such an account (it still answers 202, so the response does not reveal the account), and `POST /auth/resetPassword` (and the `POST /resetPassword` alias) refuses its token with 400 `legacy-password-reset-unavailable` and issues no tokens. Before, a social-only account could reset through these routes and then sign in at `POST /auth/login`, which has no second factor. That account resets through Better Auth instead. Accounts that already have a JWT password reset here as before, and apps without Better Auth are unchanged.
425
+ - `terreno_bootstrap_ai_rules` now returns files that `rulesync generate` accepts with rulesync 9. `rulesync.jsonc` and the root rule target `cursor`, `claudecode`, `copilot`, and `agentsmd` (rulesync 9 has no `windsurf` target; Windsurf reads `AGENTS.md`). The single `.rulesync/rules/00-root.md` root rule now covers Claude Code, so the tool no longer returns the `01-claudecode-root.md` `localRoot` rule that rulesync rejected; delete it if an earlier bootstrap left it behind. The instructions run `bunx rulesync@9 generate` instead of the removed `rulesync sync`. The root rule now includes everything `create-terreno-app` writes to `CLAUDE.md` and `.cursorrules` (including the "Adding Features" steps), and the instructions say to delete the superseded `.cursorrules` and to move hand edits to `CLAUDE.md`, `AGENTS.md`, `.claude/rules/`, `.cursor/rules/`, and `.github/instructions/` into `.rulesync/rules/10-project.md` before generating, because `rulesync generate` overwrites or deletes them.
426
+ - The MCP server's `terreno_bootstrap` frontend template drops the profile screen's "New Password" field. It sent `password` to `PATCH /auth/me`, which refuses password fields since 57.17.0; change passwords through Better Auth (`changePassword`, or `TerrenoAccountSecurityScreen` from `@terreno/rtk`). The "Current password" field that an email change needs stays.
427
+ - `terreno_generate_form_fields` now emits code that compiles against `@terreno/ui`: fields use `title`, `onChange`, and `errorText` (not `label`, `onChangeText`/`onChangeValue`, `error`), dates use `type` (not `mode`), `NumberField` gets `type="number"` and a string value, passwords use `TextField type="password"`, and required fields declare the `errors` state they read. A test type-checks the output against the `@terreno/ui` declarations so prop drift fails CI. The `terreno://docs/ui` form-field example uses the same props.
428
+ - `terreno_validate_model_schema` no longer reports "Missing TypeScript interfaces" when the model imports its `Document` type from `src/types/models/` or `src/modelInterfaces.ts`; it still reports a model with no `Document` type.
429
+ - The hosted Terreno MCP image now builds the full `@terreno/ui` props data and the versioned docs search corpus, so `terreno_get_component_docs` knows every component and `terreno_search_docs` searches versioned docs instead of only the bundled resources. The `terreno://docs/patterns` resource now shows SyncDB collection CRUD instead of the deprecated RTK pattern.
430
+ - `terreno-mcp-local` CDP tools (`read_logs` source `app`, `last_error`, `get_client_state`, `evaluate`, `navigate`) connect to Metro again on Expo 57. The debugger socket now sends the `Origin` header React Native dev-middleware requires, and when another debugger such as React Native DevTools takes the session the status names Metro's close reason.
431
+ - `terreno-mcp-local` `database_schema` and `database_query` work under Bun again. They previously failed with `undefined is not an object (evaluating 'mongoose.connection.readyState')` whenever a Mongo URI was found; each call now uses its own short-lived connection.
432
+ - `GET /oauth/authorize` accepts a loopback `redirect_uri` (`localhost`, `127.0.0.1`, or `[::1]`) on any port when a registered URI has the same scheme, host, path, and query (RFC 8252 §7.3). The authorization code is returned to the port the client sent. Those three hosts do not match each other, and every other redirect URI must still match exactly.
433
+ - Prevented modelRouter array-item PATCH requests with nested subdocument arrays from failing after the first update.
434
+ - The example app's native (Android) sign-in now works with the local mock OIDC provider: Better
435
+ Auth's Expo authorization proxy refuses plain-http provider URLs, so example-backend serves that
436
+ proxy for the mock issuer's origin itself while `MOCK_OIDC_ISSUER` is set. The mock provider takes
437
+ `MOCK_OIDC_PORT`, `MOCK_OIDC_ISSUER` and `MOCK_OIDC_REDIRECT_URI`, and
438
+ `example-frontend/maestro/native-social-login.yaml` drives the sign-in on an emulator. See the new
439
+ "Native" section of `docs/how-to/set-up-social-login.md`.
440
+ - The `operator` role can find users in the admin panel and open a user's record, and from there their Security screen, in apps that declare an `adminUser` statement (as the example app does). Seeding gives the operator `adminUser:read` (never `write`), and an operator row seeded earlier gains it once through `appliedGrants`. Before this fix, `/admin/config` left User out for operators and `/admin/User/:id` showed a spinner that never finished.
441
+ - `@terreno/api`: `OrgsApp` now clears a user's cached permissions when their membership is added, changed, or removed through `POST` / `PATCH` / `DELETE /orgs/:id/members`. Before, a newly attached org-admin could be refused the admin page (no `admin:access`) for up to the 30-second permission cache TTL, and a demoted, suspended, or removed org-admin kept it as long. Disabling (`PATCH /orgs/:id` with `disabled: true`) or deleting (`DELETE /orgs/:id`) an organization, which suspends all of its memberships, now clears the cached permissions of every member too, so the organization's org-admins lose `admin:access` on their next request.
442
+ - A server bundled with `bun build --compile` no longer crashes at startup with "tsyringe requires a reflect polyfill". `@terreno/api` now depends on `reflect-metadata` and loads it before the passkey plugin, whose `@peculiar/x509` dependency needs it.
443
+ - `@terreno/api`: the account-security reach check (an org-admin or platform operator acting on another user) and the RBAC user-write guard no longer let an actor through because a permission source was down. A failed source drops its grants from the target's resolved permissions, which made the target look less privileged than it is. Both now resolve the target with the new `access.getPermissions({user, requireComplete: true})`, which answers 503 `Permission source unavailable` when a failed source's grants are unknown (grants served from a `"use-stale"` cache still count).
444
+ - `@terreno/api`: the RoleManager target-privilege rule (`access.roles.assign`, `unassign`, `previewAssignment`, and `assertCanModifyUser`, used by `PUT /rbac/users/:id/roles`, `POST /rbac/users/:id/roles/preview`, and `@terreno/admin-backend` user writes) now resolves the target user with `requireComplete`. Before, when an additive permission source was down, the target's grants from it were dropped, so the target looked less privileged and an actor without those permissions could change its roles. The check now answers 503 `Permission source unavailable` in that case. `createRoleManager` takes an optional `getTargetPermissions` for this.
445
+ - `SocialLoginButton` now follows later changes to its `loading` prop instead of reading it only on
446
+ the first render, so a parent can start or clear the spinner after mount.
447
+ - `@terreno/api`: a recovery action that changes something (such as `suspend-membership` under `/orgs/:orgId/members/:userId/security`) now clears the target's cached permissions, so a suspended org-admin loses `admin:access` at once instead of keeping it for up to the 30-second permission cache TTL.
448
+ - @terreno/syncdb no longer sends a stream discovery (`GET /sync/streams`) that a `stop()` overtook while it was reading the token. An app that stops syncdb and then signs out (as the example's `logout` does) used to see that request go out with the revoked session and fail with a 401. `createHttpChannel`'s `fetchStreams` takes an optional `{isSuperseded}` checked before the request is sent; a superseded request rejects with the new exported `RequestSupersededError` and reaches nothing.
449
+ - `@terreno/ui`: `TextField` now sets its `autoComplete` prop as the input's `autocomplete`
450
+ attribute on web (it was ignored before). Native rendering is unchanged. The prop also accepts
451
+ `"email"`, `"new-password"`, and `"username webauthn"` (passkey autofill).
452
+ - Because the hint now reaches the browser, `autoComplete="off"` takes effect on web and turns
453
+ autofill off for that field. The example app, `create-terreno-app`, and the MCP bootstrap
454
+ templates switch their email fields from `"off"` to `"username"` (login) and `"email"`
455
+ (sign-up).
456
+ - `Field type="password"` still defaults to `"current-password"`; pass `autoComplete="new-password"`
457
+ (also through `TapToEdit`) on fields that set a new password. The example admin set-password
458
+ fields now do.
459
+ `SignUpScreen` password fields default to `"new-password"` and `LoginScreen` password fields to
460
+ `"current-password"` unless the field config sets its own.
461
+ - `LoginScreen` names each email and password input by its field label for screen readers,
462
+ instead of the generic "Text input field".
463
+ - `Avatar` takes keyboard focus only when it is editable (`status="imagePicker"`), so a display
464
+ avatar, such as the one on a remembered-account card, is no longer an empty tab stop.
465
+
466
+ ## [57.19.2] - 2026-10-26
467
+
468
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.19.2.md`](mcp-server/src/docs/upgrades/57.19.2.md).
469
+
470
+ 57.19.0 and 57.19.1 were tagged but never published to npm (the publish gate failed before the release notes had merged). This release contains everything from both.
471
+
472
+ ### Added
473
+
474
+ - `@terreno/test` gains the test helpers Flourish built and proved in its own suites:
475
+
476
+ - `waitUntil` / `waitForCalls` to poll a condition instead of sleeping, and `freezeTime` to
477
+ freeze `Date.now()`, `new Date()`, and Luxon together.
478
+ - `authWithToken` for a supertest agent with a pre-minted token (no login round-trip),
479
+ `expectAPIError` for `APIError` bodies, and `assertStrictResponseProperties` to fail on any
480
+ response field outside an allowlist.
481
+ - Opt-in `mock.module` registrars for Cloud Tasks (with `drainTasks`), SendGrid, Twilio, Expo
482
+ push, GCS, Google auth, and the `@terreno/api` notifiers, plus `createMockLanguageModel` for
483
+ the AI SDK.
484
+ - `@terreno/test/expo/*`: `registerExpoTestEnvironment`, `renderWithTerreno`, in-memory auth
485
+ token storage, and a module-alias mocker for Bun + Expo tests. `example-frontend` now uses it.
486
+ - `terreno-test-shard` (weighted test sharding) and `terreno-test-hygiene` (AST lint for leaky
487
+ or flaky Bun test patterns, with a baseline) CLIs.
488
+ - Testing skills for Terreno apps, installable with the other agent skills: `terreno-test-setup`
489
+ bootstraps backend (Bun + in-memory Mongo), frontend (Bun + React Native Testing Library,
490
+ Playwright), and mobile (Maestro) test setups; `terreno-backend-tests`,
491
+ `terreno-frontend-tests`, and `terreno-mobile-tests` cover writing tests for each. New how-to:
492
+ [Set up tests in a Terreno app](docs/how-to/set-up-app-tests.md).
493
+ - `@terreno/ui` loading skeletons: `Box` and the container components (`Card`, `EditableCard`, `ChartCard`, `Scorecard`, `Page`, `SplitPage`, and the charts) take `loading`, `loadingComponent`, `loadingAnimation`, and `loadingLabel` props. New `Skeleton`, `SkeletonText`, `SkeletonCircle`, `SkeletonCard`, `SkeletonList`, and `SkeletonGroup` components support `pulse`, `shimmer`, or no animation and respect reduce-motion. `withLoading(Component, options)` adds the props to custom components. See [Show loading skeletons](docs/how-to/show-loading-skeletons.md). (#1571)
494
+ - Published packages now include the repository `CHANGELOG.md` in their tarballs. (#1632)
495
+
496
+ ### Changed
497
+
498
+ - `@terreno/cli` and `@terreno/mcp` require Bun `>=1.4.4`. 1.4.4 patches the Brotli decoder (CVE-2026-111497). CI, `example-backend`, and `mcp-server` images pin that release. `oven/bun:1` and `oven/bun:1-slim` can still resolve to an affected release, and `bun build --compile` embeds the builder's Bun binary. New apps from `create-terreno-app` get `oven/bun:1.4.4-slim`. Existing apps should set that tag in their Dockerfile and rebuild. See [Upgrade Terreno](docs/how-to/upgrade-terreno.md).
499
+
500
+ EAS build profiles stay on Bun 1.3.10. That pin is part of the Expo fingerprint, so changing it forces a native rebuild. Those profiles remain on an affected Bun release until that rebuild is scheduled.
501
+
502
+ ### Fixed
503
+
504
+ - `create-terreno-app` did nothing when run through its package bin (`bunx create-terreno-app`,
505
+ `npx create-terreno-app`): the entry check compared the module URL with the unresolved
506
+ `node_modules/.bin` symlink. It now resolves the path first.
507
+ - `@terreno/test` now installs and imports outside the monorepo. `supertest`,
508
+ `@types/supertest`, and the `mongodb-memory-server` packages moved from
509
+ `devDependencies` to `dependencies` (57.17.0 failed with `Cannot find package
510
+ 'supertest'` on import), and the published tarball no longer ships test files.
511
+ - `registerSimpleMongoPreload` now passes `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` (default 60s) to
512
+ `mongodb-memory-server`, so a cold `mongod` start no longer fails at the library's 10s default.
513
+
514
+ ## [57.18.0] - 2026-10-25
515
+
516
+ ### Added
517
+
518
+ - Vercel hosting: `example-backend` runs on Vercel Functions (exported `http.Server` with Socket.IO, full Bun bundle), and `example-frontend` and `demo` deploy as static Expo exports. CircleCI deploys all three to the `terrenolabs` Vercel team alongside Cloud Run and Netlify. See `docs/how-to/deploy-to-vercel.md` and `docs/how-to/vercel-function-websockets.md`.
519
+ - `@terreno/rtk`: `useSocketConnection({transports})` chooses the Socket.IO transports (default unchanged: polling, then websocket). Use `["websocket"]` on hosts without session affinity, such as Vercel Functions.
520
+ - `@terreno/syncdb`: `createSyncDb({socketTransports})` does the same for the default socket transport.
521
+ - New `deploy-vercel` agent skill.
522
+
523
+ ### Fixed
524
+
525
+ - `@terreno/syncdb`'s tests now cover the debug-recorder paths (replay failures, retries, rate-limit and auth nacks, batch sends, stream join/leave/skip, cursor migration, tombstone compaction, snapshot-batch fallback, `forceResync`, and subscribe catch-up), so the 95% coverage gate passes again on Bun 1.4.4, which counts the lines of debug event objects that never run without a recorder.
526
+
527
+ ## [57.17.0] - 2026-10-23
528
+
529
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.17.0.md`](mcp-server/src/docs/upgrades/57.17.0.md).
530
+
531
+ ### Breaking
532
+
533
+ - `PATCH /auth/me` now requires `currentPassword` when the body changes the email to a different mailbox (casing-only changes and an unchanged email echoed back still need none). A missing or wrong password gets 403 and nothing is saved; a user with no password (for example GitHub-only) gets 400 and must set one through the reset flow first. The password is checked against the passport hash, which counts toward its `limitAttempts` lockout, or against the Better Auth credential when `BetterAuthApp` is registered (new `authOptions.verifyBetterAuthPassword`, wired by TerrenoApp, and `verifyBetterAuthCredentialPassword`). After a change the old address gets a `sensitive: true` notice through `authOptions.sendMail` with the new address partially masked and no links. Clients that let users change their email must collect and send `currentPassword`; the example frontend, `create-terreno-app`, and the MCP bootstrap profile screens now show a "Current password" field when the email is edited. See [Changing the email on PATCH /auth/me](docs/reference/api.md#changing-the-email-on-patch-authme).
534
+ - `PermissionSource.failClosedWhenDenyUnknown` now defaults to `true`. When a source's `getGrants` fails for a user this process has no last-known deny posture for (a new instance, or the posture was evicted), permission checks throw 503 `Permission source unavailable` instead of resolving local roles without the source's `deny`. Behavior after a successful fetch is unchanged. Add `failClosedWhenDenyUnknown: false` to app sources that never return `deny` (additive-only role or permission mappings) to keep their old outage behavior. The built-in organization membership source already sets it. See [RBAC permission sources](docs/reference/api.md#rbac-permission-sources).
535
+ - With RBAC (`accessControl`), changing another user's `email`, `disabled`, `betterAuthId`, `githubId`, or `emailVerified` now requires the new `user:updateIdentity` permission in addition to `user:update`. This applies to modelRouter, sync, MCP, instance actions, and AdminApp `PATCH` and bulk-patch. Writes without it get 403 `Missing user:updateIdentity permission`. Newly seeded `admin` roles include it and `superadmin` gets it through `*`. `seedDefaults` does not update an `admin` role that is already stored, so existing deployments must add `updateIdentity` to that role's `user` actions (for example with `PATCH /rbac/roles/admin`) to keep RBAC admins able to edit user emails and disable users. Legacy `admin: true` users and self-writes are not affected. See [RBAC user identity writes](docs/reference/api.md#rbac-user-identity-writes).
536
+
537
+ ### Added
538
+
539
+ - `attach-pr-media` agent skill. It uploads local screenshots and recordings to one shared secret gist with only the `gh` token and posts Markdown that embeds them in the pull request, for sandboxes where `/opt/cursor/artifacts/` is unavailable. `verify-ui-changes` now points to it. Install with `npx skills add TerrenoLabs/terreno --skill attach-pr-media` or from the `terreno` Claude plugin.
540
+ - `BetterAuthConfig` takes `options` and `plugins`. `options` passes any other Better Auth option (for example `emailAndPassword.requireEmailVerification`, legacy `password.{hash, verify}`, or `databaseHooks`), deep-merged over Terreno's defaults with arrays replaced and explicit values winning. `plugins` are appended after `bearer()` and mount under `basePath`. Apps no longer need to replace `BetterAuthApp` to reach them. (#1578)
541
+ - `BetterAuthConfig.oauthHandoff: {webOrigins, callbackPaths?}` lets a web app on another site finish a social or generic OAuth sign-in in bearer mode without cookies. When the OAuth callback creates a session and redirects to one of `callbackPaths` (exact paths written in normal form, default `["/"]`; the query is free) on one of `webOrigins` (exact `http(s)` origins), the redirect carries `#ott=<token>` in its URL fragment: a single-use token that expires in three minutes and is stored hashed. `POST {basePath}/one-time-token/verify` exchanges it once for the session and the `set-auth-token` header. Neither the handed-off callback redirect nor verify sets a session cookie on the API's origin. App schemes, relative paths, the API's own origin, and origins that only `trustedOrigins` allows get no token. Clients cannot mint one (`GET /one-time-token/generate` is refused), and no response sends a `set-ott` header. Start the sign-in with a top-level navigation to the new `GET {basePath}/oauth-handoff/start?provider=<id>&callbackURL=<url>` (optional `errorCallbackURL`): it sets the OAuth state cookie first-party on the API's origin and redirects to the provider, so the callback works when the browser blocks third-party cookies. It answers 400 when `Sec-Fetch-Dest` is set and is not `document`, so only a page navigation can start a sign-in. It works for social and generic OAuth providers, and answers 400 with no redirect unless `callbackURL` is one of `callbackPaths` on one of `webOrigins`, `errorCallbackURL` (optional) is on one of `webOrigins`, and `trustedOrigins` allows both. With the default `["/"]`, any other return page is refused. An unknown provider or one that cannot start sends the page back with `error=provider_not_found` or `error=sign_in_failed`. Terreno's opt-in `rateLimit` puts `{basePath}/oauth-handoff/*` and `{basePath}/one-time-token/*` in the auth bucket. Unset by default, which adds no `/one-time-token` or `/oauth-handoff` routes.
542
+ - `signInSocialWithHandoff({client, provider, callbackURL?, errorCallbackURL?})` starts a social or generic OAuth sign-in for a `createBetterAuthClient({webAuth: "bearer"})` web client. It stores a random nonce in `sessionStorage` and navigates the page to the server's `GET {basePath}/oauth-handoff/start` with `handoff=<nonce>` on the callback URL. It throws on native and in cookie mode.
543
+ - `completeBetterAuthOAuthHandoff({client})` finishes that sign-in on the return page. When the URL has `#ott=<token>` and a `handoff` equal to the stored nonce, it removes both from the URL with `history.replaceState`, then exchanges the token at `{basePath}/one-time-token/verify`, and bearer mode stores the `set-auth-token` that comes back. A token without a matching nonce is removed and never sent. Other query and fragment parts stay, and `?error=` is left for the app. It resolves `true` only when a token was stored. Await it at startup before session reads, other query handling, and Sentry. The server needs `oauthHandoff: {webOrigins, callbackPaths?}`. See [Social sign-in in bearer mode](docs/how-to/configure-better-auth.md#social-sign-in-in-bearer-mode).
544
+ - `createBetterAuthClient` takes `webAuth: "bearer"` for web apps whose frontend and API are on different sites. On web it stores the `set-auth-token` header from sign-in, sends `Authorization: Bearer` on every Better Auth call including `getSession`, and sends no cookies. `signOut()` clears the token before its request, and a successful `getSession` with no session clears it unless a newer token was stored meanwhile. The token lives in `localStorage` under `${storagePrefix}_bearer_token`, or in the `bearerStorage` you pass (for example `window.sessionStorage` on shared devices). The default stays `"cookie"`, and native is unchanged. The web origin must be in the server's Better Auth `trustedOrigins`. See [Frontend and API on different sites](docs/how-to/configure-better-auth.md#frontend-and-api-on-different-sites).
545
+ - `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` reads that stored token without a network call.
546
+ - `GPTChat` takes `showHistory` (default `true`). Pass `false` to leave out the history sidebar so the chat panel uses the full width on narrow and phone layouts.
547
+ - modelRouter actions can opt into MCP with `mcp: {name, description?}`. The action becomes a tool on `/mcp` that takes `{id, ...body}` (instance) or the body alone (collection), runs the action's own permission chain including RBAC `access`, and gets `req.isMCPRequest === true`. Actions also accept `unwrapResponse: true` to send the handler's return value as the REST body instead of `{data: …}`. See [modelRouter actions → MCP tools](docs/explanation/model-router-actions.md#mcp-tools).
548
+ - `TerrenoApp` `mcpOAuth: {enabled: true}` turns on an OAuth 2.1 authorization server for `/mcp`, so Claude.ai connectors and ChatGPT apps can connect to apps that sign in with Terreno JWTs. It is off by default. It serves RFC 9728 and RFC 8414 discovery, the authorization-code flow with PKCE S256 and an app-hosted consent screen, rotating refresh tokens with reuse detection, and revocation. Revoking a token family writes an `McpOAuthRevokedFamily` tombstone that `McpOAuthToken.issue` honors, so concurrent refreshes and code replays cannot leave a live token. Dynamic client registration (public clients, redirect hosts allowlisted) is a separate opt-in, `dynamicClientRegistration: true`; without it, the app creates `McpOAuthClient` documents itself. With `mcpOAuth` on, `/mcp` answers unauthenticated requests with a `401` `WWW-Authenticate` challenge. See [Enable MCP OAuth for a JWT app](docs/how-to/enable-mcp-oauth.md).
549
+ - `getCurrentRequestContext()?.credential` records how the caller authenticated: `"jwt"`, `"better-auth-session"`, `"mcp-service-token"`, or `"oauth"`. Permission functions and hooks can use it to keep broad access (such as an admin bypass) to the user's own session and away from delegated agent tokens. `extractAuthFromHeaders` returns the same `{user, credential}` pair for MCP mounts. See [Request credential](docs/reference/api.md#request-credential).
550
+ - `useTerrenoChat` takes a `tokenSource` option. Each chat request reads its `Authorization: Bearer` token from that source, else the one registered with `setTokenSource`, else the built-in JWT source, and sends no `Authorization` header when there is no token. JWT apps behave as before.
551
+ - `useMCPTools` takes a `tokenSource` option. The MCP transport reads its `Authorization: Bearer` token from that source, else the one registered with `setTokenSource`, else the built-in JWT source, before every MCP request instead of capturing one token per fetch. When the source has no token as a fetch starts, the hook connects without authentication, as JWT apps do today.
552
+ - `createOfflineMiddleware` takes a `tokenSource` option. With a non-JWT source (that option, else the one registered with `setTokenSource`), queued mutations are tagged with the source's `getUserId()` instead of `state.auth.userId`, so the middleware works without an `auth` slice. They replay only while that same user is signed in, with `Authorization: Bearer` from the source's `getToken()` (omitted when there is no token). The offline slice gains an optional `lastUserId` (with `setLastUserId` and `selectLastUserId`): the last user id the source confirmed, refreshed on the first action, on rehydrate, on `onAuthChange` and on reconnect. A session read that fails while offline therefore still tags new mutations, and when the `offline` slice is persisted with redux-persist, mutations made after an offline launch are tagged with the persisted user. Until a read in the running app confirms that persisted id, those mutations carry `userIdConfirmed: false` (new optional `QueuedMutation` field, with the new `resolveUnconfirmedMutations` and `dropUnconfirmedMutations` actions): a read that returns a user keeps them only if it is the same user and no auth change or source swap happened since, and any auth change drops them (even for the same user), so they survive only when the reconnect read confirms them first. syncdb's `betterAuthAdapter` fires `onAuthChange` on the session's first successful load, which after an offline launch happens once the network returns, so that event drops offline-launch writes the reconnect read has not confirmed yet. The one remaining risk needs the signed-in user to change while no running instance of the middleware observes it (for example in another browser tab sharing the persisted `offline` slice) between an unconfirmed write and the next launch's first read. On the token-source path, a sign-out reported by the source, `auth/logout`, or the Better Auth slice's `betterAuth/logout` clears the queue, conflicts and `lastUserId`, and discards writes still waiting on a user id read and any replay in progress. A reconnect with no readable user, or a user switch during a replay, keeps the remaining queue for a later reconnect, and a mutation still waiting on a user id read gets no user (so it never replays) if the user changes first. JWT apps behave as before.
553
+ - `useSocketConnection` no longer requires `getAuthToken`. Without it, the hook reads the token source: the new `tokenSource` option, else the one registered with `setTokenSource`, else the built-in JWT source. The handshake re-reads the token on every connect and reconnect, and when the source's `onAuthChange` reports a different token the hook reconnects with it (with no token it disconnects). When the server rejects the handshake token, the hook calls `refresh()` once and reconnects; a second rejection before a successful connect stops reconnecting and calls `onConnectError`. Passing `getAuthToken` keeps the previous behavior.
554
+ - `@terreno/rtk` reads request tokens from one registered token source. Call `setTokenSource(authProvider)` once at startup with the same syncdb `AuthProvider` the app builds for `createSyncDb` (for example `betterAuthAdapter(betterAuthClient)`). `getTokenSource()`, `jwtTokenSource`, and `isJwtTokenSource()` are also exported. Without a registered source, rtk keeps using the JWT tokens in storage, so JWT apps change nothing.
555
+ - `emptySplitApi` sends the registered source's token.
556
+ - `betterAuthAdapter` takes a `getBearerToken` option. When it returns a token, `getToken` uses it without calling `getSession()`; a null, empty or throwing getter falls back to `getSession()` as before. Pair it with `getBetterAuthBearerToken` from `@terreno/rtk` for web apps in Better Auth bearer mode. `getUserId`, `onAuthChange` and `refresh` are unchanged. See [createSyncDb configuration](docs/reference/syncdb.md#createsyncdb-configuration).
557
+ - `WhatsNewBanner` in `@terreno/ui` shows non-blocking release notes, such as "What's new
558
+ in 57.16.2", until the viewer opens or dismisses it, then hidden until a newer release. See
559
+ [Show release notes with WhatsNewBanner](docs/how-to/whats-new-banner.md).
560
+ - The example app's what's new banner lists the 10 most recent `CHANGELOG.md` releases.
561
+ `bun run whats-new:generate` writes them, `changelog:assemble` refreshes them on each
562
+ release, and CI fails through `bun run check:whats-new` when they are stale.
563
+ - `BannerButton` accepts an optional `testID`.
564
+
565
+ ### Changed
566
+
567
+ - `RealtimeApp` uses the Better Auth instance of a `BetterAuthApp` registered on the same `TerrenoApp`, so Better Auth socket sessions work without building a second instance and passing `betterAuth`. An explicit `betterAuth` option still wins. (#1576)
568
+ - An explicit `betterAuth` option without a `userModel` now gets the `RealtimeApp`'s (else the `TerrenoApp`'s), so its sockets resolve to the app user instead of the Better Auth id. (#1576)
569
+ - The Better Auth socket handshake creates the app `User` on first contact, as the REST session middleware does, and refuses disabled users. (#1576)
570
+ - A `RealtimeApp` registered without its own `userModel` uses the `TerrenoApp`'s, for JWT and Better Auth sockets alike. Socket authorization then reads the user from the database instead of the token's claims, and the revalidation sweep disconnects disabled or deleted users, as `TerrenoApp({realtime})` already did. (#1576)
571
+ - create-terreno-app: generated backends no longer build a second Better Auth instance for `RealtimeApp`; it uses the registered `BetterAuthApp`, so `BetterAuthConfig` `options` and `plugins` apply to sockets too. (#1594)
572
+ - create-terreno-app: generated frontends register the syncdb Better Auth adapter from `store/syncdb.ts` with `setTokenSource` before the store is built, and the generated SDK uses `@terreno/rtk`'s `emptySplitApi` (`openapi-config.ts` `apiFile: "@terreno/rtk"`). `store/betterAuthApi.ts` and its forked base query are no longer generated. Generated web clients still use Better Auth's session cookie. RTK requests now get rtk's retries, and a 401 that survives one session refresh signs the user out (`auth/logout`); the fork never did. To move an existing generated app over: export `authProvider` from `store/syncdb.ts`, call `setTokenSource(authProvider)` in `store/index.ts`, set `apiFile: "@terreno/rtk"` in `openapi-config.ts`, run `bun run sdk`, delete `store/betterAuthApi.ts`, and import `useGetVersionCheckQuery` from `@terreno/rtk` if the app used it.
573
+ - The example backend seeds its staff modal and patient banner announcements as drafts, so a
574
+ fresh database (local dev or a PR preview) no longer interrupts sign-in. Publish them from
575
+ **Admin → Announcements** to try them.
576
+ - `@terreno/feature-flags` evaluation diagnostics are off unless `FEATURE_FLAGS_DEBUG=true` (they were on unless set to `"false"`), are written at `debug` level instead of `info`, and no longer include the requesting user's value for a field rule.
577
+ - Lifecycle skills now own a PR after every push: Brew, Taste, the planning loop, taste-sweep, and the repository skills that push (fix-conflicts, respond-to-review, update-dependencies, release, track-upstream-expo, implement-ready-for-dev) keep fixing red CI and answer every review-bot comment, then escalate to the human as `BLOCKED` after three failed fix rounds instead of leaving the PR red. See `references/push-ownership.md`.
578
+
579
+ ### Fixed
580
+
581
+ - `BetterAuthApp` stores Better Auth users, accounts, sessions, and verifications in the mongoose connection's database (the `dbName` passed to `mongoose.connect`) instead of the connection string's default database. Apps that connect with a `dbName` different from the URI's database should copy the `user`, `account`, `session`, and `verification` collections into the `dbName` database when upgrading.
582
+ - A Better Auth session whose app `User` has `disabled: true` no longer authenticates on REST: `req.user` stays unset, so protected routes answer 401, matching JWT and MCP. Better Auth's own routes, such as sign-out, still work. (#1577)
583
+ - The `TerrenoApp` rate limiter resolves `.`/`..` path segments (including `%2e` forms) before choosing a bucket, so dot-segment variants of Better Auth sign-in, sign-up, and password-reset paths are charged to the auth bucket, and dot-segment paths no longer get the health/openapi/swagger skip.
584
+ - Request logging redacts credential fields under any key name and at any depth in request bodies and query strings (for example `currentPassword`, `idToken.token`, `client_secret`, `code_verifier`, and OAuth `code`), not only five top-level keys. `TerrenoApp` now honors `loggingOptions.logRequests: false`, which was previously ignored.
585
+ - Better Auth apps are no longer logged out on their first RTK Query request (#1217). With a registered non-JWT token source, `emptySplitApi` sends no `authorization` header when the token is null, and dispatches `auth/logout` only when a 401 survives one `refresh()` and retry.
586
+ - `betterAuthAdapter` no longer reports a failed session read as a sign-out. When `getSession()` rejects or returns a non-401 `{error}` envelope, `getUserId()` answers with the user id the server last confirmed instead of null. Before, going offline between a session-atom emission and the syncdb client's follow-up read cleared the client's current user, paused sync, and made `mutate()` throw "requires start() to have resolved an authenticated user" until the next sign-in.
587
+ - `betterAuthAdapter` keeps the newest confirmed user id. A session read that started before a sign-out or user switch and resolves after it no longer restores the previous user, and neither does a session-atom emission that repeats the cached session (a refetch start, or the value a new `onAuthChange` subscriber receives on subscribing). A later failed read therefore cannot revive a signed-out identity or report the wrong user.
588
+ - `betterAuthAdapter` treats a 401 session-atom emission as a sign-out, the same as a 401 from `getSession()`: it clears the confirmed user id and fires `onAuthChange`. Before, the subscribe path ignored it like a network failure and kept the previous user.
589
+
590
+ ## [57.16.2] - 2026-10-13
591
+
592
+ ### Fixed
593
+
594
+ - Light-mode primary button labels, icons, and loading indicators are white again,
595
+ including on disabled primary buttons. (#1567)
596
+
597
+ ## [57.16.1] - 2026-10-12
598
+
599
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.16.1.md`](mcp-server/src/docs/upgrades/57.16.1.md).
600
+
601
+ ### Added
602
+
603
+ - `terraform/modules/terreno-backend` provisions one Terreno backend on Cloud Run: Artifact Registry, a least-privilege runtime service account, empty Secret Manager secrets, and a websocket-safe service (session affinity, 3600s timeout, one minimum instance). Set `github_deploy` for a keyless GitHub Actions deployer. See [Deploy backend to Cloud Run](docs/how-to/deploy-backend-to-cloud-run.md#manage-with-terraform).
604
+
605
+ ### Changed
606
+
607
+ - `terraform/` now holds only reusable modules. Terreno's own GCP deployment (example backend, jobs worker, hosted MCP server) moved to `infra/flourish/`, which uses the shared modules through a symlink; CI stages a dereferenced copy for Infra Manager. No infrastructure changes: resource addresses are unchanged.
608
+
609
+ ### Fixed
610
+
611
+ - `TextField` and `TextArea` on web no longer drop a typo fix made within half a second. The
612
+ oscillation guard used to suppress any change that undid the previous one within 500ms, so
613
+ typing a character and backspacing it right away put the character back. Changes the user
614
+ types, deletes, pastes, drops or undoes (judged by the DOM input event's `inputType`) now
615
+ bypass the guard. Spellcheck replacements, IME composition and changes with no input event
616
+ stay guarded.
617
+
618
+ ## [57.15.0] - 2026-10-11
619
+
620
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.15.0.md`](mcp-server/src/docs/upgrades/57.15.0.md).
621
+
622
+ ### Changed
623
+
624
+ - SyncDB bootstraps and reconciles every stream through one `POST /sync/snapshot/batch` request
625
+ per round instead of one `GET /sync/snapshot` per stream. A reconnect with nothing new now costs
626
+ 2 requests however many collections the app syncs. The client sends the next round while it
627
+ writes the current one and writes rows in chunks of 500 so the UI keeps responding. Clients fall
628
+ back to per-stream paging against servers without the batch route. The web IndexedDB persister
629
+ no longer serializes the whole store once per transaction: it serializes once per write.
630
+
631
+ ## [57.14.0] - 2026-10-11
632
+
633
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.14.0.md`](mcp-server/src/docs/upgrades/57.14.0.md).
634
+
635
+ ### Changed
636
+
637
+ - `@terreno/syncdb` now pages snapshot streams side by side instead of one at a time, so cold
638
+ start no longer costs one sequential round trip per stream. Reconcile, subscribe catch-up,
639
+ `forceResync` and wipe re-bootstrap run up to `snapshotConcurrency` streams at once (new
640
+ `createSyncDb` option, default 6; `1` restores the old behavior). Each stream now runs one pass
641
+ at a time: a subscribe catch-up queues behind a pass that started before the room join, and a
642
+ force resync waits for the stream's in-flight pass before purging it, so a concurrent pass can
643
+ no longer mark a purged stream bootstrapped over rows it never paged. A new how-to,
644
+ [Use syncdb correctly](docs/how-to/use-syncdb-correctly.md), lists the library behavior
645
+ apps have to handle.
646
+
647
+ ## [57.13.0] - 2026-10-10
648
+
649
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.13.0.md`](mcp-server/src/docs/upgrades/57.13.0.md).
650
+
651
+ ### Added
652
+
653
+ - Agent UI rich replies: a block document can now hold a `stepper` (− value + that calls a host action and gets the replacement stepper back), a `checklist` (ticks with an "n of m" counter, local or sent to a host action), a `gallery` (2–6 photos), a `list` (titled rows with portrait thumbnails), a card `eyebrow`, and a `copy` button action that writes a block's text to the clipboard on the device. See [Add a stepper callback](docs/how-to/agent-ui-blocks.md#add-a-stepper-callback), [UI blocks](docs/reference/blocks.md), and [Agent UI blocks](docs/explanation/agent-ui-blocks.md).
654
+ - `@terreno/blocks` exports the stepper, checklist, gallery, list, and copy action types, `blockPlainText`, `stepperElementIds`, `checklistElementId`, `COPY_TARGET_TYPES`, and the error codes `OUT_OF_RANGE` and `COPY_TARGET_INVALID`. `blocksPromptSection` takes `richBlocks`, `stepperActions`, and `checklistActions`; `validateBlocks` takes the two action lists. The Sunday roast golden document is `blocks/src/fixtures/golden/sunday-roast.yaml`.
655
+ - `@terreno/ai` adds the opt-in host actions `scaleStepperHostAction` and `toggleChecklistHostAction`, `findAgentBlock` for reading the block the agent wrote from the stored history, and `runBufferedChatTurn` for one chat turn without HTTP. A host action can set `handles: "stepper" | "checklist"` and `logResponse: false` (the returned block stays out of the `ui_action` log). `uiBlocks.richBlocks` controls which blocks the prompt offers.
656
+ - `@terreno/ui` `BlocksView` renders the new blocks with Terreno components. `DataTable` has right-aligned `number` and Luxon `date` cells, and the `table` block uses them. `MarkdownView` tables get theme borders, a bold header, and sideways scroll. `Box` takes `accessibilityState` and `accessibilityLiveRegion`. `Image` uses `alt` as its accessible label.
657
+ - The example app generates a stored photo library once (`bun run photos:generate`, Vertex Imagen), gives the agent a `findPhotos` tool, serves `GET /photoLibrary/:id/url`, and registers `scaleStepper` and `toggleChecklist`. `bun run blocks:smoke` checks that a real model writes the roast reply, and skips without a model key.
658
+ - `@terreno/api` exports `createRouteRateLimitMiddleware({name, max, windowMs, store?, keyBy?})`, a fixed-window limit for one route that works without `TerrenoApp` `rateLimit`. See [Rate limiting](docs/how-to/rate-limiting.md). (#1499)
659
+ - `@terreno/admin-backend` `DocumentStorageApp` takes `access: "authenticated"` to open document storage to every signed-in user. Non-admins only see their own `users/<userId>/` folder under `folderPrefix`; admins keep the whole prefix. `uploadRateLimit` limits uploads per client IP (or `keyBy`). A `folderPrefix` without a trailing slash gets one, and a missing bucket lists as empty. (#1499)
660
+ - `@terreno/admin-frontend` `DocumentStorageBrowser` takes `backButton` and shows upload errors inline, including a 429 "Upload limit reached" message. (#1499)
661
+ - `@terreno/ui` `SegmentedControl` takes `size="sm"` (28px, sized to its labels). (#1499)
662
+
663
+ ### Changed
664
+
665
+ - The admin Roles screen shows one row per resource, with that resource's actions as badges, instead of one badge per `resource:action`. Custom permissions list before model permissions. Badges are green for read actions, amber for writes, and red for destructive actions (`delete`, `destroy`, `remove`). Saved role data is unchanged. (#1540)
666
+ - Behaviour change: hosts with `uiBlocks` on get the new rich blocks (`checklist`, `gallery`, `list`, card `eyebrow`, and `copy`, plus `stepper` when a host action has `handles: "stepper"`) in the chat system prompt by default. A native build released before them shows an error banner for a reply that uses them. Set `uiBlocks.richBlocks: false` to keep the old catalog until those builds update; the prompt is then unchanged from before, even with a stepper action registered. Validation accepts the new blocks either way. See [Keep the old catalog for shipped native builds](docs/how-to/agent-ui-blocks.md#keep-the-old-catalog-for-shipped-native-builds).
667
+ - `DataTable` columns with `columnType: "number"` now right-align their cells and header, and `columnType: "date"` cells format an ISO string or `Date` as Luxon `DATE_MED` (date only, local zone). Values that are already formatted strings show as before. Use `customColumnComponentMap` to keep the old look.
668
+ - `Image` with `alt` now sets it as the accessible label and makes the image its own screen-reader element.
669
+ - `table` blocks show column names with underscores as spaces, size to their rows, and show at most 10 rows before the body scrolls.
670
+ - The `neutral500` theme primitive is now `#949494` (was `#9A9A9A`), so extra-light text meets contrast guidelines. Disabled surfaces, dark borders, the away status, and new `create-terreno-app` projects pick up the new gray. Apps that set their own `neutral500` with `setPrimitives` are unaffected. (#1544)
671
+
672
+ ### Fixed
673
+
674
+ - On web, an open toast no longer blocks clicks on the rest of the page at the same height.
675
+ The toast's `SafeAreaView` and the `Toast` layout wrapper span the full width (up to 900px)
676
+ around the visible toast and swallowed clicks there. Both now use `pointerEvents: "box-none"`,
677
+ so only the visible toast is interactive.
678
+ - `Tooltip` no longer shows up in the top-left corner and blocks the screen when its trigger measures as zero-sized or off-screen. When the trigger measurement is invalid or the ref cannot be measured, the tooltip hides and logs `console.error`. Measurements left over from an earlier show are ignored. (#1490)
679
+ - `Tooltip` on web no longer mounts a full-screen layer over the app while it is open.
680
+ The bubble now renders into `document.body` with `position: fixed` instead of through
681
+ the `PortalHost` layer, so nothing covers the page and the content below stays
682
+ clickable. Web tooltips also use `zIndex: 9999`, matching the body-level overlays
683
+ (`Modal`, `DropdownPanel`), so tooltips inside a modal show on top of it. Native keeps
684
+ the `PortalHost` path.
685
+
686
+ ## [57.12.0] - 2026-10-07
687
+
688
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.12.0.md`](mcp-server/src/docs/upgrades/57.12.0.md).
689
+
690
+ ### Added
691
+
692
+ - `@terreno/ai` harness: `harness.createTask(def, input, {trace: {scope, tags}, prompts})` writes a scope (tenant, workspace), tags, and prompt versions onto the task's `ObsTrace`; child tasks and subagents share it. `ObsTrace` gains `scope` (indexed with `created`) and `tags`, and the admin trace list filters by `scope`. `rt.runAgent` takes `prompts` and accepts a registry `PromptVersionRef` as `instructions` (its `body` runs, its version is recorded). New subpath exports replace `dist/` deep imports: `@terreno/ai/admin`, `@terreno/ai/harness/{agentLoop,commit,events,internalRuntime}`, and `@terreno/ai/observability/{observabilityApp,localPlugin,promptStore}`. `getProjectModel()` returns the chat `Project` model, and `new AiApp({projects: false})` skips the `/gpt/projects` routes so an app can own a model named `Project`. See [AI harness reference](docs/reference/ai-harness.md#trace-scope-and-prompt-versions). Closes #1533.
693
+
694
+ ### Changed
695
+
696
+ - Harness task HTTP responses and the task OpenAPI schema omit `lease`. The fencing token stays on the server. Approve, reject, submit, and abort invalidate the same cache tags as the task, approval, and conversation lists (`harnesstasks`, `harnessapprovals`, `harnessconversations`).
697
+ - Importing `@terreno/ai` no longer registers the mongoose `Project` model. The `Project` export is a lazy proxy and is deprecated: the first use of `Project` or `getProjectModel()` registers the model. Statics, `new Project(...)`, and `instanceof Project` still work. The proxy is not the model (`Project !== mongoose.models.Project`, `Object.keys(Project)` is empty, and reading `Project.prototype` throws). Use `getProjectModel()`.
698
+
699
+ ### Fixed
700
+
701
+ - Web `SplitPage` desktop child columns scroll vertically again. The corner radius and clip sit on the pane, so `overflow: hidden` no longer blocks the column `ScrollView`. A child with `height: "100%"` stays within the visible pane and can scroll internally. Narrow web pager pages use the measured pane height, so that same full-height child can scroll internally while the page still clips its rounded corners. Labeled pager previous and next controls sit in fixed slots in a row under the column, and the column uses the height left after that row and its spacing. `desktopChildrenMinWidth` horizontal scrolling, the list column, and native `SplitPage` are unchanged.
702
+
703
+ ## [57.11.0] - 2026-10-05
704
+
705
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.11.0.md`](mcp-server/src/docs/upgrades/57.11.0.md).
706
+
707
+ ### Added
708
+
709
+ - `@terreno/ai/harness/jobsRunner`: `JobsRunner` runs durable harness phases as `@terreno/jobs` jobs (`terreno.harness.phase`) on every instance, for multi-instance deploys such as Cloud Run. The task lease decides who commits, so duplicate or late jobs are harmless. `@terreno/jobs` is an optional peer; importing `@terreno/ai` never loads it. Adds `HarnessTask.claims` (default 0, no migration). See [Run the harness on multiple instances](docs/how-to/run-harness-on-multiple-instances.md).
710
+ - `@terreno/api` can manage one Slack workspace with `SLACK_BOT_TOKEN`: `createSlackPrivateChannel` creates a private channel, invites member ids, and posts an optional first message; `inviteSlackUsersToChannel` adds staff to a channel the bot already belongs to; `postSlackMessage` posts as the bot; `findSlackChannelByName` resolves a visible channel name to its id.
711
+ - `@terreno/syncdb` query windows: `queryCollections` config, `client.queryWindows`, and the `useWindowQuery` hook (plus generated `use{Collection}Window` hooks) run server-filtered, paged list queries (`where`, `sort`, `pageSize`, `fetchNextPage`, `refetch`, `hasMore`, `total`) over a shared, deduplicated entity table. Overlapping windows store each row once and update it once; each window lists only its own members. `fullSync: true` adds windows to a collection that still snapshot-syncs.
712
+ - `useQuery` / `useEntityIds` accept the same Mongo-style `where`, string sort specs, and `limit` for local queries.
713
+ - Window refetches learn the route's `maxLimit` and pipeline page requests with a `console.warn` instead of silently truncating.
714
+ - `modelRouter` list requests that filter on a field missing from `queryFields` now return a 400 naming the field, model, and allowed fields (`meta.allowedQueryFields`), checked before query validation so the filter can never be silently stripped. syncdb raises it as `QueryFieldNotAllowedError`.
715
+ - `terreno-prd` interviews the app owner and writes a product PRD with measurable success metrics before Grow. Invoke `/terreno-prd` in Cursor, `/terreno:prd` in Claude Code, or `$terreno-prd` in Codex.
716
+
717
+ ### Changed
718
+
719
+ - `@terreno/ai/harness`: `InProcessRunner` now runs up to 8 claimed tasks at once (new `concurrency` option, default 8), each under its own task lease, and `stop()` waits for all of them. Expect parallel model calls (watch provider rate limits and cost) and more stream-counter transaction retries. Turns of one conversation stay serial. Pass `new InProcessRunner({concurrency: 1})` to keep the previous one-at-a-time behavior. See [AI harness reference → InProcessRunner](docs/reference/ai-harness.md#inprocessrunner).
720
+
721
+ ### Fixed
722
+
723
+ - Outline `Button`s match solid buttons in width and height. The border width is subtracted from horizontal padding as well as vertical, at the default and `sm` sizes.
724
+ - `@terreno/ui` date and table components import `FontAwesome6` from `@expo/vector-icons/FontAwesome6`, so the named export resolves on iOS. The public component API is unchanged.
725
+ - Web `SplitPage` navigation IconButtons use the default variant, and detail child columns get a default `md` corner radius with `overflow: hidden`. Pass `childColumnRounding` to override that radius. The list column is unchanged. Native `SplitPage` is unchanged.
726
+
727
+ ## [57.10.1] - 2026-10-04
728
+
729
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.10.1.md`](mcp-server/src/docs/upgrades/57.10.1.md).
730
+
731
+ ### Breaking
732
+
733
+ - Security: `GET /files/*gcsKey` (`addFileRoutes`, `AiApp` with `gcsBucket`) now requires authentication and returns a signed URL only for the caller's own upload. Before, anyone could get a signed URL for any stored key, and keys are guessable (`uploads/<userId>/<ms>-<name>`). Another user's file, including for an admin, returns 404 like a missing file, so keys cannot be probed. Send the user's session token with the request; an unauthenticated request now returns 401.
734
+
735
+ ### Added
736
+
737
+ - Agent UI Asks: the chat agent can pause a turn and ask the user a typed question, then continue with the exact answer. Turn it on with `asks: true` (or `{kinds, approvals, maxFileSizeBytes}`) on `AiApp` or `addGptRoutes`. Asks are off by default: with `asks` unset, tools, the system prompt, SSE events, and endpoints are unchanged. See `docs/how-to/agent-ui-asks.md`, `docs/reference/agent-ui-asks.md`, and `docs/explanation/agent-ui-asks.md`.
738
+ - New package `@terreno/blocks`: the shared Zod contracts for asks (input and answer schemas, `validateAskInput`, `validateAskResponse`, stable error codes, `ASK_LIMITS`), simple cards (`toSimpleCard`, `resolveButtonAnswer`), the compact surface, the headless endpoint bodies, and JSON Schema documents plus fixtures (`@terreno/blocks/schemas/*`, `@terreno/blocks/fixtures/*`) for native clients such as a Swift watch app.
739
+ - Five ask kinds, each a client-side `ask_<kind>` tool: `choice` (select one, or select many with an optional Other text answer), `confirm`, `markdown` (edit a draft), `form` (a few typed fields, one submit), and `files` (upload images or documents the model reads in the same turn, as data URLs or `fileId`s from `POST /files/upload`).
740
+ - Server-enforced approval: with asks on, a host tool marked `needsApproval: true` runs only after the user approves a server-made `confirm` ask. `asks.approvals` sets that ask's text per tool name. With asks off, such a tool never runs.
741
+ - `/gpt/prompt` accepts `askResponse` and `surface: "full" | "compact"`, and streams `{ask}` and `{askResolved}` events plus `pendingAsk` on `{done}`. `GptHistory` stores the pending ask. With `chat` options that turn asks on, `addGptHistoryRoutes` (and `AiApp`) adds `GET /gpt/histories/pendingAsks` and `POST /gpt/histories/:id/turn`, which run a turn to completion and return JSON for clients that do not read server-sent events.
742
+ - `@terreno/ui`: `GPTChat` shows asks as an `AskCard` in the transcript (`GPTChatMessage.ask`, `onAskSubmit`, `askErrors`, `resolveAskFiles`). New `AskCard` and `SimpleAskCard` components. `Button` has a new `wrapText` prop that wraps a long label onto centered lines instead of overflowing its container.
743
+ - The example app's AI screen answers asks end to end, and the example backend runs a scripted keyless demo agent (`terreno-demo-agent`) when no model is configured, so asks can be tried without an API key.
744
+ - Agent UI blocks: a chat reply can be one YAML document of headings, metrics, charts, tables, and actions. Turn it on with `uiBlocks` on `addGptRoutes`. The model is told its whole reply is that document. After the text, the route checks it and sends `{blocks}` before `{done}`. `repair: true` runs one repair call. See `docs/how-to/agent-ui-blocks.md`, `docs/reference/blocks.md`, and `docs/explanation/agent-ui-blocks.md`.
745
+ - `@terreno/blocks` adds `parseBlocks`, `validateBlocks`, `blocksJsonSchema`, and `terreno-blocks validate`. Charts and tables read inline rows or a `ref` dataset. `AIService.generateBlocks` returns one validated document at temperature 0.
746
+ - `@terreno/ai` stores chart rows on `AIDataset` via `registerAiDataset`. `GET /gpt/datasets/:id` is owner-only and can bucket, downsample, or paginate them. `POST /gpt/actions` runs a named host callback after an owner check and a payload check.
747
+ - `@terreno/ui` `BlocksView` paints a document. `GPTChat` `uiBlocks` renders assistant messages through that view, including a spinner while the reply is still streaming.
748
+ - The example backend registers `exportDataset` and a `todoStats` tool that stores open and completed todo counts. The example AI tab loads `ref` charts and posts callbacks.
749
+ - `html` blocks are opt-in (`uiBlocks.html` on the server, `allowHtml` on `GPTChat` or `BlocksView`). The server strips scripts, event handlers, forms, frames, and links, then stores the cleaned document. The client draws it in a sandboxed frame only after the reply finishes.
750
+ - `callout`, `image`, and `details` render as a banner, an image, and an accordion. An `https` image loads only when its host is in `uiBlocks.imageHosts`.
751
+ - A reply that omits an actions block `id` gets one before it is stored. A block document written in the same step as a tool call is kept when the turn would otherwise have no text. The chat receives the document after that fill, repair, and HTML sanitizing, so an invalid draft is not shown. `{replace: "text"}` is sent only when text was already streamed and then changed. A validation banner names the field path once the reply has finished.
752
+ - Example-backend registers `accessControl: access` on `ObservabilityApp` and seeds optional `aiObservabilityViewer` and `aiObservabilityOperator` RBAC roles that demonstrate read-only and full observability access without auto-assigning them to users.
753
+ - `@terreno/ai` ships local prompt versions, nested traces, evaluators, datasets, experiments, and a review queue through `ObservabilityApp`, with the admin screens in `@terreno/admin-frontend`. Prompts, datasets, and experiments have one writer. Traces and scores fan out to every registered sink (local Mongo, Langfuse, or OTLP). Six RBAC resources cover read-only, reviewer, and operator access. See `docs/how-to/observe-llm-calls.md`.
754
+ - `@terreno/ai/harness` runs phased tasks on Mongo that survive a process crash. Each checkpoint and its trace span commit in one transaction. Tasks can wait for a person, retry, abort from the bottom up, and stay pinned to the definition version they started with. `defineTask`, `defineAgent`, and `defineTool` describe the work; approvals have their own permissions and an admin inbox. See `docs/how-to/build-a-durable-workflow.md`.
755
+
756
+ ### Fixed
757
+
758
+ - `GET` and `DELETE /files/*gcsKey` find uploads by their full key. Express 5 passes the wildcard as path segments, so a key with slashes, such as the `uploads/<userId>/<ms>-<name>` key every upload gets, returned 404.
759
+
760
+ ## [57.9.0] - 2026-10-03
761
+
762
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.9.0.md`](mcp-server/src/docs/upgrades/57.9.0.md).
763
+
764
+ ### Added
765
+
766
+ - `@terreno/ui` can compose operations dashboards with scorecards, comparison sparklines, multi-series line and area charts, donut center labels, previous-period bar overlays, chart-card headers, and spanning dashboard items.
767
+ - File uploads can be turned off with the `file-uploads` feature flag. `POST /files/upload`, document storage uploads, and chat attachments return 403 when the flag is off. The example app hides the upload controls. A missing flag leaves uploads enabled.
768
+ - `/gpt/prompt` rejects client-only attachment URLs (`blob:`, `file:`, `content:`, `ph:`) with `400` before streaming. With `fileStorageService` configured, `data:` attachments are uploaded and history stores the storage `url` plus `gcsKey`; later turns send the model signed URLs.
769
+ - `/gpt/prompt` saves the turn before streaming, sends a first `{historyId, started: true, streamId}` event, and persists partial reply text about every second with `status: "streaming"`. The finished reply is saved with `status: "complete"`; a failed one keeps its partial text with `status: "error"`.
770
+ - New `GET /gpt/histories/:id/stream` re-attaches to an in-flight reply after a reload or remount (`offset` skips text the client already shows). The example app reopens the last chat after a reload and resumes its live reply.
771
+ - `store-assets` agent skill for Expo apps. It keeps a simulator/emulator dev client in sync with the native fingerprint (downloading a matching EAS build or starting one), captures App Store and Google Play screenshots on store-sized devices with Maestro flows that log in through the `@terreno/ui` LoginScreen, and renders the Play feature graphic and 512×512 icon from a repo's `storeAssets.config.json`. Install with `npx skills add TerrenoLabs/terreno --skill store-assets` or from the `terreno` Claude plugin.
772
+
773
+ ### Changed
774
+
775
+ - `GPTChat` history rows move rename and delete into a three-dot overflow menu. Rename uses an outlined pencil in the dark secondary color; titles truncate before the menu trigger at any sidebar width.
776
+ - `GPTChat` shows "Scroll to bottom" only when content sits below the viewport, never in an empty chat.
777
+ - The `GPTChat` composer grows with long text up to 200px, then scrolls. `TextField` / `TextArea` accept `maxHeight` to cap `grow`.
778
+ - Generated images in `GPTChat` offer copy-image and (web) download. Copying an image-only reply copies the image instead of text.
779
+ - `FilePickerButton` opens an anchored dropdown instead of a modal. On web, **Document** now opens the file picker, and both options return `data:` URLs instead of `blob:` URLs.
780
+
781
+ ### Fixed
782
+
783
+ - Web toasts now render above open modals and remain correctly centered in statically exported apps instead of inheriting a zero-width server-rendered viewport.
784
+
785
+ ## [57.8.0] - 2026-10-02
786
+
787
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.8.0.md`](mcp-server/src/docs/upgrades/57.8.0.md).
788
+
789
+ ### Added
790
+
791
+ - `@terreno/announcements` imports a versioned release pack (`pack.yaml` plus one Markdown file per announcement) through idempotent `POST /announcements/import-release`. Imports default to drafts; pass `publish: true` to publish on import. Re-importing the same product, release version, channel, and slug updates the existing row, and a matching soft-deleted row is restored. Admin auth or `ANNOUNCEMENTS_UPLOAD_TOKEN` can call the route. Request bodies for the import and for impression and click events are strict Zod schemas.
792
+ - `SplitPage` accepts opt-in web layout props. `desktopChildrenMinWidth` keeps one or two desktop children at least that many pixels wide and scrolls horizontally when the row is tighter. `narrowViewportChildLabels`, `narrowViewportSelectionActive`, `narrowViewportSelectionKey`, and `narrowViewportListButtonLabel` replace the dotted narrow swiper with a labeled pager. `narrowBelowWidth` uses the narrow layout at or below that pixel width; omit it to keep `isNarrowViewport()`. Native `SplitPage` ignores these props. `IconButton` accepts `backgroundOpacity` to tint only the button background.
793
+
794
+ ### Changed
795
+
796
+ - CircleCI `publish-release` compiles every lockstep package once and publishes them in parallel instead of recompiling and retesting each package in turn. Tagged commits already passed CI on master. Releases drop from 25+ minutes to about 4.
797
+ - `deploy-demo` runs alongside `publish-release` instead of after it.
798
+ - `scripts/ci/netlify-deploy.sh` passes `--no-build`, so Netlify deploys no longer rerun the root `netlify.toml` docs build (about 3.5 minutes per demo and frontend deploy).
799
+
800
+ ### Deprecated
801
+
802
+ - `isMobileDevice` is deprecated. Call `isNarrowViewport` instead. The old function stays exported and returns the same result until it is removed.
803
+
804
+ ### Fixed
805
+
806
+ - `DropdownPanel` now anchors to its trigger rather than to the row around it. The measured wrapper stretched to fill its container, so the panel opened at the start of that row — visible whenever the trigger is not at the start edge, such as a right-aligned button or a right-to-left layout, where the panel appeared on the opposite side of the screen.
807
+ - `/gpt/prompt` sends each generated image once. Image-output models no longer duplicate the SSE `image` event and saved content part, and image-only responses save empty text instead of the `(image)` placeholder. `GptHistory` prompt `text` is now required only when the prompt has no `content` parts.
808
+
809
+ ## [57.7.0] - 2026-09-28
810
+
811
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.7.0.md`](mcp-server/src/docs/upgrades/57.7.0.md).
812
+
813
+ ### Changed
814
+
815
+ - CircleCI is now the only continuous deployer for the Netlify demo, example frontend, and docs sites and for GCP terraform, Cloud Run backend/tasks, and MCP. The GitHub Actions deploy workflows are disabled for rollback only, and CircleCI deploy jobs fail instead of skipping when `terreno-netlify` or `terreno-gcp` is missing a value. CircleCI deploys record GitHub Deployments (same environment names as before, plus `docs` and `mcp-production`) when the `terreno-github-deployments` context holds `GITHUB_DEPLOYMENTS_TOKEN`.
816
+ - `@terreno/ui`'s `Filter` is now `DropdownPanel`, reflecting that it is a general compositional dropdown (trigger + anchored panel + optional Apply/Clear/Cancel footer), not a filter-only control. `Filter` and `FilterProps` stay exported as deprecated aliases until Terreno 58; the `FilterSelectMenu` / `FilterBoolean` / `FilterAccordion` / `FilterChangesBadge` controls keep their names.
817
+ - `DropdownPanel` no longer runs off screen. The panel right-aligns to its trigger when a left-aligned panel would cross the right viewport edge, clamps to an 8px screen margin, flips above the trigger when there is no usable room below, and scrolls its body (footer pinned) when the content is taller than the space available. `align` and `maxPanelHeight` override the automatic behavior, and `computeDropdownPanelLayout` is exported for callers that position their own panels.
818
+ - `DropdownPanel` renders through the `TerrenoProvider` portal host on native, so the panel is no longer clipped inside scroll views or cards. Without a host it falls back to the previous inline overlay.
819
+ - `DropdownPanel` triggers are now styleable: `triggerVariant` (any `Button` variant), `applyButtonVariant`, `triggerSize` on labeled triggers, `fullWidth`, and `renderTrigger` for a fully custom trigger.
820
+ - `bun run prepush` now runs the local mirror of every CI job the branch triggers, using the CircleCI path-filter mapping. That covers package `test:coverage` plus the new-file LCOV gate, `ui` types, example-backend and example-frontend tests, every `repo-policies` step, rulesync drift, and the typedoc API reference. It runs every step even after a failure and prints a summary. Use `--dry-run` to see the plan and `--all` to run everything.
821
+ - New `bun run check:test-isolation` blocks new `mock.module` calls in shared Bun test suites (see `docs/explanation/test-isolation.md`).
822
+ - The lifecycle plugin now makes one commit per task after Roast `PASS` with no bookkeeping commits, keeps pushes out of Pick and Roast, has Pick list edge cases before coding, has Roast run the CI-equivalent package gate, and has Taste merge `master` only when the PR conflicts, a failure traces to base drift, or the branch is merge-ready.
823
+ - Taste invoked directly by a human now repeats bounded reactions (at most 3 fix pushes and 3 hours of waiting) until `PASS`, `BLOCKED`, or `FAIL` instead of returning `PENDING`, and a directly invoked Brew starts Taste as its next stage.
824
+
825
+ ## [57.6.1] - 2026-09-25
826
+
827
+ ### Fixed
828
+
829
+ - `modelRouter` array subroutes now return a structured `400` error when the requested field is not an array, after preserving consumer update authorization hooks, instead of throwing an iterable `TypeError`.
830
+
831
+ ## [57.6.0] - 2026-09-24
832
+
833
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.6.0.md`](mcp-server/src/docs/upgrades/57.6.0.md).
834
+
835
+ ### Added
836
+
837
+ - `sendToSlack` accepts `mentionUserIds` (Slack member IDs) so incoming webhooks can @-mention people. Names and emails in the message text do not notify anyone. New helpers: `formatSlackUserMention`, `normalizeSlackUserId`, and `lookupSlackUserIdByEmail` (optional `SLACK_BOT_TOKEN` with `users:read.email` to resolve an id from email and store it on the staff/user record).
838
+ - Add full SyncDB support to `terreno-mcp-local`: inspect debugger and local-store
839
+ state, capture/compare/merge snapshots, mutate or directly edit local entities,
840
+ flush the outbox, reconcile/resync, resolve conflicts, retry failures, and
841
+ exercise offline transitions. State-changing operations require
842
+ `TERRENO_MCP_EVAL=1`; returned state redacts sensitive fields.
843
+ - Add `@terreno/cli` (`terreno`) for docs search, codegen (syncdb, RTK SDK, models, routes, screens, forms, admin), bootstrap, unified backend/browser/Metro/app logs, Redux/RTK state inspection, opt-in CDP evaluation/navigation, Bun 1.4 WebView automation and screenshot proof, read-only database tools, OpenAPI `api list|call|request`, and `generate rest-cli` to scaffold an app CLI from a backend spec.
844
+
845
+ ### Changed
846
+
847
+ - Terreno moved to the `TerrenoLabs` GitHub organization. The repository is now `TerrenoLabs/terreno`, the roadmap board is `github.com/orgs/TerrenoLabs/projects/1`, and package `repository` URLs, docs links, and plugin/skill install commands (`/plugin marketplace add TerrenoLabs/terreno`, `npx skills add TerrenoLabs/terreno`) point at the new location. Old `FlourishHealth/terreno` URLs redirect.
848
+ - Every tsconfig builds on TypeScript 6 without `ignoreDeprecations`. Node packages (`@terreno/api`, `ai`, `admin-backend`, `announcements`, `api-health`, `comms`, `feature-flags`, `jobs`, `test`, and the `admin-spa` server plugin) now compile with `module: nodenext` to ES2023 CommonJS instead of ES5. Their dynamic `import()` stays a real ESM import, so `loadMigrations` can load migration files by `file://` URL from compiled `dist/`. Frontend libraries (`ui`, `admin-frontend`, `rtk`, `syncdb`) emit ES2022 with bundler resolution and ship declaration maps. `@terreno/test` no longer publishes its compiled Bun tests. `create-terreno-app` scaffolds drop `ignoreDeprecations` and `moduleResolution: node`, and the generated backend moves to TypeScript 6. See `docs/explanation/typescript-configuration.md`.
849
+
850
+ ### Fixed
851
+
852
+ - Framework Mongoose models added in 57.3 and 57.4 now use `Terreno*` model names
853
+ without changing their MongoDB collections, preventing collisions with consumer
854
+ models such as `Notification`, `Membership`, and `Job`. Admin UI keys and
855
+ `admin<ModelName>` RBAC resources keep the pre-namespace names. JWT recovery
856
+ routes can be disabled with `authOptions.passwordReset`, `emailVerification`,
857
+ and `legacyResetPasswordRoute`.
858
+
859
+ ## [57.4.0] - 2026-09-22
860
+
861
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.4.0.md`](mcp-server/src/docs/upgrades/57.4.0.md).
862
+
863
+ ### Added
864
+
865
+ - CircleCI Playwright now shards `admin-home`, `admin-form`, `admin-table-search-filter`, `admin-table-bulk-actions`, `admin-custom-screens`, and `admin-comms-back` alongside `admin`.
866
+ - CircleCI now runs `admin-backend-ci` and `admin-frontend-ci` (lint, compile, 95% coverage) when those packages change. Use `bun run admin-frontend:test` and `bun run admin-backend:test` from the repo root.
867
+ - `@terreno/admin-frontend` now ships `adminRequest`, a native `fetch` helper (timeout, JSON/`FormData`, credentials) for admin RPC that is leaving RTK `injectEndpoints`.
868
+ - Example admin Todos now allow admin delete, and Playwright covers Todo create → edit → delete on embedded `/admin` and SPA `/console`.
869
+ - Built-in admin String-`_id` model CRUD can now run windowed local-first through
870
+ `@terreno/syncdb`: REST supplies membership, TinyBase supplies live rows,
871
+ create/update/delete use the durable outbox, bulk patch stays server-side, and
872
+ `ConflictSheet` resolves admin-loaded records. The embedded example and
873
+ standalone admin SPA demonstrate Bearer and same-origin cookie hosts.
874
+ - `GET /admin/config` model meta includes `adminBroadcast` (always) and `syncCollection` when the app sync registry has `adminBroadcast: true`. `AdminProvider` accepts optional `syncDb`. `AdminModelTable` then uses REST membership + a live TinyBase overlay with a visible Refresh control; hosts that pass only `api` keep the RTK list. Refresh treats RTK `refetch` error envelopes as failures, drops in-flight Refresh when list params change, and toasts rejected `hydrateWindow` calls. Fetch-RPC mutations invalidate mounted comms, scripts, and configuration queries just like their RTK counterparts.
875
+ - `BetterAuthConfig.disableRateLimit` turns off Better Auth's built-in limiter on a throwaway in-process seed instance. Preview container seeds that create several users no longer 429 on the fourth signup when `NODE_ENV=production`.
876
+ - `@terreno/ui` now ships owned-SVG `LineChart`, `BarChart`, `AreaChart`, and `DonutChart`
877
+ plus an eager `DashboardGrid` for wrapping caller `Card`s. Charts use `react-native-svg`
878
+ and private d3 helpers — not `victory-native`. Each chart sizes itself to its container
879
+ and clips to it, so a chart never widens the card it sits in, and `height` covers the whole
880
+ chart including its tick and tooltip rows. `bun run ui:charts:compare` diffs actually
881
+ rendered gallery PNGs against `demo/rendered-snapshots/` (not JSON snapshots).
882
+ Conditional `DashboardGrid` children no longer reserve blank cells, and an open chart
883
+ tooltip now updates or closes when live data changes.
884
+ - `create-terreno-app` — unscoped npm CLI (`bunx create-terreno-app` /
885
+ `npm create terreno-app`) that scaffolds a deployable full-stack Terreno app.
886
+ Published in lockstep with `@terreno/*`; generated scaffolds pin
887
+ `^<create-terreno-app version>` on Terreno dependencies.
888
+ - **@terreno/ui `DataTable`**: optional server-side search and column filters emit
889
+ modelRouter-shaped query params via `onQueryChange` and `buildDataTableListQuery`
890
+ (web per-column `Filter` popovers; native Filters sheet).
891
+ - **@terreno/admin-frontend `AdminModelTable`**: adopts DataTable filter/search UI;
892
+ list search still uses `q`; choice filters support multi-value `$in`. Admin list
893
+ URLs use `qs` bracket serialization so nested filters work with any RTK base query.
894
+ - **@terreno/admin-backend**: `parseAdminListFilters` accepts choice `{$in: string[]}`
895
+ and escaped-literal text `{$regex, $options: "i"}` while rejecting extra operators.
896
+ Optional choice filters auto-enable an **Empty** option (`__empty__` wire sentinel → Mongo
897
+ `null`) when the Mongoose field is not required; explicit `allowEmpty` overrides.
898
+ Empty and Empty-plus-value filters remain normalized when combined with toolbar search.
899
+ - **@terreno/api**: OpenAPI query validation accepts choice scalars, `{$in: [...]}`, and
900
+ the empty sentinel alongside enum values. Nested query operators reject extra keys and
901
+ executable regex patterns; `queryFilter` can remove consumed wire keys with `undefined`.
902
+ - **@terreno/ui `buildDataTableListQuery`**: single concrete choice values emit scalar
903
+ equality (validator-friendly); empty-only and empty+concrete use `$in` with `__empty__`.
904
+ - **@terreno/ui `Filter`**: new `iconOnly` and `triggerSize` props render a compact
905
+ icon trigger for dense chrome; DataTable column headers use it.
906
+ - **@terreno/ui `DataTable`**: single-column filter popovers no longer duplicate the
907
+ popover's own Clear with a per-field **Clear filter**; the boolean per-field clear
908
+ now appears only where one surface hosts several filters.
909
+ - **@terreno/ui `DataTable`**: date range filters use date inputs, so entering a
910
+ calendar date filters immediately and either bound works on its own. Previously the
911
+ datetime input emitted nothing until an hour and minute were also entered. The **to**
912
+ bound now closes the chosen UTC day instead of landing on midnight, so rows recorded
913
+ later that day stay in range.
914
+ - Opt-in append-only `AuditEvent` log. Register `AuditApp` and set `audit: true` (or
915
+ `{redact: ["ssn"]}`) on `modelRouter` so successful HTTP CRUD and array mutations
916
+ persist redacted changed-field diffs. Default redaction includes compound keys such as
917
+ `tokenHash`. AdminApp writes the same collection when the plugin is registered and omits
918
+ each model's `hiddenFields` / `excludeFields`; RBAC uses
919
+ `persistRbacAuditToAuditEvent`. List/read is admin-only at `GET /audit-events` and
920
+ `/admin/audit-events`; create/update/delete over HTTP return 405. Persist is
921
+ fire-and-forget; a non-24-hex `actorId` (including 12-character strings mongoose would
922
+ accept) is dropped from the event instead of dropping the event. Update/delete snapshots
923
+ run only when `audit` is on, and a throwing `toJSON` does not fail the mutation. Set `GCP_TASKS_AUDIT_QUEUE` and `AUDIT_TASKS_URL` to enqueue Cloud Tasks
924
+ instead of writing Mongo on the request. Default retention is forever; `retentionDays`
925
+ adds a Mongo TTL index on `created`. See `docs/how-to/audit-log.md`.
926
+ - # GCP durable-jobs worker infrastructure
927
+
928
+ Infra Manager now owns the example Cloud Tasks queue `terreno-example-jobs-v2`, the
929
+ private `terreno-backend-example-tasks` Cloud Run service, the
930
+ `terreno-backend-runtime` API identity (sole queue enqueuer / jobs-invoker
931
+ `actAs`), and the `terreno-jobs-invoker` OIDC callback identity. CD deploys
932
+ matching PR tags for the API and tasks services. The example API still uses
933
+ `MongoJobRunner` until a follow-up selects `JOBS_RUNNER=gcp-cloud-tasks`. The
934
+ example image compiles `@terreno/jobs` before `@terreno/admin-backend`.
935
+ - `GPTChat` accepts an optional `mascot` React node from the consumer. Terreno does not
936
+ ship a default character. The node renders on an empty chat and hides once messages
937
+ exist. The empty state (mascot plus suggested prompts) stays centered in the chat
938
+ panel inside a scrollable message area, and the composer row centers the
939
+ attachment, tools, and Send controls vertically against the input. The example
940
+ consumer bundles four plant-robot mascots and chooses one on each AI screen mount.
941
+ - Unattended agents can claim one unstarted GitHub issue labeled
942
+ `status:ready-for-dev`, Pick ⇄ Roast a posted plan, and Brew a draft PR via the
943
+ `implement-ready-for-dev` skill. Pickup skips open linked PRs via GraphQL (not
944
+ `linked:<number>`) and aborts if an untrusted author edits the issue body after
945
+ the label. Dashboard paste lives in that skill's `references/cursor-automation.md`.
946
+ Interactive `/work-github-issues` still requires chat confirmation.
947
+ - `@terreno/jobs` adds durable background work for Terreno backends: MongoDB-persisted jobs
948
+ with retries, dead-lettering, cron schedules (IANA timezones), admin list/detail/retry/requeue/cancel,
949
+ and pluggable runners (`MongoJobRunner`, GCP Cloud Tasks, Vercel Queues, custom). Workers start
950
+ only via explicit `startWorker()` — register `JobsApp` on `TerrenoApp`, define handlers, enqueue
951
+ with `getJobsService()`, and choose in-process or standalone worker processes. Admin UI widgets
952
+ ship in `@terreno/admin-frontend`. See `docs/how-to/background-jobs.md` and
953
+ `docs/reference/jobs.md`.
954
+ - MongoDB migrations tooling: `terreno-migrate` (`check` / `generate` / `status` / `up` / `down`), `TerrenoApp` `migrations.runOnStart`, admin **Migrations** page, and `example-backend/migrations/`. Admin HTTP is `modelRouter` collection actions `GET /admin/migrations/status` and `POST /admin/migrations/run` (`{data: ...}`), documented in `/openapi.json` under the `adminMigrations` tag. Production wet apply requires `ALLOW_MIGRATIONS=true`. History lives in `terreno_migrations` and is not wiped by seed `--reset`.
955
+ - The in-app notification center adds `NotificationsApp` in `@terreno/api` with
956
+ owner-scoped `Notification` and `NotificationPreference`
957
+ models, `getNotificationService().notify()`, `notificationsBeforeSend`, mark-all-read, and
958
+ optional `retainDays` tombstone sweep. `@terreno/ui` provides presentational
959
+ `NotificationBell`, `NotificationInbox`, and `NotificationPreferences` components.
960
+ The example app demonstrates syncdb collections, a todos-header bell that toggles a
961
+ right-side drawer, a full active and archived history page, notification preferences,
962
+ seeded notification examples, todo activity notifications, and a development notify
963
+ route. Inbox list, mark-read, and dismiss (archive via `archivedAt`) use syncdb only;
964
+ `POST /notifications/dev-notify` remains the development send path. `terreno-syncdb-codegen` now accepts hyphenated collection names such as
965
+ `notification-preferences`. Consumers can replace the bell icon and unread bubble with
966
+ `NotificationBell` render props while retaining its layout and accessibility behavior.
967
+ - Optional organization management: `OrgsApp` adds `Organization` / `Membership`, org context, isolation filters, and membership-scoped `org-admin` plus operator RBAC. Existing single-tenant apps stay unchanged. Admin hosts get an operator directory, org switcher, settings, and members screens. Newly bootstrapped apps enable an org by default.
968
+ - `terreno-pick-roast-loop` drives an approved plan through all actionable Pick/Roast
969
+ cycles without stopping on ordinary engineering failures. It accumulates task, attempt,
970
+ verification, artifact, docs, and risk evidence into one final report. When a genuine
971
+ human decision is required, it first explains the overall plan state, completed work,
972
+ decisive evidence, options and impact, and its recommendation, then asks one exact
973
+ question. The loop excludes Grow, Brew, Taste, and product-CI monitoring. Cursor,
974
+ Codex, Claude Code, and `npx skills` expose the loop.
975
+ - `@terreno/announcements` ships `AnnouncementsApp` for admin-managed modal, banner, and feed product updates. Targeting covers staff, patients, and all users, plus custom `audience` segments. Acknowledgements, impressions, frequency caps, minimum-build gates, and CTA click tracking are first-class. Admin screens include a guided overview with aggregate and per-row analytics. Frontend surfaces are `AnnouncementNavigator`, `AnnouncementBanner`, and `AnnouncementScreen` in `@terreno/ui`. See `docs/how-to/product-announcements.md`.
976
+ - CircleCI `publish-release` and the GitHub `publish-on-tag.yml` fallback now publish `@terreno/announcements` in lockstep with the other `@terreno/*` packages.
977
+ - `SyncConfig.adminBroadcast` (default `false`) is stored on collection registration. When `true`, change-stream `sync:delta` emission also targets `{collection}|admin`. Admin clients join that stream with `sync:subscribe {mode: "window"}` (`createSyncDb({windowCollections})`) and skip `GET /sync/snapshot` paging for those collections. Window subscribe requires admin panel and model-list access. `hydrateWindow` renders REST rows immediately, then refreshes canonical seq/deleted metadata for every requested id through `GET /sync/entities` (unknown ids ignored). AdminApp list/read permissions and `queryFilter` apply to hydrate and live deltas. `{collection}|admin` deltas apply only to ids already in the local window; Refresh is REST + `hydrateWindow`.
978
+ - Admin-window sync writes use an explicit `mutationMode: "adminWindow"` marker on `POST /sync/mutate`, `POST /sync/mutate/batch`, `sync:mutate`, and `sync:mutateBatch`. `createSyncDb({windowCollections})` tags matching outbox rows automatically. The server validates the marker together with `adminBroadcast`, admin-window access, and a registered AdminApp write scope, then runs the shared sync executor with AdminApp pre/post hooks (not product `modelRouter` hooks), applying the same permission, stripping, User admin-flag/role gates, and audit semantics as REST. Product clients without the marker keep existing product sync permissions and hooks.
979
+ - `@terreno/syncdb` exports `bridgeBetterAuthReactClient`, which adapts a Better Auth
980
+ **react** client to the session-subscription surface `betterAuthAdapter` watches. Without
981
+ it the adapter cannot see the client's nanostore session atom and falls back to polling
982
+ `getSession()`. The example app and the admin SPA both use it.
983
+ - `TextField` with `type="password"` now renders a show/hide eye control so users can check what
984
+ they typed. The value starts masked, a disabled field cannot be revealed, and
985
+ `showVisibilityToggle={false}` removes the control. The toggle is reachable in tests at
986
+ `{testID}.visibility-toggle` (override with `testIDs.visibilityToggle`). `Field`, `LoginScreen`,
987
+ and `SignUpScreen` password fields inherit it.
988
+ - Dependency updates go through the daily `update-dependencies` skill: one trusted,
989
+ same-repository rolling PR with a landed/failed ledger, a test that imports each
990
+ bumped package, and a freeze on Expo fingerprint changes until a release.
991
+
992
+ ### Changed
993
+
994
+ - `AdminProvider` accepts host-injected `credentials` and `getAuthHeaders` for native `adminRequest`. The admin SPA uses cookie `same-origin` credentials; the example app sends a Bearer token.
995
+ - Admin RPC hooks dual-run: hosts that inject `credentials` / `getAuthHeaders` on `AdminProvider` use native `adminRequest`; hosts that pass only `api` keep RTK `injectEndpoints`.
996
+ - Admin panel HTTP script runs enqueue durable job `admin/script` when `JobsApp` is registered.
997
+ The Scripts UI still polls `BackgroundTask`. If runner enqueue throws, the HTTP handler
998
+ marks that `BackgroundTask` failed and returns 500. CLI script runs stay in-process. See
999
+ `docs/how-to/background-jobs.md`.
1000
+ - CircleCI starts fewer concurrent jobs: repository policies share one `repo-policies` check, Playwright runs five shards instead of one container per spec, and path filters no longer start example-backend / new-file-coverage for unrelated UI, RTK, or e2e-spec-only changes. `repo-policies` uses Node 22.14 for Knip. Require `repo-policies`; treat `e2e-*` shard names as path-filtered (config-only PRs post `e2e-auth`).
1001
+ - CircleCI uses smaller Docker classes for Playwright e2e shards, skips empty
1002
+ Netlify/GCP contexts before `bun install`, and turns off Docker Layer Caching.
1003
+ - The `terreno-planning` plugin now includes reusable Terreno backend/API, UI, data,
1004
+ schema, SDK, admin, prompt-governance, documentation, upgrade, deployment, and UI
1005
+ verification skills alongside the lifecycle. It also ships `pre-commit` and
1006
+ `ui-verifier` agents. The redundant `commit` and `create-pr` skills are removed in
1007
+ favor of Brew. Conflicting or unused Expo skills (`native-data-fetching`,
1008
+ `building-native-ui`, `expo-ui`, App Clip, brownfield, Observe, Tailwind setup,
1009
+ EAS update insights, and Expo module authoring) are no longer distributed. Taste's
1010
+ pre-push gate runs lint, typecheck, and affected tests for every supported host.
1011
+ Rulesync now also generates native stop hooks for Cursor, Claude Code, GitHub Copilot,
1012
+ and Devin that run repository lint and typecheck, emit each host's blocking JSON
1013
+ protocol, and avoid repeated checks on structured Stop-hook retries. Plugin
1014
+ `terreno-planning` is `2.8.0`.
1015
+ - CircleCI Playwright shards now run only when a changed file can reach them. `e2e-prepare` resolves the affected shards with `bun run check:e2e-affected` (import graph with barrel, lazy-registry, and lockfile resolution) and each shard halts before `bun install` when it is unaffected. The gate fails open on anything it cannot resolve. See [circleci.md](../../docs/how-to/circleci.md#e2e-affected-gate).
1016
+ - The deployed example backend selects `JOBS_RUNNER=gcp-cloud-tasks`, verifies Cloud
1017
+ Tasks OIDC on `POST /jobs/execute`, and points each PR preview at its matching
1018
+ tasks-service tag. GitHub Actions and CircleCI both deploy that tag, and production
1019
+ deploys overwrite env vars so preview `MONGO_DB_NAME` / `PR_NUMBER` cannot stick.
1020
+ The API
1021
+ process starts the schedule ticker; the tasks service does not. The compiled Cloud Run binary enqueues through the Cloud Tasks REST
1022
+ API (`google-auth-library`) because `@google-cloud/tasks` cannot load its JSON
1023
+ config from a `bun build --compile` image. Enqueued HTTP tasks set
1024
+ `dispatchDeadline` to protobuf `{seconds: 1800}` (REST JSON `"1800s"`) so Cloud
1025
+ Tasks does not retry a still-running 30-minute handler at the 10-minute HTTP
1026
+ default. The REST client encodes that Duration; the optional `@google-cloud/tasks`
1027
+ peer receives the protobuf object. Queue and worker service resources live in Infra
1028
+ Manager.
1029
+ - Grow now ends with a standalone approval brief instead of a 15-line index. It opens
1030
+ with a paragraph on where the repository is and where the change takes it, adds
1031
+ background on current state when a reviewer needs it, then lays out the idea and the
1032
+ plan (tasks, tracer, verification, out of scope, risks). The Decisions table follows
1033
+ the plan and now pairs every settled human decision with the question that prompted
1034
+ it, so a reviewer can see what was asked as well as what was chosen; it is still
1035
+ omitted when grilling settled none. A reviewer should be able to approve from the
1036
+ brief alone, without opening the IP, the ticket, or the interview history.
1037
+ - `implement-ready-for-dev` now Grow-shapes the claimed issue without a chat pause
1038
+ (assume answers unless a genuine human gate), then Pick ⇄ Roast, Brew, and Taste
1039
+ to a mergeable PR. Dashboard paste is in that skill's `references/cursor-automation.md`.
1040
+ There is no separate `autobot-ready-for-dev` skill.
1041
+ - `new-file-coverage` no longer reruns a package's full test suite when that package's CI already produced LCOV. The 90% new-file gate runs against the package report instead. Remaining reruns use colocated tests, compile `@terreno/*` dist deps only when imported, and coverage-script unit tests run in a separate `coverage-scripts` job.
1042
+ - Planning Brew titles are `[ticket] Short feature title`. The ticket uses the attached
1043
+ Linear id (`[FH-1632]`) or GitHub issue (`[#412]`); the rest names the feature only.
1044
+ Titles must not use `feat:` / `docs:` prefixes or lifecycle labels such as
1045
+ `IP Approved` and `Task list`.
1046
+
1047
+ PR bodies now preserve the IP's initial justification and a brief overview of its
1048
+ intended outcomes. Verification always includes reproducible testing instructions;
1049
+ Brew updates that section as testing changes while keeping the rationale and overview
1050
+ stable.
1051
+ - Planning stages and outer loops now close every wait-for-human or done chat with the
1052
+ current PR's GitHub Deployment demo URLs as the last visible section when those URLs
1053
+ exist. Plugin `terreno-planning` is `2.11.0`.
1054
+ - Removed the `terreno_bootstrap_app` MCP tool. The only supported scaffold path
1055
+ is `bunx create-terreno-app` / `npm create terreno-app`. MCP still offers
1056
+ `terreno_bootstrap_ai_rules` for editor rules after the CLI runs.
1057
+ - Roast no longer asks two unconstrained subagents to each rediscover the repository
1058
+ (full-branch diff plus skill catalog). Pick, Roast, and Brew pass a task-scoped
1059
+ briefing (`plugins/terreno-planning/references/subagent-briefing.md`): this task's
1060
+ criteria, file list, and patch. Roast may spawn at most one UI/runtime verifier when
1061
+ this task lists UI files, and must not spawn a conventions reviewer. Installable
1062
+ lifecycle skills copy only the plugin references they link.
1063
+ - Taste records failed tests from the last product-CI run and re-verifies those exact
1064
+ local commands before any GitHub push. `prepush` does not substitute for that
1065
+ re-verify. Example-frontend now exposes `test:ci` so root `bun run test` includes it.
1066
+ Cursor Cloud test mapping lives in `docs/how-to/run-tests-locally.md`. Plugin
1067
+ `terreno-planning` is `2.12.0`.
1068
+ - Taste now runs a repository root's `prepush` package script, when present, in its fresh
1069
+ no-context subagent before pushing. Repositories own the exact local gate; when the
1070
+ script is absent, Taste retains its affected-package lint, typecheck, and test fallback.
1071
+ Terreno's root `prepush` gate runs workspace lint, TypeScript compilation, and full
1072
+ Knip/dependency-cruiser static analysis. Plugin `terreno-planning` is `2.10.0`.
1073
+ - The component demo and documentation site now use distinct Terreno Garden app
1074
+ icons. The demo home screen also shows a Terreno Garden banner that flows inline
1075
+ with the component grid, filling the first two card slots.
1076
+
1077
+ ### Deprecated
1078
+
1079
+ - `@terreno/admin-frontend`'s `useAdminApi` RTK `injectEndpoints` path and required
1080
+ `api` prop are deprecated. Terreno 57 retains them for ObjectId/API-only
1081
+ compatibility; Terreno 58 removes them. New admin RPC uses the host-bound fetch
1082
+ client, and eligible String-`_id` model CRUD uses windowed syncdb.
1083
+
1084
+ ### Removed
1085
+
1086
+ - The UI component demo no longer includes announcement or in-app notification
1087
+ stories. Exercise those surfaces in `example-frontend` instead.
1088
+
1089
+ ### Fixed
1090
+
1091
+ - The `cloud_run_service` Terraform module no longer ignores `template.revision`.
1092
+ Ignoring it pinned the live revision name in state, so any structural change
1093
+ failed with Cloud Run 409 `Revision named '<name>' with different configuration
1094
+ already exists`. The Cloud Tasks queue now also waits on the IAM module so the
1095
+ first apply cannot 403 on `cloudtasks.queues.create`.
1096
+ - `compile-workspace-deps` uses `tsconfig.server.json` when present so
1097
+ `@terreno/admin-spa` emits `src/dist` for example-backend coverage.
1098
+ - Compiled example-backend images (`bun build --compile`) no longer point admin migrations at `$bunfs`. The Docker image copies `migrations/` to `/app/migrations` and sets `MIGRATIONS_DIR`. A missing directory returns 404 `Migration directory not found` instead of a generic 500.
1099
+ - `test:coverage` and new-file coverage no longer fail when Bun 1.4.2+ exits 1 for bunfig `coverageThreshold` after every test passed. Isolated LCOV merges still enforce the 95% / 90% gates.
1100
+ - Terraform preview CI now prints Infra Manager `errorCode` / `errorLogs` when
1101
+ `previews create` fails, and `terraform-admin` gains `roles/logging.logWriter`
1102
+ so Cloud Build can write regional logs.
1103
+ - Admin Migrations re-enables Dry run / Apply when task polling fails. `terreno-migrate generate` keeps safe index operations as comments in fail-closed stubs that also include unsafe ops.
1104
+ - `terreno-pick-roast-loop` resumes only Pick or Roast. A stored `next: brew` finishes
1105
+ the loop without launching Brew, Taste, or Grow.
1106
+ - example-backend listens on `PORT` before connecting to MongoDB and attaching
1107
+ the Terreno Express app. If that boot later throws, the process closes the
1108
+ holder and exits so Cloud Run does not keep a 503 listener. GitHub Actions
1109
+ preview deploys overwrite Cloud Run revision secrets, smoke-test the same
1110
+ `JOBS_*` env as the revision, rebuild traffic from Ready revisions (omitting
1111
+ failed tags), deploy `--no-traffic` without `--tag`, then tag the new revision.
1112
+ - RBAC permission resolution now keys the in-memory grant cache by user id and
1113
+ sorted role names, so promoting a user (for example e2e `setUserAdmin`) takes
1114
+ effect on the next `/auth/me` instead of serving a 30s stale set.
1115
+ - Conflict resolution with **Keep mine** now preserves the admin-window mutation marker
1116
+ when cloning a durable outbox row, so its retry retains AdminApp authorization,
1117
+ protected-field stripping, hooks, and audit handling.
1118
+ - `Tooltip` no longer disappears the instant it opens on web. The portal layer that
1119
+ renders overlays (tooltips, modals, toasts) declared `pointerEvents: "box-none"` in an
1120
+ inline style, which react-native-web drops, so every mounted portal covered the app with
1121
+ a full-screen overlay that swallowed hover and press events. The portal layer now uses a
1122
+ registered `StyleSheet` style, and `Tooltip` stays off screen until its trigger has been
1123
+ measured instead of flashing in the top-left corner.
1124
+ - `TextField` on React Native Web no longer enters an update-depth loop when
1125
+ browser autocorrect alternates controlled values. Web autocorrect/spellcheck is
1126
+ disabled, the input handler stays stable, and rapid synthetic A→B→A reversals
1127
+ are suppressed without blocking normal typing or delayed backspace/retype.
1128
+
1129
+ ## [57.3.0] - 2026-09-02
1130
+
1131
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.3.0.md`](mcp-server/src/docs/upgrades/57.3.0.md).
1132
+
1133
+ ### Added
1134
+
1135
+ - The lifecycle stages now ship as a Claude Code plugin. Add the marketplace with
1136
+ `/plugin marketplace add TerrenoLabs/terreno`, install with
1137
+ `/plugin install terreno@terreno-plugins`, then invoke `/terreno:1-grow`. The marketplace
1138
+ name is `terreno-plugins` so it does not collide with the plugin name `terreno` (Claude
1139
+ Code's installer breaks when those names match). Claude Code takes a
1140
+ plugin skill's command from the frontmatter `name`, so its shortened stage names
1141
+ (`1-grow` … `5-taste`) ship as a generated copy at `plugins/terreno-claude/`
1142
+ (`bun run skills:sync`). Cursor and `npx skills` are unchanged: plugin
1143
+ `terreno-planning`, stages `terreno-1-grow` … `terreno-5-taste`.
1144
+ - The lifecycle stages now ship as a Codex plugin. Add the marketplace with
1145
+ `codex plugin marketplace add TerrenoLabs/terreno`, install with
1146
+ `codex plugin install terreno-planning --source terreno-plugins`, then invoke
1147
+ `$terreno-1-grow`. Codex uses the canonical `plugins/terreno-planning/` tree
1148
+ (`.codex-plugin/plugin.json`) and the repo marketplace at
1149
+ `.agents/plugins/marketplace.json`. Stage names match Cursor and `npx skills`.
1150
+ - `TwilioSmsProvider` at `@terreno/comms/adapters/twilioSms` (optional peer `twilio`). Sends
1151
+ prefer a messaging service SID over a from-number, require valid E.164 destinations, classify
1152
+ Twilio error codes, and store a console deep link on accepted sends. The example backend
1153
+ registers the adapter when Twilio env vars are complete.
1154
+ - `TwilioVerifyProvider` at `@terreno/comms/adapters/twilioVerify` (optional peer `twilio`).
1155
+ Starts and checks SMS/email OTP via a Verify service SID, classifies Twilio errors, redacts
1156
+ destinations, never stores codes, and marks verification rows non-retryable. The example
1157
+ backend registers the adapter when `TWILIO_VERIFY_SERVICE_SID` is set with account
1158
+ credentials.
1159
+ - Admin comms dashboard: filter and inspect delivery logs, retry failed sends (including bulk retry
1160
+ with a cap), and view per-provider failure rates. Created and attempt timestamps print in the
1161
+ operator locale. List, stats, and bulk retry share a trailing 7-day window when dates are omitted.
1162
+ Editing a filter while that window is implicit keeps both date bounds. Push
1163
+ `beforeSend` cancel returns `loggedMessageId`. Retry returns the log row created by that send. Routes live on `@terreno/comms`; screens ship in
1164
+ `@terreno/admin-frontend` as the `comms` custom screen.
1165
+ - `describeModel()` and `describeModelForRouter()` in `@terreno/api` walk Mongoose schemas once and expose a canonical `ModelDescription` field graph. OpenAPI (`getOpenApiSpecForModel`), admin `/admin/config` field metadata, and MCP Zod tool schemas now format that graph instead of independently walking `schema.paths` or mongoose-to-swagger. Map fields take their value kind from Mongoose `of` / `getEmbeddedSchemaType()` (not a date fallback).
1166
+
1167
+ Exports include `modelDescriptionToOpenApiSpec`, `modelDescriptionToAdminFields`, and `fieldDescriptionToZodType`. See `docs/explanation/schema-metadata.md`.
1168
+ - `ExpoPushProvider` at `@terreno/comms/adapters/expoPush` (optional peer
1169
+ `expo-server-sdk`). `sendPush` returns one `SendResult` per token, chunks Expo
1170
+ payloads, classifies ticket/receipt errors, and polls receipts. `DeviceNotRegistered`
1171
+ deactivates `PushToken` rows via `CommsService.deactivatePushToken`. `MessageTooBig` is
1172
+ `errorClass: config` and does not deactivate the token. `expo-server-sdk`
1173
+ moved off `@terreno/api`. The example app requests notification permission, registers the device token after
1174
+ login, and exposes a profile-screen test send (`POST /comms/dev/testPush`) in
1175
+ non-production.
1176
+ - Added `create-github-issue` and `work-github-issues` skills plus a lifecycle
1177
+ work-item GitHub form so agents can file pick-ready issues, confirm a plan,
1178
+ post it as the Roast contract, then implement with Pick ⇄ Roast.
1179
+ - Opt-in HTTP rate limiting on `TerrenoApp` via `rateLimit: {}` (memory default; `redis` or `mongo` for shared buckets). Login and related auth routes use 20 requests / 15 minutes; other framework HTTP uses 600 / 15 minutes. Credential-exchange JWT routes ignore a stale access token. Trailing slashes and Express `req.path` drive the auth/api bucket. `trustProxy` defaults off (set `1` on Cloud Run). Omitted `rateLimit` is a no-op until Terreno 58. 429 is `APIError` `code: "rate-limit-exceeded"`. See `docs/how-to/rate-limiting.md`.
1180
+ - Inbound webhooks on `WebhooksApp`: raw-body capture, HMAC/Stripe/Twilio/SendGrid
1181
+ verifiers (SendGrid ECDSA uses the same 300s timestamp window as Stripe), memory or
1182
+ Mongo `webhookReceipts` idempotency. `CommsApp` mounts Twilio status/inbound and
1183
+ SendGrid Event Webhook routes when passed the same plugin. `recordDeliveryEvent`
1184
+ rethrows a failed `CommsMessage` save so webhook claims release. See
1185
+ `docs/how-to/inbound-webhooks.md`.
1186
+ - `terreno_search_docs` and `terreno_get_component_docs` accept an optional
1187
+ `version` so agents can search retained docs snapshots for the consumer's
1188
+ `@terreno/*` lockstep version. Snapshot component pages match both hyphenated
1189
+ camelCase filenames and concatenated generator slugs.
1190
+ - Pull requests now fail when a newly added workspace `.ts` or `.tsx` implementation
1191
+ file is below 90% function coverage or 90% line coverage. Run
1192
+ `bun run check:new-file-coverage --base=origin/master --threshold=90` locally.
1193
+ The gate reuses each package's `bun test` file arguments so Playwright `*.spec.ts`
1194
+ files are not collected. Glob arguments are expanded before spawn so packages such
1195
+ as `example-frontend` still collect `*.test.ts` files. Globs that match no files
1196
+ are omitted.
1197
+ - JWT and Better Auth password reset plus email verification: `POST /auth/forgotPassword` (always 202), `POST /auth/resetPassword`, `POST /auth/sendVerification`, and `POST /auth/verifyEmail`. Opt in with `emailVerificationPlugin`, `tokenEpoch`, `authOptions.publicAppUrl` / `sendMail`, and `@terreno/comms` `renderAuthMail`. Better Auth uses the same `publicAppUrl` and `sendMail`. `LoginScreen` accepts `onForgotPassword`. See `docs/how-to/password-reset.md`.
1198
+ - The `terreno-planning` Cursor plugin (`2.3.0`) adds two outer-loop skills beside the
1199
+ five stages: `terreno-planning-loop` walks the approved task list (default Grow once,
1200
+ then Pick once — Pick owns the pick-roast inner loop; pass `phases=` to restrict to
1201
+ `grow`, `pick`, `roast`, `brew`, and/or `taste`), and `terreno-taste-sweep` finds the
1202
+ author's open non-draft PRs that are conflicting or failing and reinvokes Taste until
1203
+ each is mergeable or blocked.
1204
+ - `Popover` in `@terreno/ui` for previewing a document with loading, loaded, and error
1205
+ states, an open action, and optional thumbs up/down feedback.
1206
+ - CI now fails production TypeScript that uses `function` declarations, `Date`/`Date.now()`, `throw new Error`, `console.log`, Mongoose `findOne`, or unsuppressed `as any`. Run `bun run check:source-rules`.
1207
+ - Upgrade notes format, 0.21.0–0.30.0 backfill, `terreno_get_upgrade_guide` coverage headers, versioning policy, `upgrading-terreno` skill, and changelog links to `mcp-server/src/docs/upgrades/`.
1208
+
1209
+ ### Changed
1210
+
1211
+ - Brew and Taste now discover and observe product CI on every configured host, including
1212
+ CircleCI and Buildkite, not only GitHub check runs. Waits use provider-native hooks such
1213
+ as `gh pr checks --watch`, `circleci run watch`, and `bk build watch` where available,
1214
+ with bounded polling only as fallback. Plugin `terreno-planning` is `2.4.0`.
1215
+ - Brew and Taste now sleep until async review bots such as Bugbot and CodeQL finish on
1216
+ the current head, then continue so they can react in the same invocation. Ordinary
1217
+ product CI still uses Taste `PENDING` and the outer loop. Plugin `terreno-planning` is
1218
+ `2.2.0`.
1219
+ - PR GitHub Actions spend fewer minutes on docs, Playwright, Expo fingerprints,
1220
+ Maestro, and the example-backend Docker check. Docs previews build only the
1221
+ current version (unminified, no local search index) and reuse generated
1222
+ TypeDoc/component MDX when the source hashes match. Docusaurus Faster
1223
+ (Rspack) is on for PR and `master`; production still builds every versioned
1224
+ tree.
1225
+
1226
+ CI pins Bun `1.4.0` instead of `latest`. Playwright e2e compiles the workspace
1227
+ once per run and shares `dist/` with the spec shards. Example-backend CI
1228
+ compiles `@terreno/*` deps in one process and watches `api/**`. The backend
1229
+ Docker check rebuilds only when the image recipe changes; CD still builds
1230
+ preview images from source.
1231
+
1232
+ Backend startup now defers sync index creation until after MongoDB connects.
1233
+ This prevents import-time Mongoose buffering timeouts from blocking Cloud Run
1234
+ containers before they begin listening. The example backend also avoids the
1235
+ OpenTelemetry Mongoose patch that deadlocked index creation in Bun-compiled
1236
+ binaries, keeps startup logging on Cloud Run's captured stdout instead of a
1237
+ blocking network transport, and lets Better Auth-only realtime run without a
1238
+ legacy JWT secret.
1239
+ - Architectural PR review and Maestro web E2E now run on CircleCI. Matching GitHub
1240
+ workflows are retained with `on: []` for rollback. Cursor Approval Agent, Bugbot,
1241
+ and Security Agent stay on GitHub (Cursor GitHub App automations, not repo
1242
+ workflows).
1243
+ - CircleCI path filters start Netlify and GCP production and PR preview jobs.
1244
+ Those jobs skip (exit 0) until `terreno-netlify` and `terreno-gcp` are filled.
1245
+ GitHub Actions remains the live deployer in that window. After CircleCI
1246
+ secrets exist and a deploy succeeds, set the GHA deploy workflows back to
1247
+ `on: []` so terraform is not applied twice. Fork PRs skip CircleCI previews.
1248
+ Preview cleanup on PR close uses GitHub `preview-cleanup.yml`.
1249
+ - CircleCI provides Netlify and GCP deploy jobs, semver-tag npm releases, and
1250
+ manual preview cleanup, preview deploy, EAS development build, and
1251
+ single-package publish operations. Automatic production deploy path triggers
1252
+ stay paused until the Netlify contexts and GCP OIDC bootstrap pass manual
1253
+ verification. Matching GitHub CI/CD workflows are retained with `on: []` for
1254
+ rollback. CircleCI uses OIDC for GCP; no service-account JSON key is required.
1255
+ - `modelRouter` registers collections in one in-memory catalog keyed by route path. MCP, realtime, and sync read shared `ModelRouterOptions` from that catalog; `replaceCollectionOptions` updates every surface at once. Registry `clear*` helpers clear the whole catalog in tests.
1256
+ - `@terreno/api` RBAC routes now narrow path parameters at runtime instead of
1257
+ relying on implicit `any`. `@terreno/ui` Hyperlink props (`linkify`, styles,
1258
+ `injectViewProps`) and the Google Maps `window.google` global use concrete
1259
+ types instead of `any`.
1260
+ - Root lint now rejects explicit `any` suppressions without a `noExplicitAny:` rationale.
1261
+ - `improve-rulesync` now keeps user-facing guidance in `docs/` and agent-facing
1262
+ guidance in `.ai/` or rules, with cross-links instead of duplicated architecture.
1263
+ - Heavy optional `@terreno/ui` screens (`GPTChat`, `MarkdownEditor`, `ConsentFormScreen`, `AIRequestExplorer`, and the other named exports from `lazyBoundaries/heavyOptionalExports`) load through lazy boundaries so a root `@terreno/ui` import stays smaller. Named exports are unchanged.
1264
+ - MCP create, update, and delete tools run through the same `executeCreate` /
1265
+ `executeUpdate` / `executeDelete` pipeline as REST and Sync. Permission denials
1266
+ and hook failures use `APIError` titles in the MCP error envelope. User-role
1267
+ stripping happens after hooks in the executor (MCP uses the registry model
1268
+ name). `loadDocOr404` maps invalid document `_id` values to 404, not populate
1269
+ `CastError`s.
1270
+ - Pick and Roast now run as an automated inner loop: implement one unblocked task, roast
1271
+ it, then pick the next until the approved list is done. Roast never invokes Pick; Pick
1272
+ owns continuation. Exactly one driver continues after each Roast. Later tasks rediscover
1273
+ docs and skills. Brew starts only after every in-scope task has Roast `PASS`. Plugin
1274
+ `terreno-planning` is `2.3.0`.
1275
+ - Planning plugin skills (five stages plus planning-loop and taste-sweep) are model-invocable. The lifecycle checker requires those skills omit `disable-model-invocation`.
1276
+ - Aligned README, docs landing, docs site tagline, agent context, MCP overview, and npm package descriptions on the canonical "Django/Rails for TypeScript — with universal app support" positioning.
1277
+ - CircleCI is enabled again for package CI, repo policies, and Playwright e2e
1278
+ (`.circleci/config.yml` setup + path-filtering). Config-only PRs run a small
1279
+ smoke slice; mixed PRs skip that slice so path-filtered jobs are not doubled.
1280
+ E2E shards share one compile + `expo export`. `rulesync-check` runs only when
1281
+ rule sources change. CircleCI deploy jobs are available through manual
1282
+ parameters while automatic production path triggers remain paused. Matching
1283
+ GitHub deploy workflows are retained with `on: []`. See `docs/how-to/circleci.md`.
1284
+ - Taste waits in-process for product CI with a GitHub CLI or CircleCI CLI watch loop.
1285
+ Before any push it always pulls latest `master`, then runs `bun lint` (and affected
1286
+ tests) in a fresh subagent with no parent conversation, then pushes and watches CI.
1287
+ Plugin `terreno-planning` is `2.5.0`.
1288
+
1289
+ ### Deprecated
1290
+
1291
+ - `modelRouter` `realtime` and `@terreno/rtk` cache-patching helpers (`realtimeList`,
1292
+ `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`) are deprecated and will
1293
+ be removed in Terreno **58**. Migrate collection live updates to `sync` +
1294
+ `@terreno/syncdb`. `RealtimeApp` remains required for sync sockets. See
1295
+ [migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md) and
1296
+ [remove-legacy-realtime.md](docs/tasks/remove-legacy-realtime.md).
1297
+
1298
+ ### Fixed
1299
+
1300
+ - Admin custom screens now show a clickable back arrow by default. The shared
1301
+ `AdminScreenPage` routes back to admin home reliably on web instead of depending on
1302
+ browser history, and `Page` supports an explicit `onBack` handler.
1303
+ - Better Auth lazy User create no longer sets `oauthProvider: null` on email/password sign-up, so `strict: "throw"` User schemas without that field get `req.user` on the first authenticated request instead of 401.
1304
+ - CircleCI Playwright chaos e2e no longer force-restarts the sync client after
1305
+ flaps. `goOnline` waiting for the Offline banner to hide is the reconnect
1306
+ signal; `client.stop()` hung for 30s on the production static export.
1307
+ - CircleCI automatic Netlify and GCP production path triggers are paused until
1308
+ their contexts and OIDC bootstrap pass manual verification. Netlify jobs now
1309
+ validate credentials before expensive builds, docs builds avoid minification to
1310
+ fit the available executor memory, and the Terraform configuration is formatted.
1311
+ - CircleCI no longer deploys the production demo from prerelease tags, Zoom
1312
+ release notify reports failure when publish fails, Maestro keeps Xvfb alive as
1313
+ a background step, and CircleCI OIDC can require the `terreno-gcp` context UUID
1314
+ before GCP impersonation.
1315
+ - Taste can query CircleCI with `CIRCLECI_TOKEN` on the GitHub App project slug. The
1316
+ new-file coverage job compiles the same workspace packages as example-backend CI.
1317
+ The example profile form no longer resets the name field when `/auth/me` is refetched.
1318
+ - Example backend Cloud Run images include `expo-server-sdk` in the compiled
1319
+ binary by injecting an `Expo` client into `ExpoPushProvider`. Preview deploys
1320
+ no longer crash at boot with a missing optional peer.
1321
+ - GitHub `cd.yml` GCP preview jobs (`terraform-preview`,
1322
+ `backend-deploy-preview`) and `preview-cleanup.yml` skip fork pull requests.
1323
+ OIDC `id-token` is granted only on jobs that authenticate to GCP, not the
1324
+ whole workflow. Fork PRs keep `repository: TerrenoLabs/terreno` on the
1325
+ token, so WIF would otherwise accept them.
1326
+ - Sync mutation date equality now parses date-only ISO strings as UTC and rejects invalid input instead of throwing. The unused datetime NumberPicker stores UTC ISO so Luxon can round-trip the picker value.
1327
+ - The `maestro-e2e` CI job now exports the example-frontend web bundle before starting the
1328
+ example backend. That executor shares its memory with the mongo service container, and
1329
+ bundling alongside a running backend got the export OOM-killed (`SIGKILL`) even though the
1330
+ same export succeeds in `e2e-prepare`.
1331
+ - `bun run check:new-file-coverage` now expands the glob arguments it reads from a package's
1332
+ `test` script before spawning `bun`, so packages without a `src/` directory (such as
1333
+ `example-frontend`) no longer fail with "filters did not match any test files". Expo Router
1334
+ route-structural entry files under `app/` (`index`, `_layout`, `+not-found`, and dynamic
1335
+ segments) are exempt from the gate; the router mounts them by file path and the screens they
1336
+ render are covered in their owning package.
1337
+ - JWT password reset now updates Better Auth credentials and sessions when both stacks are mounted, Better Auth password reset updates the JWT password and `tokenEpoch`, mailbox changes invalidate unused reset tokens even without `emailVerified`, authenticated verification resend returns 501 without a `publicAppUrl`, and Better Auth recovery hooks refuse to send relative links.
1338
+ - Select dropdown chevrons stay inside the field border in narrow selects on web. The `am`/`pm`
1339
+ and timezone pickers in `DateTimeField` no longer render their chevrons far to the right of
1340
+ their boxes.
1341
+ - Example backend Cloud Run images include `twilio` in the compiled binary by injecting
1342
+ a Twilio client into the SMS and Verify adapters. Invalid SMS destinations return a
1343
+ permanent `SendResult` instead of throwing, so the facade does not retry them. Partial
1344
+ Twilio env in the example backend throws `APIError` at boot.
1345
+
1346
+ ## [57.2.0] - 2026-08-24
1347
+
1348
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.2.0.md`](mcp-server/src/docs/upgrades/57.2.0.md).
1349
+
1350
+ ### Breaking
1351
+
1352
+ - Lifecycle stage handoff YAML is now compact `v: 2` (`v`, `stage`, `status`, `next`,
1353
+ `action`; omit empty keys). Outer loops that parse `schema_version` / `recommended_next_stage`
1354
+ must switch keys. Chat and PRs keep the YAML in a Details toggle. Grow lists every grilled
1355
+ decision in an unbounded table after the 15-line index, or omits that table when there were
1356
+ none, and stays on a question until the answer is executable.
1357
+
1358
+ ### Added
1359
+
1360
+ - Admin RBAC is now first-class: `admin:access` is the only permission that opens the admin
1361
+ page. Per-model access is `read` / `write` / `writeOwned` (with `isOwned` helpers and custom
1362
+ `authorize` callbacks). The shell hides models, custom screens, scripts, and Platform tools
1363
+ the caller cannot use; row update/delete follow per-record capabilities; roles use a
1364
+ one-select access level; denied forms are read-only.
1365
+
1366
+ ### Changed
1367
+
1368
+ - Upgraded the toolchain to Bun 1.4. The GitHub Actions jobs that pinned an exact
1369
+ Bun version now use `1.4.0`, `@types/bun` and `bun-types` move to `^1.4.0`, and
1370
+ apps scaffolded by `@terreno/mcp` get `@types/bun@^1.4.0`. Contributors should
1371
+ run Bun 1.4 or newer locally. EAS build profiles keep their existing Bun pin:
1372
+ `eas.json` is an Expo fingerprint input, so bumping it would change the native
1373
+ runtime version and force a rebuild for a toolchain-only change.
1374
+
1375
+ ## [57.1.0] - 2026-08-23
1376
+
1377
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.1.0.md`](mcp-server/src/docs/upgrades/57.1.0.md).
1378
+
1379
+ ### Added
1380
+
1381
+ - Admin configuration can now live beside `modelRouter` registrations or in plugin
1382
+ `adminContribution()` metadata. `@terreno/admin-frontend` adds a provider-backed widget registry,
1383
+ screen router, filter drawer, bulk actions, autocomplete references, and first-party Feature Flags,
1384
+ Consent, Documents, and AI admin integrations.
1385
+ - `terreno_bootstrap_app` scaffolds Better Auth + `@terreno/syncdb` (replica-set MongoDB,
1386
+ `SyncApp`/`RealtimeApp`, `SyncDbProvider`) instead of JWT `generateAuthSlice`
1387
+ - CircleCI dual-run for package CI, repo policies, and Playwright e2e (`.circleci/`;
1388
+ deploys still on GitHub Actions). See `docs/how-to/circleci.md`.
1389
+ - `@terreno/comms` Phase 1 gap-fill: `beforeSend` mutate/cancel, `recordDeliveryEvent` /
1390
+ `recordOptOut`, attempt history on `CommsMessage`, payload retention
1391
+ (`retainPayloadDays`, `redactPayload`), and channel-wide transient retry (SMS,
1392
+ verification start, per-token push). `onRetry` stays `(context, result)` with
1393
+ `context.attempt`. Push prune honors `errorClass: "permanent"` as well as
1394
+ `isPermanentFailure`.
1395
+ - `createCollectionHooks` in `@terreno/syncdb/react` and optional per-mutation
1396
+ `maxAttempts` on syncdb writes.
1397
+ - Model Context Protocol support in `modelRouter` via an `mcp` option: opted-in
1398
+ models expose their CRUD operations as MCP tools at `POST /mcp`, reusing the
1399
+ same permissions, query filters, population, and lifecycle hooks as REST
1400
+ ([#358](https://github.com/TerrenoLabs/terreno/pull/358)). `getMCPTools(user)`
1401
+ in `@terreno/ai` returns those tools as Vercel AI SDK objects for in-process
1402
+ chat. MCP list filters accept Mongo comparison operators (`$in`, `$gte`, `$ne`,
1403
+ and friends) and top-level `$and` / `$or` on `queryFields`; operators that can
1404
+ execute code (`$where`, `$expr`, `$function`) are rejected. `@terreno/rtk`
1405
+ adds `useMCPTools()` and `useTerrenoChat()`. MCP HTTP/stdio servers use the
1406
+ TypeScript SDK v2 (`2026-07-28`) with a how-to guide and structured tool-call
1407
+ logs. Lifecycle hooks and REST `responseHandler` receive a stub Express-shaped
1408
+ request from `createMCPRequest` (authenticated user, tool args as `body`, empty
1409
+ `headers`/`query`/`params`, `isMCPRequest: true`) rather than forwarded HTTP
1410
+ headers. `registerMCPTool` adds custom tools alongside modelRouter CRUD. The
1411
+ example backend exposes `users_todo_statuses` (admin-only: every user and their
1412
+ todo completed flags).
1413
+ - Example app profiles list every assigned role and link superadmins to role editing. The admin
1414
+ script runner includes a guarded `resetDatabase` action, and seed data includes
1415
+ `superadmin@example.com`. The admin roles page supports creating and editing roles by selecting
1416
+ from the server's available permissions. Admin script execution requires `admin:runScripts` when
1417
+ RBAC is configured, and live production resets require `ALLOW_ADMIN_DB_RESET=true`.
1418
+ - `SendGridMailProvider` at `@terreno/comms/adapters/sendgrid` (optional peer
1419
+ `@sendgrid/mail`) with sandbox mode, `errorCode`/`errorClass` taxonomy, Email Activity
1420
+ deep links, and one transient retry via `CommsService` hooks (`onError` / `onRetry` /
1421
+ `onSend`).
1422
+ - `terreno-syncdb-codegen` CLI in `@terreno/syncdb` generates typed collection hooks
1423
+ (`store/syncDbSdk.ts`) from OpenAPI `x-terreno-sync` list operations. The CLI is a
1424
+ bin of `@terreno/syncdb`, not a separate `@terreno/syncdb-codegen` package.
1425
+ - `@terreno/syncdb` documentation: reference (`docs/reference/syncdb.md`), migration guide
1426
+ (`docs/how-to/migrate-rtk-to-syncdb.md`), and local-first explainer
1427
+ (`docs/explanation/local-first-data.md`)
1428
+
1429
+ ### Changed
1430
+
1431
+ - User admin forms now load existing RBAC roles into an **Add role** dropdown. Assigned roles
1432
+ remain visible and can be removed without entering raw array values. Blank optional enum
1433
+ fields no longer block otherwise valid saves, and role-update responses include the new roles.
1434
+ - The admin sidebar groups Scripts, Roles, Version, Audit Log, Feature Flags, and Configuration
1435
+ under a consistently spaced **Platform** section at the bottom. Empty navigation sections are
1436
+ hidden, and Audit Log / Feature Flags no longer appear twice under Models.
1437
+ - The Terreno planning plugin 2.0 is a fresh-invocation loop-engineering lifecycle:
1438
+ Grow → Pick → Roast → Brew → Taste. Every bounded stage discovers repository skills,
1439
+ reads/writes a shared evidence-oriented result/state contract, and exits. Brew no longer
1440
+ executes Taste; Taste reacts once to current CI/mergeability/review state while the outer
1441
+ loop owns waiting and reinvocation. Terreno package commands and domain rules remain in
1442
+ repo-local skills. Grow retains interactive grilling and a 15-line approval summary; Pick
1443
+ retains vertical-slice TDD and independent implementation/test-quality review; Roast is
1444
+ the authoritative verifier. The former Grind behavior remains an outer-loop feature
1445
+ profile, and static validation enforces names, transitions, portability, and loop bounds.
1446
+ Deprecated repo-local routers (`ip`, `implement`, `submit`, `autobot`, `check-watcher`)
1447
+ are removed. Quiet package tests (`test:agent`) run with `AGENT=1`. Brew, Taste, and
1448
+ repository PR skills now enforce an attention-budgeted GitHub format: only `Why`, `What
1449
+ changed`, and `Verification` remain visible; optional detail is expandable; comments are
1450
+ reserved for blocked decisions or non-obvious review resolutions.
1451
+ - Unreleased notes are one file per feature in `changelog/unreleased/` instead of a shared
1452
+ `CHANGELOG.md` `## [Unreleased]` section. `docs/implementationPlans/PLAN_INDEX.md` is
1453
+ removed for the same reason — status lives on each IP's `**Status:**` header. Parallel
1454
+ PRs no longer conflict on those shared files.
1455
+ - GitHub Actions package CI (API, AI, UI, RTK, comms, syncdb, examples, E2E, Maestro, admin SPA) now runs on `pull_request` with path filters, and on `push` only to `master`. This stops the first push of a new branch from ignoring path filters and running unrelated jobs (the 13 Playwright shards were the main cost). Docs-only and rules-only PRs skip package CI, Rulesync, CD, and frontend/demo deploy workflows. Example Frontend Deploy PR path filters match the frontend build gate (backend-only PRs skip the workflow; mixed PRs still retarget preview `BASE_URL`).
1456
+ - CircleCI pipelines are disabled (`.circleci/config.yml` no-op). GitHub Actions
1457
+ remains the CI of record. Restore with `.circleci/config.setup.yml`. See
1458
+ `docs/how-to/circleci.md`.
1459
+ - Lifecycle stages now follow a documentation contract: read architecture docs before
1460
+ acting, update them in the same slice, and fail user-visible or architectural work that
1461
+ ships without matching docs. All agent skills are installable with
1462
+ `npx skills add TerrenoLabs/terreno`. The committed `skills/` tree is generated from
1463
+ `.rulesync/skills/`, the planning plugin stages, and `<package>/.ai/skills/` overlays via
1464
+ `bun run skills:sync`.
1465
+ - Role assignment **preview** no longer writes `RbacAudit` denied-assign rows. Denied
1466
+ mutation audits are recorded only for the escalation `403` (`Cannot grant permissions
1467
+ you do not hold`); other failures are not stored as denials, and a failed audit write
1468
+ does not replace that original `403`.
1469
+ - When a `PermissionSource` refresh fails with `staleOnFailure: "deny"` (the default),
1470
+ last-cached `deny` grants stay in force so IdP/ABAC restrictions do not lift. Additive
1471
+ `roles` / `permissions` from that source are still omitted.
1472
+ - RBAC `createView: "deny"` and unknown field-view names fail closed instead of granting a
1473
+ full mask. Nested field omits clone documents; write masks honor dotted paths. Bulk create
1474
+ and array mutations apply the same write mask as single-document writes.
1475
+ - AdminApp model CRUD requires resource actions in addition to `admin:access`. Self-service
1476
+ still cannot write User `admin`/`roles`. Without RBAC, admin CRUD may set `admin`.
1477
+ With RBAC, `roles` go through `RoleManager.assign`. Changing `admin` requires
1478
+ `rbac:assignRoles` plus an actor who already holds the legacy admin flag;
1479
+ `rbac:manageRoles` is not a substitute. Unchanged echoed `admin`
1480
+ values are allowed. If `assign` fails after an admin User update or bulk-patch,
1481
+ non-role fields are restored so the request does not keep a partial write.
1482
+ Role update requires the actor to hold the union of current and incoming
1483
+ permissions, and delete requires holding the role's current permissions, so
1484
+ `manageRoles` cannot empty or remove grants the actor lacks.
1485
+ assign/unassign require the actor to already hold the
1486
+ target user's current permissions. The seeded `auditor` role no longer receives
1487
+ `admin:access` via read-only expansion. Admin CRUD for a resource missing
1488
+ from statements fails closed for list/read, search, and writes. Framework statements now
1489
+ include `featureFlag`, `consentForm`, and `consentResponse` so superadmin `*`
1490
+ can list those admin models; example-backend adds `adminAuditLog` list/read.
1491
+ `POST /admin/background-tasks`
1492
+ requires `admin:runScripts`; version-config GET/PUT require `configuration:read` /
1493
+ `configuration:update`.
1494
+ - MCP and SyncDB registries use resolved RBAC options (TerrenoApp-injected and the
1495
+ documented `access` + `accessControl` path, including pathless `modelRouter`)
1496
+ instead of the pre-build legacy permission arrays. Create/update also apply
1497
+ `validateAccessWritePayload` (field views / `createView: "deny"`).
1498
+ User `roles` on modelRouter writes (HTTP, sync, and MCP) are dropped when
1499
+ `accessControl` is set.
1500
+ Example-backend `backfillAdmins` is dry-run unless `RBAC_BACKFILL_ADMINS=true`.
1501
+ - `runActionPermissions` combines legacy `action.permissions` with RBAC instead of replacing
1502
+ them. Actions without `access` inherit the router's `access.resource` and the mapped CRUD
1503
+ verb (`instance` POST → `update`, `collection` POST → `create`). Example todo
1504
+ `bulkComplete` / `markComplete` set `access: {resource: "todo", action: "update"}`
1505
+ so they cannot bypass `todo:update`. Empty action permissions and inherited CRUD actions
1506
+ mapped to `null` remain disabled. Assignment previews use uncached permission resolution
1507
+ on both the current and proposed sets, honor source `staleOnFailure` without writing
1508
+ caches, and reject role permissions the actor does not already hold. The admin role
1509
+ editor keeps Create/Save in the
1510
+ modal footer and scrolls the permission grid so a larger statement vocabulary cannot
1511
+ hide the save control. Create/list responses always apply the **read** field
1512
+ mask. Per-router `access.scope`
1513
+ extra PermissionSets are evaluated on HTTP and realtime reads. Sync and realtime
1514
+ serializers accept change-stream BSON post-images (no Mongoose `toObject`) so
1515
+ `sync:delta` is not dropped after RBAC wraps `defaultResponseHandler`. Invalid permission sets
1516
+ use a stable `APIError.title`.
1517
+ - Self-service signup and `PATCH /auth/me` strip `organizationIds` alongside `admin` and `roles`,
1518
+ preventing callers from assigning themselves tenant membership. Administrative organization
1519
+ membership changes must use a privileged server-side path.
1520
+ - Ordinary RBAC-enabled User modelRouter, sync, and MCP writes strip `admin`, `roles`, and
1521
+ `organizationIds`. AdminApp strips `organizationIds` the same way, and marks legacy
1522
+ `admin` writes only after `assignRoles` and an existing-admin check succeed.
1523
+ - `RoleManager` writes `RbacAudit` (with `permissionDelta`) on create, update, remove, assign,
1524
+ and unassign. Denied escalation attempts are stored with `denied: true`. HTTP `rbacRouter`
1525
+ no longer writes a second audit row. Apps can pass `auditSink` (one function or an array)
1526
+ to fan records into a consuming-app log; set `persistAudit: false` to skip the built-in
1527
+ collection (at least one sink is then required).
1528
+ - `RbacRole.seedDefaults` accepts `extraRoles` and shares `upsertSeededRole` with
1529
+ `RoleManager.seedDefaults`. `previewRoleChange` reports a real `affectedUserCount`.
1530
+ - Importing `@terreno/api` no longer registers `RbacRole` / `RbacAudit` on the default
1531
+ mongoose connection. Use `createAccess({connection})` or `createRbacRoleModel` /
1532
+ `createRbacAuditModel`. The `RbacRoleModel` / `RbacAuditModel` singletons are removed;
1533
+ the `RbacRoleModel` type remains.
1534
+ - The RBAC implementation plan is Complete (phases 1–6 shipped).
1535
+
1536
+ ### Deprecated
1537
+
1538
+ - **`@terreno/rtk` for data synchronization** — deprecated as of **56.0.0**. Still published
1539
+ through the current major line; will not ship in the next major. Migrate collection CRUD to
1540
+ [`@terreno/syncdb`](docs/reference/syncdb.md) using
1541
+ [migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md). Continue using `@terreno/rtk`
1542
+ for the OpenAPI SDK, Better Auth Redux, feature flags, and sockets.
1543
+
1544
+ ### Fixed
1545
+
1546
+ - Admin lists no longer inherit public `queryFilter` scoping, and `adminFilter` Mongo operators
1547
+ are not rejected as client filters.
1548
+ - Admin mutations and responses consistently scrub excluded fields, including populated refs.
1549
+ - Plugin admin contributions forward `populatePaths`.
1550
+ - Document Storage clients use the contributed `/documents` API path.
1551
+ - Admin search applies the same `queryFilter`/`adminFilter` as list CRUD.
1552
+ - AI Request Explorer multi-type filters use `$in` instead of dropping the filter.
1553
+ - Admin list search (`q`) is a case-insensitive partial match across `searchFields`.
1554
+ - The filter drawer can clear all filters and disables Apply when the draft is unchanged.
1555
+ - Admin config `name` is unique when the same Mongoose model is mounted at more than one
1556
+ `routePath`, and list search/bulk-patch metadata is looked up by path.
1557
+ - Document download failures log a status code only — not storage paths or provider payloads.
1558
+ - The admin Roles screen scrolls its role cards and the "Available permissions" list instead
1559
+ of clipping them, and keeps the heading and "Add role" button pinned above the scroll area.
1560
+ - `@terreno/ui` no longer triggers the react-native-web warning `"shadow*" style props are
1561
+ deprecated. Use "boxShadow".` — `Filter`, `WebDropdownMenu` (select fields, timezone picker,
1562
+ address field), and `DraggableList` now use `boxShadow`, and the new `createBoxShadow` /
1563
+ `applyColorOpacity` helpers build the shadow value from a color plus opacity.
1564
+ - Patched `react-native-modalize` and `react-native-actions-sheet` to use `boxShadow` on
1565
+ iOS/web and `elevation` only on Android, so the two APIs do not stack. The modalize
1566
+ stylesheet ran at import time, so the deprecation warning appeared on every web page that
1567
+ imported `@terreno/ui`.
1568
+ - Android centered dropdowns (`WebDropdownMenu` `presentation="centered"`) keep `elevation` and
1569
+ omit `boxShadow`, so the two APIs do not stack.
1570
+ - Push hooks and retries are per-token; provider results are zipped to token strings
1571
+ after `beforeSend`. Mail payloads retain `replyTo` and `dynamicTemplateData`.
1572
+ `DeliveryEvent.errorClass` is persisted on the log row. `defaultFrom` is reapplied
1573
+ after `beforeSend`. Payload cleanup is best-effort and cannot fail `logSend` /
1574
+ `appendAttempt`. Hook exception text is logged only; `metadata.hookErrors` stores
1575
+ `hook-threw`, not `String(error)`.
1576
+ - Conflict `requeue` copies per-mutation `maxAttempts` onto the cloned outbox
1577
+ row so `retries: false` stays fail-fast after keepMine.
1578
+ - Expo native fingerprints for the demo and example frontend now exclude `package.json`
1579
+ scripts. Development and test command changes no longer trigger unnecessary native build
1580
+ acknowledgements or rebuilds.
1581
+ - MCP create/update apply REST `validation.excludeFromCreate` /
1582
+ `excludeFromUpdate` and MCP `excludeFields` as a write denylist on persist and
1583
+ hook request bodies, including nested/dot paths and literal dotted keys
1584
+ (`"metadata.nested.token"`). Invalid ObjectIds return a structured not-found
1585
+ instead of crashing on `CastError`; mixed-case 24-hex ids still work. List
1586
+ returns a structured error when `queryFilter` throws. A throwing
1587
+ `responseHandler` / `mcpResponseHandler` becomes a structured tool error instead
1588
+ of a protocol crash. List filters ignore queryFields that sit under an
1589
+ `excludeFields` parent path, matching tool schema generation. Lifecycle hooks
1590
+ that throw `APIError` return `error.title` to the MCP client, matching
1591
+ `queryFilter` handling.
1592
+ - `@terreno/ui` no longer triggers the React Native Web warning
1593
+ `props.pointerEvents is deprecated. Use style.pointerEvents`. Every component that set
1594
+ `pointerEvents` as a prop (`DateTimeField`, `Filter`, `SidebarNavigation`, `ToastNotifications`,
1595
+ `WebDropdownMenu`) now sets it in `style`.
1596
+ - The `react-native-portalize` dependency, which set the deprecated prop on every screen through
1597
+ `TerrenoProvider`, is replaced by an internal portal host. `Host` and `Portal` are now exported
1598
+ from `@terreno/ui` with the same API.
1599
+ - `seedDefaults` no longer overwrites customized unsealed roles on restart; sealed defaults still refresh from code.
1600
+ API tests clear `RbacRole` / `RbacAudit` in `setupDb` so leftover unsealed names cannot leak across cases.
1601
+ - Admin `/bulk-patch` authorizes each target document, so scoped `update` cannot patch out-of-scope ids.
1602
+ - Admin User CRUD can set the `admin` flag when RBAC is off. With RBAC, changing `admin` requires `rbac:assignRoles`; echoed unchanged `admin` values on create/update do not.
1603
+ - Permission resolver caches evict expired and overflow entries so distinct identities cannot grow unbounded.
1604
+ - Clearing a role description in the admin UI sends `null` so PATCH removes the field.
1605
+ - Admin User create rolls back the new row if `RoleManager.assign` fails after insert.
1606
+ - `assign` / `unassign` refuse to change a user whose current permissions the actor does not hold.
1607
+ - MCP model tools pick up TerrenoApp-injected `accessControl` (permissions, query filters, write masks) instead of keeping the pre-build legacy checks.
1608
+ - Example todos Sync Lab panel starts collapsed so the first list row stays
1609
+ above the tab bar on short web viewports.
1610
+ - `terreno-syncdb-codegen` rejects non-identifier collection/type names and
1611
+ JSON-escapes generated strings so a remote OpenAPI document cannot inject
1612
+ TypeScript.
1613
+ - `@terreno/syncdb` no longer misses server changes that land between its startup snapshot
1614
+ and the socket joining a stream's room: each `sync:subscribed` confirmation now pages the
1615
+ streams it names from their cursor, instead of leaving the client stale until the next
1616
+ periodic reconcile
1617
+
1618
+ ## [57.0.0] - 2026-08-20
1619
+
1620
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0.md`](mcp-server/src/docs/upgrades/57.0.0.md).
1621
+
1622
+ First stable release of the Expo SDK 57 line, cut from `master` after
1623
+ [`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes to the npm
1624
+ `latest` dist-tag. Apps on `0.x` should stay pinned until they upgrade Expo.
1625
+
1626
+ ### Added
1627
+
1628
+ - `Filter` and its composable select, boolean, accordion, and change-badge controls in
1629
+ `@terreno/ui` for desktop web filtering flows
1630
+ ([#972](https://github.com/TerrenoLabs/terreno/pull/972))
1631
+
1632
+ ### Changed
1633
+
1634
+ - Terreno's version major now tracks Expo SDK 57 (`57.x.y`). `npm install @terreno/ui`
1635
+ resolves to this line; pin `0.x` if the app has not upgraded Expo yet
1636
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1637
+ - Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
1638
+ (`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
1639
+ `expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
1640
+ `npx expo install expo@latest --fix` then rebuild native binaries
1641
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1642
+ - `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
1643
+ `expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1644
+
1645
+ ## [57.0.0-beta.1] - 2026-08-20
1646
+
1647
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0-beta.1.md`](mcp-server/src/docs/upgrades/57.0.0-beta.1.md).
1648
+
1649
+ First beta of the Expo SDK 57 line, cut from `master` after merging
1650
+ [`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes under the npm
1651
+ `beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
1652
+
1653
+ ### Added
1654
+
1655
+ - `Filter` and its composable select, boolean, accordion, and change-badge controls in
1656
+ `@terreno/ui` for desktop web filtering flows
1657
+ ([#972](https://github.com/TerrenoLabs/terreno/pull/972))
1658
+
1659
+ ### Changed
1660
+
1661
+ - Terreno's version major now tracks Expo SDK 57 (`57.x.y`). The stable `0.x` packages are
1662
+ unaffected; this beta does not move `latest`
1663
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1664
+ - Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
1665
+ (`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
1666
+ `expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
1667
+ `npx expo install expo@latest --fix` then rebuild native binaries
1668
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1669
+ - `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
1670
+ `expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1671
+
1672
+ ## [56.0.0-beta.2] - 2026-08-17
1673
+
1674
+ Upgrade note: [`mcp-server/src/docs/upgrades/56.0.0-beta.2.md`](mcp-server/src/docs/upgrades/56.0.0-beta.2.md).
1675
+
1676
+ Second beta of the Expo SDK 56 line, cut from `master` after merging
1677
+ [`#976`](https://github.com/TerrenoLabs/terreno/pull/976). Publishes under the npm
1678
+ `beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
1679
+
1680
+ ### Added
1681
+
1682
+ - Expo SDK 56 target for frontend packages: `expo ~56.0.12`, `react-native 0.85.3`,
1683
+ `react 19.2.3`, TypeScript 6
1684
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1685
+ - `@terreno/syncdb` local-first data layer (TinyBase MergeableStore, durable outbox,
1686
+ websocket delta sync, encrypted web persistence) plus `SyncApp` / sync protocol support
1687
+ in `@terreno/api`
1688
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1689
+ - `SyncStatusBanner` and `ConflictSheet` in `@terreno/ui` for sync UX
1690
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1691
+ - `@terreno/comms` with pluggable mail, SMS, push, and verification contracts, console
1692
+ development providers, delivery logging, owner-scoped push-token routes, an admin delivery
1693
+ explorer, and generated RTK Query hooks
1694
+ ([#1037](https://github.com/TerrenoLabs/terreno/pull/1037))
1695
+
1696
+ ### Changed
1697
+
1698
+ - Terreno's version major now tracks the Expo SDK major it targets (`56.x.y` for Expo 56).
1699
+ The stable `0.x` packages are unaffected; this beta does not move `latest`
1700
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1701
+ - Frontend peer/catalog stack moves to Expo 56 / React Native 0.85 / React 19.2 /
1702
+ TypeScript 6 — consuming apps must upgrade Expo before installing this beta
1703
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1704
+ - `excludeArchivedPlugin` now filters `findOne` the same way as `find`, matching
1705
+ `isDeletedPlugin`
1706
+ - Sync/REST CRUD executors preserve kebab-case error `code` values
1707
+ (`create-not-allowed`, `update-not-allowed`, `delete-not-allowed`,
1708
+ `invalid-request-body`) and status-specific error names on hook-rejection paths
1709
+ - Publish workflow pins `@terreno/ui` to the release version for prerelease
1710
+ `admin-frontend` / `admin-spa` publishes (same as `ui` / `rtk`)
1711
+ - Catalog `@shopify/react-native-skia` bumped to `2.6.5` (drops the postinstall that
1712
+ could hang `bun install --frozen-lockfile`); removed unused payment/native deps from
1713
+ `example-frontend`
1714
+ - The `terreno-5-dialin` plugin skill now owns merge conflicts that appear after the
1715
+ Pour handoff: it checks PR mergeability each cycle, resolves conflicts by merging the
1716
+ base branch without rewriting pushed history, re-runs checks and frontend
1717
+ verification, and treats a conflicted PR as broken rather than mergeable
1718
+ ([#1039](https://github.com/TerrenoLabs/terreno/pull/1039))
1719
+
1720
+ ### Fixed
1721
+
1722
+ - `generateTokens` in `@terreno/api` now logs and falls back to its default expiration when
1723
+ `TOKEN_EXPIRES_IN` or `REFRESH_TOKEN_EXPIRES_IN` is not a valid duration, instead of throwing
1724
+ from `jwt.sign`
1725
+ - Sync seq claims under Mongoose 9: `claimSyncSeqs` passes `updatePipeline: true` for
1726
+ its aggregation-pipeline `findOneAndUpdate` (required after the master mongoose 9 merge)
1727
+ - `RealtimeApp` accepts injectable `SocketServer` / `startChangeStreamWatcher` so setup
1728
+ tests no longer use process-wide `mock.module` (which broke socketAuth + sync integration)
1729
+ - Mongoose 9 `insertMany` probe in `syncFrontier.test.ts` no longer calls removed `next()`
1730
+ - Sync save error middleware: `throw` instead of `next(error)` under Mongoose 9/Kareem 3,
1731
+ and release pending seq claims on any failed claimed save (not only VersionError)
1732
+ - Drop orphaned `ui` consent-history PDF test left after the move to `admin-frontend`
1733
+
1734
+ ## [0.31.0] - 2026-08-11
1735
+
1736
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.31.0.md`](mcp-server/src/docs/upgrades/0.31.0.md).
1737
+
1738
+ ### Added
1739
+
1740
+ - `EditableCard` in `@terreno/ui`: a summary card with an optional icon, badge,
1741
+ description, helper text, edit button, and attention state
1742
+ ([#981](https://github.com/TerrenoLabs/terreno/pull/981))
1743
+ - MIT `LICENSE` file in every published package, plus contribution guide, changelog,
1744
+ and GitHub issue/PR templates ([#985](https://github.com/TerrenoLabs/terreno/pull/985))
1745
+ - Public roadmap generated from `docs/` into GitHub, with `roadmap:generate` /
1746
+ `roadmap:check` scripts and roadmap discussion setup
1747
+ ([#986](https://github.com/TerrenoLabs/terreno/pull/986), [#991](https://github.com/TerrenoLabs/terreno/pull/991))
1748
+ - `check:upgrade-docs` release gate that fails a tagged publish when a release
1749
+ documents breaking, changed, deprecated, or removed behavior without an
1750
+ `mcp-server/src/docs/upgrades/<version>.md` note ([#987](https://github.com/TerrenoLabs/terreno/pull/987))
1751
+ - Deployment foundation for GCP, deploy guides, and agentic SDLC plugin phase 1
1752
+ ([#988](https://github.com/TerrenoLabs/terreno/pull/988))
1753
+ - Reference documentation for `@terreno/ai`, `@terreno/admin-spa`, and `@terreno/test`,
1754
+ plus full READMEs for `admin-backend`, `admin-frontend`, `ai`, and `api-health`
1755
+ ([#995](https://github.com/TerrenoLabs/terreno/pull/995))
1756
+
1757
+ ### Changed
1758
+
1759
+ - Mongoose 9 support: the workspace runs on Mongoose 9.7.4 and every published
1760
+ package widens its `mongoose` peer dependency to `^8.0.0 || ^9.0.0`, so Mongoose 8
1761
+ consumers keep working ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1762
+ - All published packages are MIT licensed; they were previously Apache-2.0
1763
+ ([#985](https://github.com/TerrenoLabs/terreno/pull/985))
1764
+ - `modelRouter` accepts a wider Mongoose model generic, so models carrying custom
1765
+ query helpers, methods, or virtuals no longer need a cast ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1766
+ - `findOneOrNoneFor` takes `ModelQuery<T>` instead of Mongoose's `FilterQuery<T>`
1767
+ ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1768
+ - `@terreno/admin-backend` reads array field metadata through Mongoose's public
1769
+ `getEmbeddedSchemaType()` (falling back to the Mongoose 8 `caster`) and only emits
1770
+ `itemEnum` when the embedded enum is an array ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1771
+ - Local development requires Node >= 20.19.0 ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1772
+ - Positioning copy blocks and an honest framework comparison table
1773
+ ([#993](https://github.com/TerrenoLabs/terreno/pull/993))
1774
+ - Implementation plans and program docs for the infrastructure MCP server, the B2B
1775
+ platform program, the IP + roadmap flow, and the RTK-to-syncdb migration strategy,
1776
+ with merged plans linked to their roadmap tracking issues
1777
+ ([#990](https://github.com/TerrenoLabs/terreno/pull/990), [#996](https://github.com/TerrenoLabs/terreno/pull/996), [#998](https://github.com/TerrenoLabs/terreno/pull/998), [#999](https://github.com/TerrenoLabs/terreno/pull/999), [#1028](https://github.com/TerrenoLabs/terreno/pull/1028))
1778
+ - Test coverage, rule alignment, and explicit-any remediation across `api`, `ui`, and `rtk`
1779
+ ([#946](https://github.com/TerrenoLabs/terreno/pull/946), [#977](https://github.com/TerrenoLabs/terreno/pull/977), [#978](https://github.com/TerrenoLabs/terreno/pull/978), [#979](https://github.com/TerrenoLabs/terreno/pull/979), [#980](https://github.com/TerrenoLabs/terreno/pull/980), [#982](https://github.com/TerrenoLabs/terreno/pull/982), [#983](https://github.com/TerrenoLabs/terreno/pull/983), [#1000](https://github.com/TerrenoLabs/terreno/pull/1000), [#1001](https://github.com/TerrenoLabs/terreno/pull/1001), [#1002](https://github.com/TerrenoLabs/terreno/pull/1002))
1780
+
1781
+ ### Fixed
1782
+
1783
+ - CI queues `terreno-example` EAS native builds only for new fingerprints
1784
+ ([#965](https://github.com/TerrenoLabs/terreno/pull/965))
1785
+ - Keep the Dial In PR loop active through slow or pending CI, and only return
1786
+ broken checks when no autonomous action can advance them or user direction is
1787
+ required ([#1027](https://github.com/TerrenoLabs/terreno/pull/1027))
1788
+ - Dial In preserves existing PR descriptions instead of overwriting them
1789
+ ([#1030](https://github.com/TerrenoLabs/terreno/pull/1030))
1790
+
1791
+ ## [0.30.0] - 2026-08-03
1792
+
1793
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.30.0.md`](mcp-server/src/docs/upgrades/0.30.0.md) (consolidated 0.21.0 → 0.30.0).
1794
+
1795
+ ### Added
1796
+
1797
+ - `ThumbsUpDownFeedback` UI component ([#948](https://github.com/TerrenoLabs/terreno/pull/948))
1798
+
1799
+ ### Fixed
1800
+
1801
+ - Pass `APIError` context through `modelRouter` without re-wrapping errors raised
1802
+ inside hooks or Mongoose middleware ([#967](https://github.com/TerrenoLabs/terreno/pull/967))
1803
+ - `modelRouter` no longer drops `status`, `title`, `detail`, `code`, and `meta`
1804
+ when re-throwing an `APIError` from create/update/delete handlers, populate,
1805
+ `queryFilter`, list serialization, or array-operation paths
1806
+ - Mongoose validation and cast errors from `modelRouter` writes surface per-field
1807
+ messages via `meta.fields` again instead of generic wrapper titles
1808
+ - `queryFilter` wrapper details use readable message extraction instead of
1809
+ `String(error)`
1810
+ - Framework-thrown errors follow the 0.28.0 contract: stable `title`, per-occurrence
1811
+ text in `detail`, kebab-case `code`, and status subclasses where applicable
1812
+ - `apiUnauthorizedMiddleware` only matches plain `Error("Unauthorized")`, so a
1813
+ `ForbiddenError` titled `"Unauthorized"` stays 403
1814
+
1815
+ ### Changed
1816
+
1817
+ - Add `errorDetail(error)` helper for nested `APIError` detail in framework wrappers
1818
+ - Test coverage, rule alignment, and explicit-any remediation commits ([#959](https://github.com/TerrenoLabs/terreno/pull/959), [#963](https://github.com/TerrenoLabs/terreno/pull/963), [#968](https://github.com/TerrenoLabs/terreno/pull/968), [#969](https://github.com/TerrenoLabs/terreno/pull/969))
1819
+
1820
+ ## [0.29.0] - 2026-07-31
1821
+
1822
+ ### Added
1823
+
1824
+ - Typed and draw/type signature capture fields in `@terreno/ui` ([#958](https://github.com/TerrenoLabs/terreno/pull/958))
1825
+ - Short-attention-span skill for action-first AI output ([#956](https://github.com/TerrenoLabs/terreno/pull/956))
1826
+
1827
+ ## [0.28.0] - 2026-07-30
1828
+
1829
+ ### Added
1830
+
1831
+ - Open source launch program: 15 implementation plans, task lists, and the
1832
+ `build-terreno-app` dogfooding skill ([#942](https://github.com/TerrenoLabs/terreno/pull/942))
1833
+
1834
+ ### Changed
1835
+
1836
+ - Redesign `APIError` to use standard `Error` fields for Sentry grouping ([#949](https://github.com/TerrenoLabs/terreno/pull/949))
1837
+ - Rename `SelectField` `searchable` prop to `disableSearch` ([#954](https://github.com/TerrenoLabs/terreno/pull/954))
1838
+
1839
+ ### Fixed
1840
+
1841
+ - Queue only missing platform dev builds on EAS PR slow path ([#955](https://github.com/TerrenoLabs/terreno/pull/955))
1842
+
1843
+ ## [0.27.0] - 2026-07-29
1844
+
1845
+ ### Added
1846
+
1847
+ - Terreno-native agent skills (`building-terreno-apps`, `terreno-data-fetching`,
1848
+ `terreno-backend-api`, `terreno-ui`) ([#941](https://github.com/TerrenoLabs/terreno/pull/941))
1849
+ - Explicit-any audit script for tracking `any` usage across the monorepo ([#927](https://github.com/TerrenoLabs/terreno/pull/927))
1850
+ - RBAC permissions API design doc ([#887](https://github.com/TerrenoLabs/terreno/pull/887))
1851
+
1852
+ ### Changed
1853
+
1854
+ - Document custom icon registration in `@terreno/ui` ([#910](https://github.com/TerrenoLabs/terreno/pull/910))
1855
+ - Change example-backend seed admin user to `admin@example.com` (password
1856
+ unchanged) ([#935](https://github.com/TerrenoLabs/terreno/pull/935))
1857
+ - Require frontend app login, feature exercise, and PR evidence in agent
1858
+ workflows ([#915](https://github.com/TerrenoLabs/terreno/pull/915))
1859
+
1860
+ ### Fixed
1861
+
1862
+ - Fix `Modal` Confirm button on native Android tablets ([#952](https://github.com/TerrenoLabs/terreno/pull/952))
1863
+ - Guard `@terreno/rtk` web tests against `IsWeb` platform mock leakage ([#934](https://github.com/TerrenoLabs/terreno/pull/934))
1864
+
1865
+ ## [0.26.0] - 2026-07-15
1866
+
1867
+ ### Added
1868
+
1869
+ - `IconButton` gains an `active` interaction state via `state?: "default" | "active"`
1870
+ ([#885](https://github.com/TerrenoLabs/terreno/pull/885))
1871
+
1872
+ ### Changed
1873
+
1874
+ - Remove internal barrel imports; ban new internal barrel `index.ts` files via
1875
+ Biome lint and `check:no-barrel-imports` ([#907](https://github.com/TerrenoLabs/terreno/pull/907))
1876
+ - Order Terreno planning skills by workflow step (`terreno-1-blend` through
1877
+ `terreno-5-dialin`) ([#900](https://github.com/TerrenoLabs/terreno/pull/900))
1878
+
1879
+ ### Fixed
1880
+
1881
+ - Fix `Table`/`DataTable` preview cards on the demo home page rendering as a
1882
+ floating overlay ([#911](https://github.com/TerrenoLabs/terreno/pull/911))
1883
+ - Fix demo, docs, and example-frontend production Netlify deploys silently
1884
+ no-opping on push to `master` ([#903](https://github.com/TerrenoLabs/terreno/pull/903))
1885
+ - Fix duplicate `if` keys in example-app E2E and Admin SPA integration workflows
1886
+ ([#901](https://github.com/TerrenoLabs/terreno/pull/901))
1887
+ - Stabilize AI and admin frontend test suites ([#902](https://github.com/TerrenoLabs/terreno/pull/902))
1888
+
1889
+ ## [0.25.0] - 2026-07-12
1890
+
1891
+ ### Added
1892
+
1893
+ - `@terreno/api`: HTTP client layer — `createAuthenticatedClient`,
1894
+ `normalizeApiError`, and `withApiErrorHandling` ([#870](https://github.com/TerrenoLabs/terreno/pull/870))
1895
+ - `SelectField` / `WebDropdownMenu`: type-to-filter searchable dropdown
1896
+ ([#615](https://github.com/TerrenoLabs/terreno/pull/615))
1897
+ - Demo AI palette generator (`/palette`) with WCAG contrast checks ([#863](https://github.com/TerrenoLabs/terreno/pull/863))
1898
+
1899
+ ### Changed
1900
+
1901
+ - Add repo subagents and upgrade rulesync to v9 ([#892](https://github.com/TerrenoLabs/terreno/pull/892))
1902
+ - Include run evidence (screenshots/videos) in PRs via submit and pour skills
1903
+ ([#871](https://github.com/TerrenoLabs/terreno/pull/871))
1904
+
1905
+ ### Fixed
1906
+
1907
+ - `@terreno/api`: return `401` (not `500`) for auth failures in `bun build --compile`
1908
+ binaries ([#894](https://github.com/TerrenoLabs/terreno/pull/894))
1909
+ - Website: prune docs versions correctly so `versions.json` stays valid ([#866](https://github.com/TerrenoLabs/terreno/pull/866))
1910
+
1911
+ ## [0.24.0] - 2026-07-03
1912
+
1913
+ ### Added
1914
+
1915
+ - `AiSuggestionBox`: `hidden` suggestion status, condensed collapsed states, race-safe
1916
+ expansion, and refreshed sparkles/thumbs visuals ([#865](https://github.com/TerrenoLabs/terreno/pull/865))
1917
+
1918
+ ### Changed
1919
+
1920
+ - Test coverage and explicit-any remediation commits ([#852](https://github.com/TerrenoLabs/terreno/pull/852), [#851](https://github.com/TerrenoLabs/terreno/pull/851), [#861](https://github.com/TerrenoLabs/terreno/pull/861), [#862](https://github.com/TerrenoLabs/terreno/pull/862))
1921
+
1922
+ ## [0.23.1] - 2026-07-02
1923
+
1924
+ ### Added
1925
+
1926
+ - Architectural PR review workflow ([#844](https://github.com/TerrenoLabs/terreno/pull/844))
1927
+
1928
+ ### Changed
1929
+
1930
+ - Document Cursor Cloud dev-environment setup for example-backend MongoDB ([#845](https://github.com/TerrenoLabs/terreno/pull/845))
1931
+ - Demo Appium CI non-blocking unless a mobile build feature changes ([#846](https://github.com/TerrenoLabs/terreno/pull/846))
1932
+ - Consolidate shared dependencies into Bun catalog ([#848](https://github.com/TerrenoLabs/terreno/pull/848))
1933
+ - Align dev API port with docs; clear required on warning ([#842](https://github.com/TerrenoLabs/terreno/pull/842))
1934
+
1935
+ ### Fixed
1936
+
1937
+ - Correct example-backend consent enum snapshot ordering ([#628](https://github.com/TerrenoLabs/terreno/pull/628))
1938
+ - Fix lint regression and iOS Appium smoke timeout ([#843](https://github.com/TerrenoLabs/terreno/pull/843))
1939
+ - Fix E2E todos web server startup in CI ([#811](https://github.com/TerrenoLabs/terreno/pull/811))
1940
+ - Resolve UI and demo formatting CI failures ([#789](https://github.com/TerrenoLabs/terreno/pull/789))
1941
+ - Stop Expo fingerprint churn forcing a native build on every PR ([#849](https://github.com/TerrenoLabs/terreno/pull/849))
1942
+ - `Button`: use transparent background for ghost variant ([#858](https://github.com/TerrenoLabs/terreno/pull/858))
1943
+
1944
+ ## [0.23.0] - 2026-06-26
1945
+
1946
+ ### Added
1947
+
1948
+ - `@terreno/ui` compound components expose predictable dot-suffixed test IDs ([#832](https://github.com/TerrenoLabs/terreno/pull/832), [#840](https://github.com/TerrenoLabs/terreno/pull/840))
1949
+ - Admin Script Runner CLI (`runScriptCli`, declared `args` on scripts) ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
1950
+ - Consent response list/read populate `userId`; `ConsentResponseViewer` user section
1951
+ ([#833](https://github.com/TerrenoLabs/terreno/pull/833))
1952
+
1953
+ ### Changed
1954
+
1955
+ - Document `GCP_SA_*` service account secrets in cloud agent instructions ([#829](https://github.com/TerrenoLabs/terreno/pull/829))
1956
+ - Add example-backend Script Runner CI workflow and CLI docs ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
1957
+
1958
+ ### Fixed
1959
+
1960
+ - Equalize `MarkdownEditor` edit and preview pane heights ([#831](https://github.com/TerrenoLabs/terreno/pull/831))
1961
+
1962
+ ## [0.22.2] - 2026-06-24
1963
+
1964
+ ### Fixed
1965
+
1966
+ - `@terreno/api`: declare `@terreno/test` as a runtime dependency so
1967
+ `@terreno/api/testing` resolves under isolated installs ([#827](https://github.com/TerrenoLabs/terreno/pull/827))
1968
+
1969
+ ### Changed
1970
+
1971
+ - Publish `@terreno/feature-flags` for the first time since `0.21.0`; all packages
1972
+ move to `0.22.2` to stay in lockstep
1973
+
1974
+ ## [0.22.1] - 2026-06-24
1975
+
1976
+ ### Fixed
1977
+
1978
+ - `@terreno/test`: pin `qs` to `^6.14.1` instead of a missing `catalog:` entry so
1979
+ the publish workflow succeeds ([#824](https://github.com/TerrenoLabs/terreno/pull/824))
1980
+
1981
+ ### Changed
1982
+
1983
+ - Publish `@terreno/test`, `@terreno/ai`, `@terreno/admin-backend`, and
1984
+ `@terreno/feature-flags` for the first time at `0.22.1`; all packages move to
1985
+ `0.22.1` to stay in lockstep
1986
+
1987
+ ## [0.22.0] - 2026-06-24
1988
+
1989
+ ### Added
1990
+
1991
+ - New `@terreno/test` package: shared Bun test helpers and in-memory MongoDB fixtures
1992
+ ([#822](https://github.com/TerrenoLabs/terreno/pull/822), [#823](https://github.com/TerrenoLabs/terreno/pull/823))
1993
+ - `Button`: `ghost` variant and `sm` size; fix `outline` button height ([#816](https://github.com/TerrenoLabs/terreno/pull/816))
1994
+ - `OpenApiMiddlewareBuilder.withOperationId()` for custom OpenAPI `operationId` ([#815](https://github.com/TerrenoLabs/terreno/pull/815))
1995
+
1996
+ ### Changed
1997
+
1998
+ - Add SyncDB local-first data layer plan and tasks ([#739](https://github.com/TerrenoLabs/terreno/pull/739))
1999
+
2000
+ ## [0.21.0] - 2026-06-22
2001
+
2002
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.21.0.md`](mcp-server/src/docs/upgrades/0.21.0.md).
2003
+
2004
+ ### Changed
2005
+
2006
+ - **Breaking:** `setupServer` removed — use `TerrenoApp` instead ([#795](https://github.com/TerrenoLabs/terreno/pull/795))
2007
+ - **Breaking:** JSON object responses include `requestId` in the body ([#793](https://github.com/TerrenoLabs/terreno/pull/793))
2008
+
2009
+ ### Added
2010
+
2011
+ - Admin UI v2 backend: `schemaVersion: 2` config, bulk-patch, background tasks ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
2012
+ - Traceable API logging: `createScopedLogger`, `createFeatureFlaggedLogger` ([#799](https://github.com/TerrenoLabs/terreno/pull/799))
2013
+ - `Card` redesign: `container` and `display` variants ([#375](https://github.com/TerrenoLabs/terreno/pull/375))
2014
+ - MCP documentation search: `terreno_search_docs`, `terreno_get_component_docs` ([#796](https://github.com/TerrenoLabs/terreno/pull/796))
2015
+ - `@terreno/rtk` exports `devStore` utilities ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
2016
+
2017
+ ### Fixed
2018
+
2019
+ - Unblock Dependabot by upgrading `@terreno/ai` multer dependency ([#803](https://github.com/TerrenoLabs/terreno/pull/803))
2020
+ - Fix CI: install workspace deps before Trigger EAS Workflow dispatch ([#805](https://github.com/TerrenoLabs/terreno/pull/805))
2021
+
2022
+ ## [0.20.2] - 2026-06-17
2023
+
2024
+ ### Changed
2025
+
2026
+ - Normalize `Badge` height to 20px; add Badge vs SelectBadge demo ([#751](https://github.com/TerrenoLabs/terreno/pull/751))
2027
+
2028
+ ## [0.20.1] - 2026-06-16
2029
+
2030
+ ### Added
2031
+
2032
+ - `@terreno/ui`: support registering custom icons ([#771](https://github.com/TerrenoLabs/terreno/pull/771))
2033
+ - Expo skills for AI agents ([#777](https://github.com/TerrenoLabs/terreno/pull/777))
2034
+ - Admin UI v2 IP with Django-style `home.slots` ([#775](https://github.com/TerrenoLabs/terreno/pull/775))
2035
+ - `@terreno/ai`: export `./parseAiJson` subpath ([#783](https://github.com/TerrenoLabs/terreno/pull/783))
2036
+
2037
+ ### Changed
2038
+
2039
+ - Normalize skill descriptions to single-line UI summaries ([#772](https://github.com/TerrenoLabs/terreno/pull/772))
2040
+
2041
+ ### Fixed
2042
+
2043
+ - Restore demo CI ordering checks ([#770](https://github.com/TerrenoLabs/terreno/pull/770))
2044
+ - Fix Appium dev-client smoke tests on Android and iOS ([#773](https://github.com/TerrenoLabs/terreno/pull/773))
2045
+
2046
+ ## [0.16.0] - 2026-06-02
2047
+
2048
+ Historical `@terreno/api` release notes preserved from the former
2049
+ `api/CHANGELOG.md` (packages now version in lockstep from 0.20.0 onward).
2050
+
2051
+ ### Added
2052
+
2053
+ - **`modelRouter` actions** — `instanceActions` and `collectionActions` on
2054
+ `ModelRouterOptions` for named operations at `/resource/:id/action` and
2055
+ `/resource/action`
2056
+ - **`loadDocOr404`** — shared document loader used by permission middleware and
2057
+ instance actions (soft-delete-aware 404 metadata preserved)
2058
+
2059
+ ### Changed
2060
+
2061
+ - Permission middleware doc loading delegates to `loadDocOr404` (behavior-preserving)
2062
+
2063
+ ### Dependencies
2064
+
2065
+ - Added `@asteasolutions/zod-to-openapi` ^8.5.0
2066
+ - Added **`zod` ^4.3.6 as a peer dependency** for backends defining action Zod
2067
+ schemas
2068
+
2069
+ ### Migration
2070
+
2071
+ - Regenerate frontend SDKs after adding actions; `operationId` values follow
2072
+ `{tag}_{actionName}` (e.g. `todos_markComplete`)
2073
+
2074
+ ## [0.20.0] - 2026-06-14
2075
+
2076
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.20.0.md`](mcp-server/src/docs/upgrades/0.20.0.md).
2077
+
2078
+ ### Changed
2079
+
2080
+ - **Breaking:** `@terreno/api` `ConfigurationApp`: `POST {basePath}/list-secrets` is
2081
+ now read-only validation/status — it no longer resolves or returns secret values;
2082
+ `PATCH {basePath}` strips `secret: true` fields ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
2083
+ - **`configurationPlugin` no longer adds the `_singleton` unique index by default**
2084
+ — opt in via `enforceSingletonIndex: true`
2085
+ - **`configurationPlugin` singleton semantics are soft-delete aware**
2086
+ - **`configurationPlugin.updateConfig` applies updates via `findOneAndUpdate({$set})`
2087
+ with dotted paths** instead of `Object.assign` + `doc.save()`
2088
+ - Prefix `terreno-planning` Cursor plugin skills with `terreno-` ([#764](https://github.com/TerrenoLabs/terreno/pull/764))
2089
+ - Submit skill: merge-first PR body updates ([#765](https://github.com/TerrenoLabs/terreno/pull/765))
2090
+
2091
+ ### Added
2092
+
2093
+ - `@terreno/api` configuration and secret upgrades: `CompositeSecretProvider`,
2094
+ `CachingSecretProvider`, pluggable `permissions`, `preUpdate`/`postUpdate` hooks,
2095
+ optional `version` on secret resolution ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
2096
+ - OpenFeature migration for feature flags: `MongoFeatureFlagProvider`,
2097
+ `GET …/flagConfiguration`, `useTerrenoFeatureFlags` hook ([#761](https://github.com/TerrenoLabs/terreno/pull/761))
2098
+ - `SecretProvider.getSecret(secretName, version?)` and `flattenToDotPaths` export
2099
+
2100
+ ### Deprecated
2101
+
2102
+ - Legacy `GET …/evaluate` feature-flag endpoint (sends `Deprecation`/`Sunset` headers)
2103
+ ([#761](https://github.com/TerrenoLabs/terreno/pull/761))
2104
+
2105
+ [57.0.0]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0
2106
+ [57.0.0-beta.1]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0-beta.1
2107
+ [56.0.0-beta.2]: https://github.com/TerrenoLabs/terreno/releases/tag/56.0.0-beta.2
2108
+ [0.30.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.30.0
2109
+ [0.29.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.29.0
2110
+ [0.28.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.28.0
2111
+ [0.27.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.27.0
2112
+ [0.26.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.26.0
2113
+ [0.25.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.25.0
2114
+ [0.24.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.24.0
2115
+ [0.23.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.1
2116
+ [0.23.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.0
2117
+ [0.22.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.2
2118
+ [0.22.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.1
2119
+ [0.22.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.0
2120
+ [0.21.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.21.0
2121
+ [0.20.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.2
2122
+ [0.20.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.1
2123
+ [0.20.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.0
2124
+ [0.16.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.16.0