@terreno/jobs 57.17.0 → 57.19.2

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.
Files changed (2) hide show
  1. package/CHANGELOG.md +1681 -0
  2. package/package.json +3 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,1681 @@
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.19.2] - 2026-10-26
24
+
25
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.19.2.md`](mcp-server/src/docs/upgrades/57.19.2.md).
26
+
27
+ 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.
28
+
29
+ ### Added
30
+
31
+ - `@terreno/test` gains the test helpers Flourish built and proved in its own suites:
32
+
33
+ - `waitUntil` / `waitForCalls` to poll a condition instead of sleeping, and `freezeTime` to
34
+ freeze `Date.now()`, `new Date()`, and Luxon together.
35
+ - `authWithToken` for a supertest agent with a pre-minted token (no login round-trip),
36
+ `expectAPIError` for `APIError` bodies, and `assertStrictResponseProperties` to fail on any
37
+ response field outside an allowlist.
38
+ - Opt-in `mock.module` registrars for Cloud Tasks (with `drainTasks`), SendGrid, Twilio, Expo
39
+ push, GCS, Google auth, and the `@terreno/api` notifiers, plus `createMockLanguageModel` for
40
+ the AI SDK.
41
+ - `@terreno/test/expo/*`: `registerExpoTestEnvironment`, `renderWithTerreno`, in-memory auth
42
+ token storage, and a module-alias mocker for Bun + Expo tests. `example-frontend` now uses it.
43
+ - `terreno-test-shard` (weighted test sharding) and `terreno-test-hygiene` (AST lint for leaky
44
+ or flaky Bun test patterns, with a baseline) CLIs.
45
+ - Testing skills for Terreno apps, installable with the other agent skills: `terreno-test-setup`
46
+ bootstraps backend (Bun + in-memory Mongo), frontend (Bun + React Native Testing Library,
47
+ Playwright), and mobile (Maestro) test setups; `terreno-backend-tests`,
48
+ `terreno-frontend-tests`, and `terreno-mobile-tests` cover writing tests for each. New how-to:
49
+ [Set up tests in a Terreno app](docs/how-to/set-up-app-tests.md).
50
+ - `@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)
51
+ - Published packages now include the repository `CHANGELOG.md` in their tarballs. (#1632)
52
+
53
+ ### Changed
54
+
55
+ - `@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).
56
+
57
+ 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.
58
+
59
+ ### Fixed
60
+
61
+ - `create-terreno-app` did nothing when run through its package bin (`bunx create-terreno-app`,
62
+ `npx create-terreno-app`): the entry check compared the module URL with the unresolved
63
+ `node_modules/.bin` symlink. It now resolves the path first.
64
+ - `@terreno/test` now installs and imports outside the monorepo. `supertest`,
65
+ `@types/supertest`, and the `mongodb-memory-server` packages moved from
66
+ `devDependencies` to `dependencies` (57.17.0 failed with `Cannot find package
67
+ 'supertest'` on import), and the published tarball no longer ships test files.
68
+ - `registerSimpleMongoPreload` now passes `TERRENO_TEST_MONGO_LAUNCH_TIMEOUT_MS` (default 60s) to
69
+ `mongodb-memory-server`, so a cold `mongod` start no longer fails at the library's 10s default.
70
+
71
+ ## [57.18.0] - 2026-10-25
72
+
73
+ ### Added
74
+
75
+ - 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`.
76
+ - `@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.
77
+ - `@terreno/syncdb`: `createSyncDb({socketTransports})` does the same for the default socket transport.
78
+ - New `deploy-vercel` agent skill.
79
+
80
+ ### Fixed
81
+
82
+ - `@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.
83
+
84
+ ## [57.17.0] - 2026-10-23
85
+
86
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.17.0.md`](mcp-server/src/docs/upgrades/57.17.0.md).
87
+
88
+ ### Breaking
89
+
90
+ - `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).
91
+ - `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).
92
+ - 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).
93
+
94
+ ### Added
95
+
96
+ - `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.
97
+ - `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)
98
+ - `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.
99
+ - `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.
100
+ - `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).
101
+ - `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).
102
+ - `getBetterAuthBearerToken({storagePrefix?, bearerStorage?})` reads that stored token without a network call.
103
+ - `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.
104
+ - 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).
105
+ - `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).
106
+ - `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).
107
+ - `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.
108
+ - `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.
109
+ - `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.
110
+ - `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.
111
+ - `@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.
112
+ - `emptySplitApi` sends the registered source's token.
113
+ - `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).
114
+ - `WhatsNewBanner` in `@terreno/ui` shows non-blocking release notes, such as "What's new
115
+ in 57.16.2", until the viewer opens or dismisses it, then hidden until a newer release. See
116
+ [Show release notes with WhatsNewBanner](docs/how-to/whats-new-banner.md).
117
+ - The example app's what's new banner lists the 10 most recent `CHANGELOG.md` releases.
118
+ `bun run whats-new:generate` writes them, `changelog:assemble` refreshes them on each
119
+ release, and CI fails through `bun run check:whats-new` when they are stale.
120
+ - `BannerButton` accepts an optional `testID`.
121
+
122
+ ### Changed
123
+
124
+ - `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)
125
+ - 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)
126
+ - The Better Auth socket handshake creates the app `User` on first contact, as the REST session middleware does, and refuses disabled users. (#1576)
127
+ - 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)
128
+ - 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)
129
+ - 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.
130
+ - The example backend seeds its staff modal and patient banner announcements as drafts, so a
131
+ fresh database (local dev or a PR preview) no longer interrupts sign-in. Publish them from
132
+ **Admin → Announcements** to try them.
133
+ - `@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.
134
+ - 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`.
135
+
136
+ ### Fixed
137
+
138
+ - `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.
139
+ - 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)
140
+ - 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.
141
+ - 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.
142
+ - 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.
143
+ - `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.
144
+ - `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.
145
+ - `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.
146
+
147
+ ## [57.16.2] - 2026-10-13
148
+
149
+ ### Fixed
150
+
151
+ - Light-mode primary button labels, icons, and loading indicators are white again,
152
+ including on disabled primary buttons. (#1567)
153
+
154
+ ## [57.16.1] - 2026-10-12
155
+
156
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.16.1.md`](mcp-server/src/docs/upgrades/57.16.1.md).
157
+
158
+ ### Added
159
+
160
+ - `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).
161
+
162
+ ### Changed
163
+
164
+ - `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.
165
+
166
+ ### Fixed
167
+
168
+ - `TextField` and `TextArea` on web no longer drop a typo fix made within half a second. The
169
+ oscillation guard used to suppress any change that undid the previous one within 500ms, so
170
+ typing a character and backspacing it right away put the character back. Changes the user
171
+ types, deletes, pastes, drops or undoes (judged by the DOM input event's `inputType`) now
172
+ bypass the guard. Spellcheck replacements, IME composition and changes with no input event
173
+ stay guarded.
174
+
175
+ ## [57.15.0] - 2026-10-11
176
+
177
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.15.0.md`](mcp-server/src/docs/upgrades/57.15.0.md).
178
+
179
+ ### Changed
180
+
181
+ - SyncDB bootstraps and reconciles every stream through one `POST /sync/snapshot/batch` request
182
+ per round instead of one `GET /sync/snapshot` per stream. A reconnect with nothing new now costs
183
+ 2 requests however many collections the app syncs. The client sends the next round while it
184
+ writes the current one and writes rows in chunks of 500 so the UI keeps responding. Clients fall
185
+ back to per-stream paging against servers without the batch route. The web IndexedDB persister
186
+ no longer serializes the whole store once per transaction: it serializes once per write.
187
+
188
+ ## [57.14.0] - 2026-10-11
189
+
190
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.14.0.md`](mcp-server/src/docs/upgrades/57.14.0.md).
191
+
192
+ ### Changed
193
+
194
+ - `@terreno/syncdb` now pages snapshot streams side by side instead of one at a time, so cold
195
+ start no longer costs one sequential round trip per stream. Reconcile, subscribe catch-up,
196
+ `forceResync` and wipe re-bootstrap run up to `snapshotConcurrency` streams at once (new
197
+ `createSyncDb` option, default 6; `1` restores the old behavior). Each stream now runs one pass
198
+ at a time: a subscribe catch-up queues behind a pass that started before the room join, and a
199
+ force resync waits for the stream's in-flight pass before purging it, so a concurrent pass can
200
+ no longer mark a purged stream bootstrapped over rows it never paged. A new how-to,
201
+ [Use syncdb correctly](docs/how-to/use-syncdb-correctly.md), lists the library behavior
202
+ apps have to handle.
203
+
204
+ ## [57.13.0] - 2026-10-10
205
+
206
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.13.0.md`](mcp-server/src/docs/upgrades/57.13.0.md).
207
+
208
+ ### Added
209
+
210
+ - 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).
211
+ - `@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`.
212
+ - `@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.
213
+ - `@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.
214
+ - 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.
215
+ - `@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)
216
+ - `@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)
217
+ - `@terreno/admin-frontend` `DocumentStorageBrowser` takes `backButton` and shows upload errors inline, including a 429 "Upload limit reached" message. (#1499)
218
+ - `@terreno/ui` `SegmentedControl` takes `size="sm"` (28px, sized to its labels). (#1499)
219
+
220
+ ### Changed
221
+
222
+ - 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)
223
+ - 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).
224
+ - `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.
225
+ - `Image` with `alt` now sets it as the accessible label and makes the image its own screen-reader element.
226
+ - `table` blocks show column names with underscores as spaces, size to their rows, and show at most 10 rows before the body scrolls.
227
+ - 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)
228
+
229
+ ### Fixed
230
+
231
+ - On web, an open toast no longer blocks clicks on the rest of the page at the same height.
232
+ The toast's `SafeAreaView` and the `Toast` layout wrapper span the full width (up to 900px)
233
+ around the visible toast and swallowed clicks there. Both now use `pointerEvents: "box-none"`,
234
+ so only the visible toast is interactive.
235
+ - `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)
236
+ - `Tooltip` on web no longer mounts a full-screen layer over the app while it is open.
237
+ The bubble now renders into `document.body` with `position: fixed` instead of through
238
+ the `PortalHost` layer, so nothing covers the page and the content below stays
239
+ clickable. Web tooltips also use `zIndex: 9999`, matching the body-level overlays
240
+ (`Modal`, `DropdownPanel`), so tooltips inside a modal show on top of it. Native keeps
241
+ the `PortalHost` path.
242
+
243
+ ## [57.12.0] - 2026-10-07
244
+
245
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.12.0.md`](mcp-server/src/docs/upgrades/57.12.0.md).
246
+
247
+ ### Added
248
+
249
+ - `@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.
250
+
251
+ ### Changed
252
+
253
+ - 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`).
254
+ - 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()`.
255
+
256
+ ### Fixed
257
+
258
+ - 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.
259
+
260
+ ## [57.11.0] - 2026-10-05
261
+
262
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.11.0.md`](mcp-server/src/docs/upgrades/57.11.0.md).
263
+
264
+ ### Added
265
+
266
+ - `@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).
267
+ - `@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.
268
+ - `@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.
269
+ - `useQuery` / `useEntityIds` accept the same Mongo-style `where`, string sort specs, and `limit` for local queries.
270
+ - Window refetches learn the route's `maxLimit` and pipeline page requests with a `console.warn` instead of silently truncating.
271
+ - `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`.
272
+ - `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.
273
+
274
+ ### Changed
275
+
276
+ - `@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).
277
+
278
+ ### Fixed
279
+
280
+ - 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.
281
+ - `@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.
282
+ - 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.
283
+
284
+ ## [57.10.1] - 2026-10-04
285
+
286
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.10.1.md`](mcp-server/src/docs/upgrades/57.10.1.md).
287
+
288
+ ### Breaking
289
+
290
+ - 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.
291
+
292
+ ### Added
293
+
294
+ - 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`.
295
+ - 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.
296
+ - 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`).
297
+ - 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.
298
+ - `/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.
299
+ - `@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.
300
+ - 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.
301
+ - 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`.
302
+ - `@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.
303
+ - `@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.
304
+ - `@terreno/ui` `BlocksView` paints a document. `GPTChat` `uiBlocks` renders assistant messages through that view, including a spinner while the reply is still streaming.
305
+ - 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.
306
+ - `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.
307
+ - `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`.
308
+ - 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.
309
+ - 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.
310
+ - `@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`.
311
+ - `@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`.
312
+
313
+ ### Fixed
314
+
315
+ - `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.
316
+
317
+ ## [57.9.0] - 2026-10-03
318
+
319
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.9.0.md`](mcp-server/src/docs/upgrades/57.9.0.md).
320
+
321
+ ### Added
322
+
323
+ - `@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.
324
+ - 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.
325
+ - `/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.
326
+ - `/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"`.
327
+ - 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.
328
+ - `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.
329
+
330
+ ### Changed
331
+
332
+ - `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.
333
+ - `GPTChat` shows "Scroll to bottom" only when content sits below the viewport, never in an empty chat.
334
+ - The `GPTChat` composer grows with long text up to 200px, then scrolls. `TextField` / `TextArea` accept `maxHeight` to cap `grow`.
335
+ - Generated images in `GPTChat` offer copy-image and (web) download. Copying an image-only reply copies the image instead of text.
336
+ - `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.
337
+
338
+ ### Fixed
339
+
340
+ - Web toasts now render above open modals and remain correctly centered in statically exported apps instead of inheriting a zero-width server-rendered viewport.
341
+
342
+ ## [57.8.0] - 2026-10-02
343
+
344
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.8.0.md`](mcp-server/src/docs/upgrades/57.8.0.md).
345
+
346
+ ### Added
347
+
348
+ - `@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.
349
+ - `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.
350
+
351
+ ### Changed
352
+
353
+ - 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.
354
+ - `deploy-demo` runs alongside `publish-release` instead of after it.
355
+ - `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).
356
+
357
+ ### Deprecated
358
+
359
+ - `isMobileDevice` is deprecated. Call `isNarrowViewport` instead. The old function stays exported and returns the same result until it is removed.
360
+
361
+ ### Fixed
362
+
363
+ - `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.
364
+ - `/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.
365
+
366
+ ## [57.7.0] - 2026-09-28
367
+
368
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.7.0.md`](mcp-server/src/docs/upgrades/57.7.0.md).
369
+
370
+ ### Changed
371
+
372
+ - 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`.
373
+ - `@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.
374
+ - `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.
375
+ - `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.
376
+ - `DropdownPanel` triggers are now styleable: `triggerVariant` (any `Button` variant), `applyButtonVariant`, `triggerSize` on labeled triggers, `fullWidth`, and `renderTrigger` for a fully custom trigger.
377
+ - `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.
378
+ - New `bun run check:test-isolation` blocks new `mock.module` calls in shared Bun test suites (see `docs/explanation/test-isolation.md`).
379
+ - 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.
380
+ - 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.
381
+
382
+ ## [57.6.1] - 2026-09-25
383
+
384
+ ### Fixed
385
+
386
+ - `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`.
387
+
388
+ ## [57.6.0] - 2026-09-24
389
+
390
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.6.0.md`](mcp-server/src/docs/upgrades/57.6.0.md).
391
+
392
+ ### Added
393
+
394
+ - `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).
395
+ - Add full SyncDB support to `terreno-mcp-local`: inspect debugger and local-store
396
+ state, capture/compare/merge snapshots, mutate or directly edit local entities,
397
+ flush the outbox, reconcile/resync, resolve conflicts, retry failures, and
398
+ exercise offline transitions. State-changing operations require
399
+ `TERRENO_MCP_EVAL=1`; returned state redacts sensitive fields.
400
+ - 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.
401
+
402
+ ### Changed
403
+
404
+ - 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.
405
+ - 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`.
406
+
407
+ ### Fixed
408
+
409
+ - Framework Mongoose models added in 57.3 and 57.4 now use `Terreno*` model names
410
+ without changing their MongoDB collections, preventing collisions with consumer
411
+ models such as `Notification`, `Membership`, and `Job`. Admin UI keys and
412
+ `admin<ModelName>` RBAC resources keep the pre-namespace names. JWT recovery
413
+ routes can be disabled with `authOptions.passwordReset`, `emailVerification`,
414
+ and `legacyResetPasswordRoute`.
415
+
416
+ ## [57.4.0] - 2026-09-22
417
+
418
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.4.0.md`](mcp-server/src/docs/upgrades/57.4.0.md).
419
+
420
+ ### Added
421
+
422
+ - 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`.
423
+ - 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.
424
+ - `@terreno/admin-frontend` now ships `adminRequest`, a native `fetch` helper (timeout, JSON/`FormData`, credentials) for admin RPC that is leaving RTK `injectEndpoints`.
425
+ - Example admin Todos now allow admin delete, and Playwright covers Todo create → edit → delete on embedded `/admin` and SPA `/console`.
426
+ - Built-in admin String-`_id` model CRUD can now run windowed local-first through
427
+ `@terreno/syncdb`: REST supplies membership, TinyBase supplies live rows,
428
+ create/update/delete use the durable outbox, bulk patch stays server-side, and
429
+ `ConflictSheet` resolves admin-loaded records. The embedded example and
430
+ standalone admin SPA demonstrate Bearer and same-origin cookie hosts.
431
+ - `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.
432
+ - `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`.
433
+ - `@terreno/ui` now ships owned-SVG `LineChart`, `BarChart`, `AreaChart`, and `DonutChart`
434
+ plus an eager `DashboardGrid` for wrapping caller `Card`s. Charts use `react-native-svg`
435
+ and private d3 helpers — not `victory-native`. Each chart sizes itself to its container
436
+ and clips to it, so a chart never widens the card it sits in, and `height` covers the whole
437
+ chart including its tick and tooltip rows. `bun run ui:charts:compare` diffs actually
438
+ rendered gallery PNGs against `demo/rendered-snapshots/` (not JSON snapshots).
439
+ Conditional `DashboardGrid` children no longer reserve blank cells, and an open chart
440
+ tooltip now updates or closes when live data changes.
441
+ - `create-terreno-app` — unscoped npm CLI (`bunx create-terreno-app` /
442
+ `npm create terreno-app`) that scaffolds a deployable full-stack Terreno app.
443
+ Published in lockstep with `@terreno/*`; generated scaffolds pin
444
+ `^<create-terreno-app version>` on Terreno dependencies.
445
+ - **@terreno/ui `DataTable`**: optional server-side search and column filters emit
446
+ modelRouter-shaped query params via `onQueryChange` and `buildDataTableListQuery`
447
+ (web per-column `Filter` popovers; native Filters sheet).
448
+ - **@terreno/admin-frontend `AdminModelTable`**: adopts DataTable filter/search UI;
449
+ list search still uses `q`; choice filters support multi-value `$in`. Admin list
450
+ URLs use `qs` bracket serialization so nested filters work with any RTK base query.
451
+ - **@terreno/admin-backend**: `parseAdminListFilters` accepts choice `{$in: string[]}`
452
+ and escaped-literal text `{$regex, $options: "i"}` while rejecting extra operators.
453
+ Optional choice filters auto-enable an **Empty** option (`__empty__` wire sentinel → Mongo
454
+ `null`) when the Mongoose field is not required; explicit `allowEmpty` overrides.
455
+ Empty and Empty-plus-value filters remain normalized when combined with toolbar search.
456
+ - **@terreno/api**: OpenAPI query validation accepts choice scalars, `{$in: [...]}`, and
457
+ the empty sentinel alongside enum values. Nested query operators reject extra keys and
458
+ executable regex patterns; `queryFilter` can remove consumed wire keys with `undefined`.
459
+ - **@terreno/ui `buildDataTableListQuery`**: single concrete choice values emit scalar
460
+ equality (validator-friendly); empty-only and empty+concrete use `$in` with `__empty__`.
461
+ - **@terreno/ui `Filter`**: new `iconOnly` and `triggerSize` props render a compact
462
+ icon trigger for dense chrome; DataTable column headers use it.
463
+ - **@terreno/ui `DataTable`**: single-column filter popovers no longer duplicate the
464
+ popover's own Clear with a per-field **Clear filter**; the boolean per-field clear
465
+ now appears only where one surface hosts several filters.
466
+ - **@terreno/ui `DataTable`**: date range filters use date inputs, so entering a
467
+ calendar date filters immediately and either bound works on its own. Previously the
468
+ datetime input emitted nothing until an hour and minute were also entered. The **to**
469
+ bound now closes the chosen UTC day instead of landing on midnight, so rows recorded
470
+ later that day stay in range.
471
+ - Opt-in append-only `AuditEvent` log. Register `AuditApp` and set `audit: true` (or
472
+ `{redact: ["ssn"]}`) on `modelRouter` so successful HTTP CRUD and array mutations
473
+ persist redacted changed-field diffs. Default redaction includes compound keys such as
474
+ `tokenHash`. AdminApp writes the same collection when the plugin is registered and omits
475
+ each model's `hiddenFields` / `excludeFields`; RBAC uses
476
+ `persistRbacAuditToAuditEvent`. List/read is admin-only at `GET /audit-events` and
477
+ `/admin/audit-events`; create/update/delete over HTTP return 405. Persist is
478
+ fire-and-forget; a non-24-hex `actorId` (including 12-character strings mongoose would
479
+ accept) is dropped from the event instead of dropping the event. Update/delete snapshots
480
+ 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
481
+ instead of writing Mongo on the request. Default retention is forever; `retentionDays`
482
+ adds a Mongo TTL index on `created`. See `docs/how-to/audit-log.md`.
483
+ - # GCP durable-jobs worker infrastructure
484
+
485
+ Infra Manager now owns the example Cloud Tasks queue `terreno-example-jobs-v2`, the
486
+ private `terreno-backend-example-tasks` Cloud Run service, the
487
+ `terreno-backend-runtime` API identity (sole queue enqueuer / jobs-invoker
488
+ `actAs`), and the `terreno-jobs-invoker` OIDC callback identity. CD deploys
489
+ matching PR tags for the API and tasks services. The example API still uses
490
+ `MongoJobRunner` until a follow-up selects `JOBS_RUNNER=gcp-cloud-tasks`. The
491
+ example image compiles `@terreno/jobs` before `@terreno/admin-backend`.
492
+ - `GPTChat` accepts an optional `mascot` React node from the consumer. Terreno does not
493
+ ship a default character. The node renders on an empty chat and hides once messages
494
+ exist. The empty state (mascot plus suggested prompts) stays centered in the chat
495
+ panel inside a scrollable message area, and the composer row centers the
496
+ attachment, tools, and Send controls vertically against the input. The example
497
+ consumer bundles four plant-robot mascots and chooses one on each AI screen mount.
498
+ - Unattended agents can claim one unstarted GitHub issue labeled
499
+ `status:ready-for-dev`, Pick ⇄ Roast a posted plan, and Brew a draft PR via the
500
+ `implement-ready-for-dev` skill. Pickup skips open linked PRs via GraphQL (not
501
+ `linked:<number>`) and aborts if an untrusted author edits the issue body after
502
+ the label. Dashboard paste lives in that skill's `references/cursor-automation.md`.
503
+ Interactive `/work-github-issues` still requires chat confirmation.
504
+ - `@terreno/jobs` adds durable background work for Terreno backends: MongoDB-persisted jobs
505
+ with retries, dead-lettering, cron schedules (IANA timezones), admin list/detail/retry/requeue/cancel,
506
+ and pluggable runners (`MongoJobRunner`, GCP Cloud Tasks, Vercel Queues, custom). Workers start
507
+ only via explicit `startWorker()` — register `JobsApp` on `TerrenoApp`, define handlers, enqueue
508
+ with `getJobsService()`, and choose in-process or standalone worker processes. Admin UI widgets
509
+ ship in `@terreno/admin-frontend`. See `docs/how-to/background-jobs.md` and
510
+ `docs/reference/jobs.md`.
511
+ - 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`.
512
+ - The in-app notification center adds `NotificationsApp` in `@terreno/api` with
513
+ owner-scoped `Notification` and `NotificationPreference`
514
+ models, `getNotificationService().notify()`, `notificationsBeforeSend`, mark-all-read, and
515
+ optional `retainDays` tombstone sweep. `@terreno/ui` provides presentational
516
+ `NotificationBell`, `NotificationInbox`, and `NotificationPreferences` components.
517
+ The example app demonstrates syncdb collections, a todos-header bell that toggles a
518
+ right-side drawer, a full active and archived history page, notification preferences,
519
+ seeded notification examples, todo activity notifications, and a development notify
520
+ route. Inbox list, mark-read, and dismiss (archive via `archivedAt`) use syncdb only;
521
+ `POST /notifications/dev-notify` remains the development send path. `terreno-syncdb-codegen` now accepts hyphenated collection names such as
522
+ `notification-preferences`. Consumers can replace the bell icon and unread bubble with
523
+ `NotificationBell` render props while retaining its layout and accessibility behavior.
524
+ - 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.
525
+ - `terreno-pick-roast-loop` drives an approved plan through all actionable Pick/Roast
526
+ cycles without stopping on ordinary engineering failures. It accumulates task, attempt,
527
+ verification, artifact, docs, and risk evidence into one final report. When a genuine
528
+ human decision is required, it first explains the overall plan state, completed work,
529
+ decisive evidence, options and impact, and its recommendation, then asks one exact
530
+ question. The loop excludes Grow, Brew, Taste, and product-CI monitoring. Cursor,
531
+ Codex, Claude Code, and `npx skills` expose the loop.
532
+ - `@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`.
533
+ - CircleCI `publish-release` and the GitHub `publish-on-tag.yml` fallback now publish `@terreno/announcements` in lockstep with the other `@terreno/*` packages.
534
+ - `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`.
535
+ - 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.
536
+ - `@terreno/syncdb` exports `bridgeBetterAuthReactClient`, which adapts a Better Auth
537
+ **react** client to the session-subscription surface `betterAuthAdapter` watches. Without
538
+ it the adapter cannot see the client's nanostore session atom and falls back to polling
539
+ `getSession()`. The example app and the admin SPA both use it.
540
+ - `TextField` with `type="password"` now renders a show/hide eye control so users can check what
541
+ they typed. The value starts masked, a disabled field cannot be revealed, and
542
+ `showVisibilityToggle={false}` removes the control. The toggle is reachable in tests at
543
+ `{testID}.visibility-toggle` (override with `testIDs.visibilityToggle`). `Field`, `LoginScreen`,
544
+ and `SignUpScreen` password fields inherit it.
545
+ - Dependency updates go through the daily `update-dependencies` skill: one trusted,
546
+ same-repository rolling PR with a landed/failed ledger, a test that imports each
547
+ bumped package, and a freeze on Expo fingerprint changes until a release.
548
+
549
+ ### Changed
550
+
551
+ - `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.
552
+ - Admin RPC hooks dual-run: hosts that inject `credentials` / `getAuthHeaders` on `AdminProvider` use native `adminRequest`; hosts that pass only `api` keep RTK `injectEndpoints`.
553
+ - Admin panel HTTP script runs enqueue durable job `admin/script` when `JobsApp` is registered.
554
+ The Scripts UI still polls `BackgroundTask`. If runner enqueue throws, the HTTP handler
555
+ marks that `BackgroundTask` failed and returns 500. CLI script runs stay in-process. See
556
+ `docs/how-to/background-jobs.md`.
557
+ - 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`).
558
+ - CircleCI uses smaller Docker classes for Playwright e2e shards, skips empty
559
+ Netlify/GCP contexts before `bun install`, and turns off Docker Layer Caching.
560
+ - The `terreno-planning` plugin now includes reusable Terreno backend/API, UI, data,
561
+ schema, SDK, admin, prompt-governance, documentation, upgrade, deployment, and UI
562
+ verification skills alongside the lifecycle. It also ships `pre-commit` and
563
+ `ui-verifier` agents. The redundant `commit` and `create-pr` skills are removed in
564
+ favor of Brew. Conflicting or unused Expo skills (`native-data-fetching`,
565
+ `building-native-ui`, `expo-ui`, App Clip, brownfield, Observe, Tailwind setup,
566
+ EAS update insights, and Expo module authoring) are no longer distributed. Taste's
567
+ pre-push gate runs lint, typecheck, and affected tests for every supported host.
568
+ Rulesync now also generates native stop hooks for Cursor, Claude Code, GitHub Copilot,
569
+ and Devin that run repository lint and typecheck, emit each host's blocking JSON
570
+ protocol, and avoid repeated checks on structured Stop-hook retries. Plugin
571
+ `terreno-planning` is `2.8.0`.
572
+ - 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).
573
+ - The deployed example backend selects `JOBS_RUNNER=gcp-cloud-tasks`, verifies Cloud
574
+ Tasks OIDC on `POST /jobs/execute`, and points each PR preview at its matching
575
+ tasks-service tag. GitHub Actions and CircleCI both deploy that tag, and production
576
+ deploys overwrite env vars so preview `MONGO_DB_NAME` / `PR_NUMBER` cannot stick.
577
+ The API
578
+ process starts the schedule ticker; the tasks service does not. The compiled Cloud Run binary enqueues through the Cloud Tasks REST
579
+ API (`google-auth-library`) because `@google-cloud/tasks` cannot load its JSON
580
+ config from a `bun build --compile` image. Enqueued HTTP tasks set
581
+ `dispatchDeadline` to protobuf `{seconds: 1800}` (REST JSON `"1800s"`) so Cloud
582
+ Tasks does not retry a still-running 30-minute handler at the 10-minute HTTP
583
+ default. The REST client encodes that Duration; the optional `@google-cloud/tasks`
584
+ peer receives the protobuf object. Queue and worker service resources live in Infra
585
+ Manager.
586
+ - Grow now ends with a standalone approval brief instead of a 15-line index. It opens
587
+ with a paragraph on where the repository is and where the change takes it, adds
588
+ background on current state when a reviewer needs it, then lays out the idea and the
589
+ plan (tasks, tracer, verification, out of scope, risks). The Decisions table follows
590
+ the plan and now pairs every settled human decision with the question that prompted
591
+ it, so a reviewer can see what was asked as well as what was chosen; it is still
592
+ omitted when grilling settled none. A reviewer should be able to approve from the
593
+ brief alone, without opening the IP, the ticket, or the interview history.
594
+ - `implement-ready-for-dev` now Grow-shapes the claimed issue without a chat pause
595
+ (assume answers unless a genuine human gate), then Pick ⇄ Roast, Brew, and Taste
596
+ to a mergeable PR. Dashboard paste is in that skill's `references/cursor-automation.md`.
597
+ There is no separate `autobot-ready-for-dev` skill.
598
+ - `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.
599
+ - Planning Brew titles are `[ticket] Short feature title`. The ticket uses the attached
600
+ Linear id (`[FH-1632]`) or GitHub issue (`[#412]`); the rest names the feature only.
601
+ Titles must not use `feat:` / `docs:` prefixes or lifecycle labels such as
602
+ `IP Approved` and `Task list`.
603
+
604
+ PR bodies now preserve the IP's initial justification and a brief overview of its
605
+ intended outcomes. Verification always includes reproducible testing instructions;
606
+ Brew updates that section as testing changes while keeping the rationale and overview
607
+ stable.
608
+ - Planning stages and outer loops now close every wait-for-human or done chat with the
609
+ current PR's GitHub Deployment demo URLs as the last visible section when those URLs
610
+ exist. Plugin `terreno-planning` is `2.11.0`.
611
+ - Removed the `terreno_bootstrap_app` MCP tool. The only supported scaffold path
612
+ is `bunx create-terreno-app` / `npm create terreno-app`. MCP still offers
613
+ `terreno_bootstrap_ai_rules` for editor rules after the CLI runs.
614
+ - Roast no longer asks two unconstrained subagents to each rediscover the repository
615
+ (full-branch diff plus skill catalog). Pick, Roast, and Brew pass a task-scoped
616
+ briefing (`plugins/terreno-planning/references/subagent-briefing.md`): this task's
617
+ criteria, file list, and patch. Roast may spawn at most one UI/runtime verifier when
618
+ this task lists UI files, and must not spawn a conventions reviewer. Installable
619
+ lifecycle skills copy only the plugin references they link.
620
+ - Taste records failed tests from the last product-CI run and re-verifies those exact
621
+ local commands before any GitHub push. `prepush` does not substitute for that
622
+ re-verify. Example-frontend now exposes `test:ci` so root `bun run test` includes it.
623
+ Cursor Cloud test mapping lives in `docs/how-to/run-tests-locally.md`. Plugin
624
+ `terreno-planning` is `2.12.0`.
625
+ - Taste now runs a repository root's `prepush` package script, when present, in its fresh
626
+ no-context subagent before pushing. Repositories own the exact local gate; when the
627
+ script is absent, Taste retains its affected-package lint, typecheck, and test fallback.
628
+ Terreno's root `prepush` gate runs workspace lint, TypeScript compilation, and full
629
+ Knip/dependency-cruiser static analysis. Plugin `terreno-planning` is `2.10.0`.
630
+ - The component demo and documentation site now use distinct Terreno Garden app
631
+ icons. The demo home screen also shows a Terreno Garden banner that flows inline
632
+ with the component grid, filling the first two card slots.
633
+
634
+ ### Deprecated
635
+
636
+ - `@terreno/admin-frontend`'s `useAdminApi` RTK `injectEndpoints` path and required
637
+ `api` prop are deprecated. Terreno 57 retains them for ObjectId/API-only
638
+ compatibility; Terreno 58 removes them. New admin RPC uses the host-bound fetch
639
+ client, and eligible String-`_id` model CRUD uses windowed syncdb.
640
+
641
+ ### Removed
642
+
643
+ - The UI component demo no longer includes announcement or in-app notification
644
+ stories. Exercise those surfaces in `example-frontend` instead.
645
+
646
+ ### Fixed
647
+
648
+ - The `cloud_run_service` Terraform module no longer ignores `template.revision`.
649
+ Ignoring it pinned the live revision name in state, so any structural change
650
+ failed with Cloud Run 409 `Revision named '<name>' with different configuration
651
+ already exists`. The Cloud Tasks queue now also waits on the IAM module so the
652
+ first apply cannot 403 on `cloudtasks.queues.create`.
653
+ - `compile-workspace-deps` uses `tsconfig.server.json` when present so
654
+ `@terreno/admin-spa` emits `src/dist` for example-backend coverage.
655
+ - 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.
656
+ - `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.
657
+ - Terraform preview CI now prints Infra Manager `errorCode` / `errorLogs` when
658
+ `previews create` fails, and `terraform-admin` gains `roles/logging.logWriter`
659
+ so Cloud Build can write regional logs.
660
+ - 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.
661
+ - `terreno-pick-roast-loop` resumes only Pick or Roast. A stored `next: brew` finishes
662
+ the loop without launching Brew, Taste, or Grow.
663
+ - example-backend listens on `PORT` before connecting to MongoDB and attaching
664
+ the Terreno Express app. If that boot later throws, the process closes the
665
+ holder and exits so Cloud Run does not keep a 503 listener. GitHub Actions
666
+ preview deploys overwrite Cloud Run revision secrets, smoke-test the same
667
+ `JOBS_*` env as the revision, rebuild traffic from Ready revisions (omitting
668
+ failed tags), deploy `--no-traffic` without `--tag`, then tag the new revision.
669
+ - RBAC permission resolution now keys the in-memory grant cache by user id and
670
+ sorted role names, so promoting a user (for example e2e `setUserAdmin`) takes
671
+ effect on the next `/auth/me` instead of serving a 30s stale set.
672
+ - Conflict resolution with **Keep mine** now preserves the admin-window mutation marker
673
+ when cloning a durable outbox row, so its retry retains AdminApp authorization,
674
+ protected-field stripping, hooks, and audit handling.
675
+ - `Tooltip` no longer disappears the instant it opens on web. The portal layer that
676
+ renders overlays (tooltips, modals, toasts) declared `pointerEvents: "box-none"` in an
677
+ inline style, which react-native-web drops, so every mounted portal covered the app with
678
+ a full-screen overlay that swallowed hover and press events. The portal layer now uses a
679
+ registered `StyleSheet` style, and `Tooltip` stays off screen until its trigger has been
680
+ measured instead of flashing in the top-left corner.
681
+ - `TextField` on React Native Web no longer enters an update-depth loop when
682
+ browser autocorrect alternates controlled values. Web autocorrect/spellcheck is
683
+ disabled, the input handler stays stable, and rapid synthetic A→B→A reversals
684
+ are suppressed without blocking normal typing or delayed backspace/retype.
685
+
686
+ ## [57.3.0] - 2026-09-02
687
+
688
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.3.0.md`](mcp-server/src/docs/upgrades/57.3.0.md).
689
+
690
+ ### Added
691
+
692
+ - The lifecycle stages now ship as a Claude Code plugin. Add the marketplace with
693
+ `/plugin marketplace add TerrenoLabs/terreno`, install with
694
+ `/plugin install terreno@terreno-plugins`, then invoke `/terreno:1-grow`. The marketplace
695
+ name is `terreno-plugins` so it does not collide with the plugin name `terreno` (Claude
696
+ Code's installer breaks when those names match). Claude Code takes a
697
+ plugin skill's command from the frontmatter `name`, so its shortened stage names
698
+ (`1-grow` … `5-taste`) ship as a generated copy at `plugins/terreno-claude/`
699
+ (`bun run skills:sync`). Cursor and `npx skills` are unchanged: plugin
700
+ `terreno-planning`, stages `terreno-1-grow` … `terreno-5-taste`.
701
+ - The lifecycle stages now ship as a Codex plugin. Add the marketplace with
702
+ `codex plugin marketplace add TerrenoLabs/terreno`, install with
703
+ `codex plugin install terreno-planning --source terreno-plugins`, then invoke
704
+ `$terreno-1-grow`. Codex uses the canonical `plugins/terreno-planning/` tree
705
+ (`.codex-plugin/plugin.json`) and the repo marketplace at
706
+ `.agents/plugins/marketplace.json`. Stage names match Cursor and `npx skills`.
707
+ - `TwilioSmsProvider` at `@terreno/comms/adapters/twilioSms` (optional peer `twilio`). Sends
708
+ prefer a messaging service SID over a from-number, require valid E.164 destinations, classify
709
+ Twilio error codes, and store a console deep link on accepted sends. The example backend
710
+ registers the adapter when Twilio env vars are complete.
711
+ - `TwilioVerifyProvider` at `@terreno/comms/adapters/twilioVerify` (optional peer `twilio`).
712
+ Starts and checks SMS/email OTP via a Verify service SID, classifies Twilio errors, redacts
713
+ destinations, never stores codes, and marks verification rows non-retryable. The example
714
+ backend registers the adapter when `TWILIO_VERIFY_SERVICE_SID` is set with account
715
+ credentials.
716
+ - Admin comms dashboard: filter and inspect delivery logs, retry failed sends (including bulk retry
717
+ with a cap), and view per-provider failure rates. Created and attempt timestamps print in the
718
+ operator locale. List, stats, and bulk retry share a trailing 7-day window when dates are omitted.
719
+ Editing a filter while that window is implicit keeps both date bounds. Push
720
+ `beforeSend` cancel returns `loggedMessageId`. Retry returns the log row created by that send. Routes live on `@terreno/comms`; screens ship in
721
+ `@terreno/admin-frontend` as the `comms` custom screen.
722
+ - `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).
723
+
724
+ Exports include `modelDescriptionToOpenApiSpec`, `modelDescriptionToAdminFields`, and `fieldDescriptionToZodType`. See `docs/explanation/schema-metadata.md`.
725
+ - `ExpoPushProvider` at `@terreno/comms/adapters/expoPush` (optional peer
726
+ `expo-server-sdk`). `sendPush` returns one `SendResult` per token, chunks Expo
727
+ payloads, classifies ticket/receipt errors, and polls receipts. `DeviceNotRegistered`
728
+ deactivates `PushToken` rows via `CommsService.deactivatePushToken`. `MessageTooBig` is
729
+ `errorClass: config` and does not deactivate the token. `expo-server-sdk`
730
+ moved off `@terreno/api`. The example app requests notification permission, registers the device token after
731
+ login, and exposes a profile-screen test send (`POST /comms/dev/testPush`) in
732
+ non-production.
733
+ - Added `create-github-issue` and `work-github-issues` skills plus a lifecycle
734
+ work-item GitHub form so agents can file pick-ready issues, confirm a plan,
735
+ post it as the Roast contract, then implement with Pick ⇄ Roast.
736
+ - 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`.
737
+ - Inbound webhooks on `WebhooksApp`: raw-body capture, HMAC/Stripe/Twilio/SendGrid
738
+ verifiers (SendGrid ECDSA uses the same 300s timestamp window as Stripe), memory or
739
+ Mongo `webhookReceipts` idempotency. `CommsApp` mounts Twilio status/inbound and
740
+ SendGrid Event Webhook routes when passed the same plugin. `recordDeliveryEvent`
741
+ rethrows a failed `CommsMessage` save so webhook claims release. See
742
+ `docs/how-to/inbound-webhooks.md`.
743
+ - `terreno_search_docs` and `terreno_get_component_docs` accept an optional
744
+ `version` so agents can search retained docs snapshots for the consumer's
745
+ `@terreno/*` lockstep version. Snapshot component pages match both hyphenated
746
+ camelCase filenames and concatenated generator slugs.
747
+ - Pull requests now fail when a newly added workspace `.ts` or `.tsx` implementation
748
+ file is below 90% function coverage or 90% line coverage. Run
749
+ `bun run check:new-file-coverage --base=origin/master --threshold=90` locally.
750
+ The gate reuses each package's `bun test` file arguments so Playwright `*.spec.ts`
751
+ files are not collected. Glob arguments are expanded before spawn so packages such
752
+ as `example-frontend` still collect `*.test.ts` files. Globs that match no files
753
+ are omitted.
754
+ - 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`.
755
+ - The `terreno-planning` Cursor plugin (`2.3.0`) adds two outer-loop skills beside the
756
+ five stages: `terreno-planning-loop` walks the approved task list (default Grow once,
757
+ then Pick once — Pick owns the pick-roast inner loop; pass `phases=` to restrict to
758
+ `grow`, `pick`, `roast`, `brew`, and/or `taste`), and `terreno-taste-sweep` finds the
759
+ author's open non-draft PRs that are conflicting or failing and reinvokes Taste until
760
+ each is mergeable or blocked.
761
+ - `Popover` in `@terreno/ui` for previewing a document with loading, loaded, and error
762
+ states, an open action, and optional thumbs up/down feedback.
763
+ - 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`.
764
+ - 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/`.
765
+
766
+ ### Changed
767
+
768
+ - Brew and Taste now discover and observe product CI on every configured host, including
769
+ CircleCI and Buildkite, not only GitHub check runs. Waits use provider-native hooks such
770
+ as `gh pr checks --watch`, `circleci run watch`, and `bk build watch` where available,
771
+ with bounded polling only as fallback. Plugin `terreno-planning` is `2.4.0`.
772
+ - Brew and Taste now sleep until async review bots such as Bugbot and CodeQL finish on
773
+ the current head, then continue so they can react in the same invocation. Ordinary
774
+ product CI still uses Taste `PENDING` and the outer loop. Plugin `terreno-planning` is
775
+ `2.2.0`.
776
+ - PR GitHub Actions spend fewer minutes on docs, Playwright, Expo fingerprints,
777
+ Maestro, and the example-backend Docker check. Docs previews build only the
778
+ current version (unminified, no local search index) and reuse generated
779
+ TypeDoc/component MDX when the source hashes match. Docusaurus Faster
780
+ (Rspack) is on for PR and `master`; production still builds every versioned
781
+ tree.
782
+
783
+ CI pins Bun `1.4.0` instead of `latest`. Playwright e2e compiles the workspace
784
+ once per run and shares `dist/` with the spec shards. Example-backend CI
785
+ compiles `@terreno/*` deps in one process and watches `api/**`. The backend
786
+ Docker check rebuilds only when the image recipe changes; CD still builds
787
+ preview images from source.
788
+
789
+ Backend startup now defers sync index creation until after MongoDB connects.
790
+ This prevents import-time Mongoose buffering timeouts from blocking Cloud Run
791
+ containers before they begin listening. The example backend also avoids the
792
+ OpenTelemetry Mongoose patch that deadlocked index creation in Bun-compiled
793
+ binaries, keeps startup logging on Cloud Run's captured stdout instead of a
794
+ blocking network transport, and lets Better Auth-only realtime run without a
795
+ legacy JWT secret.
796
+ - Architectural PR review and Maestro web E2E now run on CircleCI. Matching GitHub
797
+ workflows are retained with `on: []` for rollback. Cursor Approval Agent, Bugbot,
798
+ and Security Agent stay on GitHub (Cursor GitHub App automations, not repo
799
+ workflows).
800
+ - CircleCI path filters start Netlify and GCP production and PR preview jobs.
801
+ Those jobs skip (exit 0) until `terreno-netlify` and `terreno-gcp` are filled.
802
+ GitHub Actions remains the live deployer in that window. After CircleCI
803
+ secrets exist and a deploy succeeds, set the GHA deploy workflows back to
804
+ `on: []` so terraform is not applied twice. Fork PRs skip CircleCI previews.
805
+ Preview cleanup on PR close uses GitHub `preview-cleanup.yml`.
806
+ - CircleCI provides Netlify and GCP deploy jobs, semver-tag npm releases, and
807
+ manual preview cleanup, preview deploy, EAS development build, and
808
+ single-package publish operations. Automatic production deploy path triggers
809
+ stay paused until the Netlify contexts and GCP OIDC bootstrap pass manual
810
+ verification. Matching GitHub CI/CD workflows are retained with `on: []` for
811
+ rollback. CircleCI uses OIDC for GCP; no service-account JSON key is required.
812
+ - `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.
813
+ - `@terreno/api` RBAC routes now narrow path parameters at runtime instead of
814
+ relying on implicit `any`. `@terreno/ui` Hyperlink props (`linkify`, styles,
815
+ `injectViewProps`) and the Google Maps `window.google` global use concrete
816
+ types instead of `any`.
817
+ - Root lint now rejects explicit `any` suppressions without a `noExplicitAny:` rationale.
818
+ - `improve-rulesync` now keeps user-facing guidance in `docs/` and agent-facing
819
+ guidance in `.ai/` or rules, with cross-links instead of duplicated architecture.
820
+ - 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.
821
+ - MCP create, update, and delete tools run through the same `executeCreate` /
822
+ `executeUpdate` / `executeDelete` pipeline as REST and Sync. Permission denials
823
+ and hook failures use `APIError` titles in the MCP error envelope. User-role
824
+ stripping happens after hooks in the executor (MCP uses the registry model
825
+ name). `loadDocOr404` maps invalid document `_id` values to 404, not populate
826
+ `CastError`s.
827
+ - Pick and Roast now run as an automated inner loop: implement one unblocked task, roast
828
+ it, then pick the next until the approved list is done. Roast never invokes Pick; Pick
829
+ owns continuation. Exactly one driver continues after each Roast. Later tasks rediscover
830
+ docs and skills. Brew starts only after every in-scope task has Roast `PASS`. Plugin
831
+ `terreno-planning` is `2.3.0`.
832
+ - Planning plugin skills (five stages plus planning-loop and taste-sweep) are model-invocable. The lifecycle checker requires those skills omit `disable-model-invocation`.
833
+ - 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.
834
+ - CircleCI is enabled again for package CI, repo policies, and Playwright e2e
835
+ (`.circleci/config.yml` setup + path-filtering). Config-only PRs run a small
836
+ smoke slice; mixed PRs skip that slice so path-filtered jobs are not doubled.
837
+ E2E shards share one compile + `expo export`. `rulesync-check` runs only when
838
+ rule sources change. CircleCI deploy jobs are available through manual
839
+ parameters while automatic production path triggers remain paused. Matching
840
+ GitHub deploy workflows are retained with `on: []`. See `docs/how-to/circleci.md`.
841
+ - Taste waits in-process for product CI with a GitHub CLI or CircleCI CLI watch loop.
842
+ Before any push it always pulls latest `master`, then runs `bun lint` (and affected
843
+ tests) in a fresh subagent with no parent conversation, then pushes and watches CI.
844
+ Plugin `terreno-planning` is `2.5.0`.
845
+
846
+ ### Deprecated
847
+
848
+ - `modelRouter` `realtime` and `@terreno/rtk` cache-patching helpers (`realtimeList`,
849
+ `realtimeDocument`, `setRealtimeSocket`, `getRealtimeSocket`) are deprecated and will
850
+ be removed in Terreno **58**. Migrate collection live updates to `sync` +
851
+ `@terreno/syncdb`. `RealtimeApp` remains required for sync sockets. See
852
+ [migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md) and
853
+ [remove-legacy-realtime.md](docs/tasks/remove-legacy-realtime.md).
854
+
855
+ ### Fixed
856
+
857
+ - Admin custom screens now show a clickable back arrow by default. The shared
858
+ `AdminScreenPage` routes back to admin home reliably on web instead of depending on
859
+ browser history, and `Page` supports an explicit `onBack` handler.
860
+ - 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.
861
+ - CircleCI Playwright chaos e2e no longer force-restarts the sync client after
862
+ flaps. `goOnline` waiting for the Offline banner to hide is the reconnect
863
+ signal; `client.stop()` hung for 30s on the production static export.
864
+ - CircleCI automatic Netlify and GCP production path triggers are paused until
865
+ their contexts and OIDC bootstrap pass manual verification. Netlify jobs now
866
+ validate credentials before expensive builds, docs builds avoid minification to
867
+ fit the available executor memory, and the Terraform configuration is formatted.
868
+ - CircleCI no longer deploys the production demo from prerelease tags, Zoom
869
+ release notify reports failure when publish fails, Maestro keeps Xvfb alive as
870
+ a background step, and CircleCI OIDC can require the `terreno-gcp` context UUID
871
+ before GCP impersonation.
872
+ - Taste can query CircleCI with `CIRCLECI_TOKEN` on the GitHub App project slug. The
873
+ new-file coverage job compiles the same workspace packages as example-backend CI.
874
+ The example profile form no longer resets the name field when `/auth/me` is refetched.
875
+ - Example backend Cloud Run images include `expo-server-sdk` in the compiled
876
+ binary by injecting an `Expo` client into `ExpoPushProvider`. Preview deploys
877
+ no longer crash at boot with a missing optional peer.
878
+ - GitHub `cd.yml` GCP preview jobs (`terraform-preview`,
879
+ `backend-deploy-preview`) and `preview-cleanup.yml` skip fork pull requests.
880
+ OIDC `id-token` is granted only on jobs that authenticate to GCP, not the
881
+ whole workflow. Fork PRs keep `repository: TerrenoLabs/terreno` on the
882
+ token, so WIF would otherwise accept them.
883
+ - 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.
884
+ - The `maestro-e2e` CI job now exports the example-frontend web bundle before starting the
885
+ example backend. That executor shares its memory with the mongo service container, and
886
+ bundling alongside a running backend got the export OOM-killed (`SIGKILL`) even though the
887
+ same export succeeds in `e2e-prepare`.
888
+ - `bun run check:new-file-coverage` now expands the glob arguments it reads from a package's
889
+ `test` script before spawning `bun`, so packages without a `src/` directory (such as
890
+ `example-frontend`) no longer fail with "filters did not match any test files". Expo Router
891
+ route-structural entry files under `app/` (`index`, `_layout`, `+not-found`, and dynamic
892
+ segments) are exempt from the gate; the router mounts them by file path and the screens they
893
+ render are covered in their owning package.
894
+ - 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.
895
+ - Select dropdown chevrons stay inside the field border in narrow selects on web. The `am`/`pm`
896
+ and timezone pickers in `DateTimeField` no longer render their chevrons far to the right of
897
+ their boxes.
898
+ - Example backend Cloud Run images include `twilio` in the compiled binary by injecting
899
+ a Twilio client into the SMS and Verify adapters. Invalid SMS destinations return a
900
+ permanent `SendResult` instead of throwing, so the facade does not retry them. Partial
901
+ Twilio env in the example backend throws `APIError` at boot.
902
+
903
+ ## [57.2.0] - 2026-08-24
904
+
905
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.2.0.md`](mcp-server/src/docs/upgrades/57.2.0.md).
906
+
907
+ ### Breaking
908
+
909
+ - Lifecycle stage handoff YAML is now compact `v: 2` (`v`, `stage`, `status`, `next`,
910
+ `action`; omit empty keys). Outer loops that parse `schema_version` / `recommended_next_stage`
911
+ must switch keys. Chat and PRs keep the YAML in a Details toggle. Grow lists every grilled
912
+ decision in an unbounded table after the 15-line index, or omits that table when there were
913
+ none, and stays on a question until the answer is executable.
914
+
915
+ ### Added
916
+
917
+ - Admin RBAC is now first-class: `admin:access` is the only permission that opens the admin
918
+ page. Per-model access is `read` / `write` / `writeOwned` (with `isOwned` helpers and custom
919
+ `authorize` callbacks). The shell hides models, custom screens, scripts, and Platform tools
920
+ the caller cannot use; row update/delete follow per-record capabilities; roles use a
921
+ one-select access level; denied forms are read-only.
922
+
923
+ ### Changed
924
+
925
+ - Upgraded the toolchain to Bun 1.4. The GitHub Actions jobs that pinned an exact
926
+ Bun version now use `1.4.0`, `@types/bun` and `bun-types` move to `^1.4.0`, and
927
+ apps scaffolded by `@terreno/mcp` get `@types/bun@^1.4.0`. Contributors should
928
+ run Bun 1.4 or newer locally. EAS build profiles keep their existing Bun pin:
929
+ `eas.json` is an Expo fingerprint input, so bumping it would change the native
930
+ runtime version and force a rebuild for a toolchain-only change.
931
+
932
+ ## [57.1.0] - 2026-08-23
933
+
934
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.1.0.md`](mcp-server/src/docs/upgrades/57.1.0.md).
935
+
936
+ ### Added
937
+
938
+ - Admin configuration can now live beside `modelRouter` registrations or in plugin
939
+ `adminContribution()` metadata. `@terreno/admin-frontend` adds a provider-backed widget registry,
940
+ screen router, filter drawer, bulk actions, autocomplete references, and first-party Feature Flags,
941
+ Consent, Documents, and AI admin integrations.
942
+ - `terreno_bootstrap_app` scaffolds Better Auth + `@terreno/syncdb` (replica-set MongoDB,
943
+ `SyncApp`/`RealtimeApp`, `SyncDbProvider`) instead of JWT `generateAuthSlice`
944
+ - CircleCI dual-run for package CI, repo policies, and Playwright e2e (`.circleci/`;
945
+ deploys still on GitHub Actions). See `docs/how-to/circleci.md`.
946
+ - `@terreno/comms` Phase 1 gap-fill: `beforeSend` mutate/cancel, `recordDeliveryEvent` /
947
+ `recordOptOut`, attempt history on `CommsMessage`, payload retention
948
+ (`retainPayloadDays`, `redactPayload`), and channel-wide transient retry (SMS,
949
+ verification start, per-token push). `onRetry` stays `(context, result)` with
950
+ `context.attempt`. Push prune honors `errorClass: "permanent"` as well as
951
+ `isPermanentFailure`.
952
+ - `createCollectionHooks` in `@terreno/syncdb/react` and optional per-mutation
953
+ `maxAttempts` on syncdb writes.
954
+ - Model Context Protocol support in `modelRouter` via an `mcp` option: opted-in
955
+ models expose their CRUD operations as MCP tools at `POST /mcp`, reusing the
956
+ same permissions, query filters, population, and lifecycle hooks as REST
957
+ ([#358](https://github.com/TerrenoLabs/terreno/pull/358)). `getMCPTools(user)`
958
+ in `@terreno/ai` returns those tools as Vercel AI SDK objects for in-process
959
+ chat. MCP list filters accept Mongo comparison operators (`$in`, `$gte`, `$ne`,
960
+ and friends) and top-level `$and` / `$or` on `queryFields`; operators that can
961
+ execute code (`$where`, `$expr`, `$function`) are rejected. `@terreno/rtk`
962
+ adds `useMCPTools()` and `useTerrenoChat()`. MCP HTTP/stdio servers use the
963
+ TypeScript SDK v2 (`2026-07-28`) with a how-to guide and structured tool-call
964
+ logs. Lifecycle hooks and REST `responseHandler` receive a stub Express-shaped
965
+ request from `createMCPRequest` (authenticated user, tool args as `body`, empty
966
+ `headers`/`query`/`params`, `isMCPRequest: true`) rather than forwarded HTTP
967
+ headers. `registerMCPTool` adds custom tools alongside modelRouter CRUD. The
968
+ example backend exposes `users_todo_statuses` (admin-only: every user and their
969
+ todo completed flags).
970
+ - Example app profiles list every assigned role and link superadmins to role editing. The admin
971
+ script runner includes a guarded `resetDatabase` action, and seed data includes
972
+ `superadmin@example.com`. The admin roles page supports creating and editing roles by selecting
973
+ from the server's available permissions. Admin script execution requires `admin:runScripts` when
974
+ RBAC is configured, and live production resets require `ALLOW_ADMIN_DB_RESET=true`.
975
+ - `SendGridMailProvider` at `@terreno/comms/adapters/sendgrid` (optional peer
976
+ `@sendgrid/mail`) with sandbox mode, `errorCode`/`errorClass` taxonomy, Email Activity
977
+ deep links, and one transient retry via `CommsService` hooks (`onError` / `onRetry` /
978
+ `onSend`).
979
+ - `terreno-syncdb-codegen` CLI in `@terreno/syncdb` generates typed collection hooks
980
+ (`store/syncDbSdk.ts`) from OpenAPI `x-terreno-sync` list operations. The CLI is a
981
+ bin of `@terreno/syncdb`, not a separate `@terreno/syncdb-codegen` package.
982
+ - `@terreno/syncdb` documentation: reference (`docs/reference/syncdb.md`), migration guide
983
+ (`docs/how-to/migrate-rtk-to-syncdb.md`), and local-first explainer
984
+ (`docs/explanation/local-first-data.md`)
985
+
986
+ ### Changed
987
+
988
+ - User admin forms now load existing RBAC roles into an **Add role** dropdown. Assigned roles
989
+ remain visible and can be removed without entering raw array values. Blank optional enum
990
+ fields no longer block otherwise valid saves, and role-update responses include the new roles.
991
+ - The admin sidebar groups Scripts, Roles, Version, Audit Log, Feature Flags, and Configuration
992
+ under a consistently spaced **Platform** section at the bottom. Empty navigation sections are
993
+ hidden, and Audit Log / Feature Flags no longer appear twice under Models.
994
+ - The Terreno planning plugin 2.0 is a fresh-invocation loop-engineering lifecycle:
995
+ Grow → Pick → Roast → Brew → Taste. Every bounded stage discovers repository skills,
996
+ reads/writes a shared evidence-oriented result/state contract, and exits. Brew no longer
997
+ executes Taste; Taste reacts once to current CI/mergeability/review state while the outer
998
+ loop owns waiting and reinvocation. Terreno package commands and domain rules remain in
999
+ repo-local skills. Grow retains interactive grilling and a 15-line approval summary; Pick
1000
+ retains vertical-slice TDD and independent implementation/test-quality review; Roast is
1001
+ the authoritative verifier. The former Grind behavior remains an outer-loop feature
1002
+ profile, and static validation enforces names, transitions, portability, and loop bounds.
1003
+ Deprecated repo-local routers (`ip`, `implement`, `submit`, `autobot`, `check-watcher`)
1004
+ are removed. Quiet package tests (`test:agent`) run with `AGENT=1`. Brew, Taste, and
1005
+ repository PR skills now enforce an attention-budgeted GitHub format: only `Why`, `What
1006
+ changed`, and `Verification` remain visible; optional detail is expandable; comments are
1007
+ reserved for blocked decisions or non-obvious review resolutions.
1008
+ - Unreleased notes are one file per feature in `changelog/unreleased/` instead of a shared
1009
+ `CHANGELOG.md` `## [Unreleased]` section. `docs/implementationPlans/PLAN_INDEX.md` is
1010
+ removed for the same reason — status lives on each IP's `**Status:**` header. Parallel
1011
+ PRs no longer conflict on those shared files.
1012
+ - 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`).
1013
+ - CircleCI pipelines are disabled (`.circleci/config.yml` no-op). GitHub Actions
1014
+ remains the CI of record. Restore with `.circleci/config.setup.yml`. See
1015
+ `docs/how-to/circleci.md`.
1016
+ - Lifecycle stages now follow a documentation contract: read architecture docs before
1017
+ acting, update them in the same slice, and fail user-visible or architectural work that
1018
+ ships without matching docs. All agent skills are installable with
1019
+ `npx skills add TerrenoLabs/terreno`. The committed `skills/` tree is generated from
1020
+ `.rulesync/skills/`, the planning plugin stages, and `<package>/.ai/skills/` overlays via
1021
+ `bun run skills:sync`.
1022
+ - Role assignment **preview** no longer writes `RbacAudit` denied-assign rows. Denied
1023
+ mutation audits are recorded only for the escalation `403` (`Cannot grant permissions
1024
+ you do not hold`); other failures are not stored as denials, and a failed audit write
1025
+ does not replace that original `403`.
1026
+ - When a `PermissionSource` refresh fails with `staleOnFailure: "deny"` (the default),
1027
+ last-cached `deny` grants stay in force so IdP/ABAC restrictions do not lift. Additive
1028
+ `roles` / `permissions` from that source are still omitted.
1029
+ - RBAC `createView: "deny"` and unknown field-view names fail closed instead of granting a
1030
+ full mask. Nested field omits clone documents; write masks honor dotted paths. Bulk create
1031
+ and array mutations apply the same write mask as single-document writes.
1032
+ - AdminApp model CRUD requires resource actions in addition to `admin:access`. Self-service
1033
+ still cannot write User `admin`/`roles`. Without RBAC, admin CRUD may set `admin`.
1034
+ With RBAC, `roles` go through `RoleManager.assign`. Changing `admin` requires
1035
+ `rbac:assignRoles` plus an actor who already holds the legacy admin flag;
1036
+ `rbac:manageRoles` is not a substitute. Unchanged echoed `admin`
1037
+ values are allowed. If `assign` fails after an admin User update or bulk-patch,
1038
+ non-role fields are restored so the request does not keep a partial write.
1039
+ Role update requires the actor to hold the union of current and incoming
1040
+ permissions, and delete requires holding the role's current permissions, so
1041
+ `manageRoles` cannot empty or remove grants the actor lacks.
1042
+ assign/unassign require the actor to already hold the
1043
+ target user's current permissions. The seeded `auditor` role no longer receives
1044
+ `admin:access` via read-only expansion. Admin CRUD for a resource missing
1045
+ from statements fails closed for list/read, search, and writes. Framework statements now
1046
+ include `featureFlag`, `consentForm`, and `consentResponse` so superadmin `*`
1047
+ can list those admin models; example-backend adds `adminAuditLog` list/read.
1048
+ `POST /admin/background-tasks`
1049
+ requires `admin:runScripts`; version-config GET/PUT require `configuration:read` /
1050
+ `configuration:update`.
1051
+ - MCP and SyncDB registries use resolved RBAC options (TerrenoApp-injected and the
1052
+ documented `access` + `accessControl` path, including pathless `modelRouter`)
1053
+ instead of the pre-build legacy permission arrays. Create/update also apply
1054
+ `validateAccessWritePayload` (field views / `createView: "deny"`).
1055
+ User `roles` on modelRouter writes (HTTP, sync, and MCP) are dropped when
1056
+ `accessControl` is set.
1057
+ Example-backend `backfillAdmins` is dry-run unless `RBAC_BACKFILL_ADMINS=true`.
1058
+ - `runActionPermissions` combines legacy `action.permissions` with RBAC instead of replacing
1059
+ them. Actions without `access` inherit the router's `access.resource` and the mapped CRUD
1060
+ verb (`instance` POST → `update`, `collection` POST → `create`). Example todo
1061
+ `bulkComplete` / `markComplete` set `access: {resource: "todo", action: "update"}`
1062
+ so they cannot bypass `todo:update`. Empty action permissions and inherited CRUD actions
1063
+ mapped to `null` remain disabled. Assignment previews use uncached permission resolution
1064
+ on both the current and proposed sets, honor source `staleOnFailure` without writing
1065
+ caches, and reject role permissions the actor does not already hold. The admin role
1066
+ editor keeps Create/Save in the
1067
+ modal footer and scrolls the permission grid so a larger statement vocabulary cannot
1068
+ hide the save control. Create/list responses always apply the **read** field
1069
+ mask. Per-router `access.scope`
1070
+ extra PermissionSets are evaluated on HTTP and realtime reads. Sync and realtime
1071
+ serializers accept change-stream BSON post-images (no Mongoose `toObject`) so
1072
+ `sync:delta` is not dropped after RBAC wraps `defaultResponseHandler`. Invalid permission sets
1073
+ use a stable `APIError.title`.
1074
+ - Self-service signup and `PATCH /auth/me` strip `organizationIds` alongside `admin` and `roles`,
1075
+ preventing callers from assigning themselves tenant membership. Administrative organization
1076
+ membership changes must use a privileged server-side path.
1077
+ - Ordinary RBAC-enabled User modelRouter, sync, and MCP writes strip `admin`, `roles`, and
1078
+ `organizationIds`. AdminApp strips `organizationIds` the same way, and marks legacy
1079
+ `admin` writes only after `assignRoles` and an existing-admin check succeed.
1080
+ - `RoleManager` writes `RbacAudit` (with `permissionDelta`) on create, update, remove, assign,
1081
+ and unassign. Denied escalation attempts are stored with `denied: true`. HTTP `rbacRouter`
1082
+ no longer writes a second audit row. Apps can pass `auditSink` (one function or an array)
1083
+ to fan records into a consuming-app log; set `persistAudit: false` to skip the built-in
1084
+ collection (at least one sink is then required).
1085
+ - `RbacRole.seedDefaults` accepts `extraRoles` and shares `upsertSeededRole` with
1086
+ `RoleManager.seedDefaults`. `previewRoleChange` reports a real `affectedUserCount`.
1087
+ - Importing `@terreno/api` no longer registers `RbacRole` / `RbacAudit` on the default
1088
+ mongoose connection. Use `createAccess({connection})` or `createRbacRoleModel` /
1089
+ `createRbacAuditModel`. The `RbacRoleModel` / `RbacAuditModel` singletons are removed;
1090
+ the `RbacRoleModel` type remains.
1091
+ - The RBAC implementation plan is Complete (phases 1–6 shipped).
1092
+
1093
+ ### Deprecated
1094
+
1095
+ - **`@terreno/rtk` for data synchronization** — deprecated as of **56.0.0**. Still published
1096
+ through the current major line; will not ship in the next major. Migrate collection CRUD to
1097
+ [`@terreno/syncdb`](docs/reference/syncdb.md) using
1098
+ [migrate-rtk-to-syncdb.md](docs/how-to/migrate-rtk-to-syncdb.md). Continue using `@terreno/rtk`
1099
+ for the OpenAPI SDK, Better Auth Redux, feature flags, and sockets.
1100
+
1101
+ ### Fixed
1102
+
1103
+ - Admin lists no longer inherit public `queryFilter` scoping, and `adminFilter` Mongo operators
1104
+ are not rejected as client filters.
1105
+ - Admin mutations and responses consistently scrub excluded fields, including populated refs.
1106
+ - Plugin admin contributions forward `populatePaths`.
1107
+ - Document Storage clients use the contributed `/documents` API path.
1108
+ - Admin search applies the same `queryFilter`/`adminFilter` as list CRUD.
1109
+ - AI Request Explorer multi-type filters use `$in` instead of dropping the filter.
1110
+ - Admin list search (`q`) is a case-insensitive partial match across `searchFields`.
1111
+ - The filter drawer can clear all filters and disables Apply when the draft is unchanged.
1112
+ - Admin config `name` is unique when the same Mongoose model is mounted at more than one
1113
+ `routePath`, and list search/bulk-patch metadata is looked up by path.
1114
+ - Document download failures log a status code only — not storage paths or provider payloads.
1115
+ - The admin Roles screen scrolls its role cards and the "Available permissions" list instead
1116
+ of clipping them, and keeps the heading and "Add role" button pinned above the scroll area.
1117
+ - `@terreno/ui` no longer triggers the react-native-web warning `"shadow*" style props are
1118
+ deprecated. Use "boxShadow".` — `Filter`, `WebDropdownMenu` (select fields, timezone picker,
1119
+ address field), and `DraggableList` now use `boxShadow`, and the new `createBoxShadow` /
1120
+ `applyColorOpacity` helpers build the shadow value from a color plus opacity.
1121
+ - Patched `react-native-modalize` and `react-native-actions-sheet` to use `boxShadow` on
1122
+ iOS/web and `elevation` only on Android, so the two APIs do not stack. The modalize
1123
+ stylesheet ran at import time, so the deprecation warning appeared on every web page that
1124
+ imported `@terreno/ui`.
1125
+ - Android centered dropdowns (`WebDropdownMenu` `presentation="centered"`) keep `elevation` and
1126
+ omit `boxShadow`, so the two APIs do not stack.
1127
+ - Push hooks and retries are per-token; provider results are zipped to token strings
1128
+ after `beforeSend`. Mail payloads retain `replyTo` and `dynamicTemplateData`.
1129
+ `DeliveryEvent.errorClass` is persisted on the log row. `defaultFrom` is reapplied
1130
+ after `beforeSend`. Payload cleanup is best-effort and cannot fail `logSend` /
1131
+ `appendAttempt`. Hook exception text is logged only; `metadata.hookErrors` stores
1132
+ `hook-threw`, not `String(error)`.
1133
+ - Conflict `requeue` copies per-mutation `maxAttempts` onto the cloned outbox
1134
+ row so `retries: false` stays fail-fast after keepMine.
1135
+ - Expo native fingerprints for the demo and example frontend now exclude `package.json`
1136
+ scripts. Development and test command changes no longer trigger unnecessary native build
1137
+ acknowledgements or rebuilds.
1138
+ - MCP create/update apply REST `validation.excludeFromCreate` /
1139
+ `excludeFromUpdate` and MCP `excludeFields` as a write denylist on persist and
1140
+ hook request bodies, including nested/dot paths and literal dotted keys
1141
+ (`"metadata.nested.token"`). Invalid ObjectIds return a structured not-found
1142
+ instead of crashing on `CastError`; mixed-case 24-hex ids still work. List
1143
+ returns a structured error when `queryFilter` throws. A throwing
1144
+ `responseHandler` / `mcpResponseHandler` becomes a structured tool error instead
1145
+ of a protocol crash. List filters ignore queryFields that sit under an
1146
+ `excludeFields` parent path, matching tool schema generation. Lifecycle hooks
1147
+ that throw `APIError` return `error.title` to the MCP client, matching
1148
+ `queryFilter` handling.
1149
+ - `@terreno/ui` no longer triggers the React Native Web warning
1150
+ `props.pointerEvents is deprecated. Use style.pointerEvents`. Every component that set
1151
+ `pointerEvents` as a prop (`DateTimeField`, `Filter`, `SidebarNavigation`, `ToastNotifications`,
1152
+ `WebDropdownMenu`) now sets it in `style`.
1153
+ - The `react-native-portalize` dependency, which set the deprecated prop on every screen through
1154
+ `TerrenoProvider`, is replaced by an internal portal host. `Host` and `Portal` are now exported
1155
+ from `@terreno/ui` with the same API.
1156
+ - `seedDefaults` no longer overwrites customized unsealed roles on restart; sealed defaults still refresh from code.
1157
+ API tests clear `RbacRole` / `RbacAudit` in `setupDb` so leftover unsealed names cannot leak across cases.
1158
+ - Admin `/bulk-patch` authorizes each target document, so scoped `update` cannot patch out-of-scope ids.
1159
+ - 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.
1160
+ - Permission resolver caches evict expired and overflow entries so distinct identities cannot grow unbounded.
1161
+ - Clearing a role description in the admin UI sends `null` so PATCH removes the field.
1162
+ - Admin User create rolls back the new row if `RoleManager.assign` fails after insert.
1163
+ - `assign` / `unassign` refuse to change a user whose current permissions the actor does not hold.
1164
+ - MCP model tools pick up TerrenoApp-injected `accessControl` (permissions, query filters, write masks) instead of keeping the pre-build legacy checks.
1165
+ - Example todos Sync Lab panel starts collapsed so the first list row stays
1166
+ above the tab bar on short web viewports.
1167
+ - `terreno-syncdb-codegen` rejects non-identifier collection/type names and
1168
+ JSON-escapes generated strings so a remote OpenAPI document cannot inject
1169
+ TypeScript.
1170
+ - `@terreno/syncdb` no longer misses server changes that land between its startup snapshot
1171
+ and the socket joining a stream's room: each `sync:subscribed` confirmation now pages the
1172
+ streams it names from their cursor, instead of leaving the client stale until the next
1173
+ periodic reconcile
1174
+
1175
+ ## [57.0.0] - 2026-08-20
1176
+
1177
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0.md`](mcp-server/src/docs/upgrades/57.0.0.md).
1178
+
1179
+ First stable release of the Expo SDK 57 line, cut from `master` after
1180
+ [`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes to the npm
1181
+ `latest` dist-tag. Apps on `0.x` should stay pinned until they upgrade Expo.
1182
+
1183
+ ### Added
1184
+
1185
+ - `Filter` and its composable select, boolean, accordion, and change-badge controls in
1186
+ `@terreno/ui` for desktop web filtering flows
1187
+ ([#972](https://github.com/TerrenoLabs/terreno/pull/972))
1188
+
1189
+ ### Changed
1190
+
1191
+ - Terreno's version major now tracks Expo SDK 57 (`57.x.y`). `npm install @terreno/ui`
1192
+ resolves to this line; pin `0.x` if the app has not upgraded Expo yet
1193
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1194
+ - Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
1195
+ (`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
1196
+ `expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
1197
+ `npx expo install expo@latest --fix` then rebuild native binaries
1198
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1199
+ - `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
1200
+ `expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1201
+
1202
+ ## [57.0.0-beta.1] - 2026-08-20
1203
+
1204
+ Upgrade note: [`mcp-server/src/docs/upgrades/57.0.0-beta.1.md`](mcp-server/src/docs/upgrades/57.0.0-beta.1.md).
1205
+
1206
+ First beta of the Expo SDK 57 line, cut from `master` after merging
1207
+ [`#1065`](https://github.com/TerrenoLabs/terreno/pull/1065). Publishes under the npm
1208
+ `beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
1209
+
1210
+ ### Added
1211
+
1212
+ - `Filter` and its composable select, boolean, accordion, and change-badge controls in
1213
+ `@terreno/ui` for desktop web filtering flows
1214
+ ([#972](https://github.com/TerrenoLabs/terreno/pull/972))
1215
+
1216
+ ### Changed
1217
+
1218
+ - Terreno's version major now tracks Expo SDK 57 (`57.x.y`). The stable `0.x` packages are
1219
+ unaffected; this beta does not move `latest`
1220
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1221
+ - Frontend peer/catalog stack moves to Expo SDK 57 / React Native 0.86.2
1222
+ (`expo ~57.0.14`, matching React Native DevTools and Hermes V1 fixes from
1223
+ `expo@57.0.9+`). React stays at `19.2.3`. Consuming apps should run
1224
+ `npx expo install expo@latest --fix` then rebuild native binaries
1225
+ ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1226
+ - `@terreno/syncdb` bumps `tinybase` to `^9.5.1` for Expo SDK 57 /
1227
+ `expo-sqlite` type compatibility ([#1065](https://github.com/TerrenoLabs/terreno/pull/1065))
1228
+
1229
+ ## [56.0.0-beta.2] - 2026-08-17
1230
+
1231
+ Upgrade note: [`mcp-server/src/docs/upgrades/56.0.0-beta.2.md`](mcp-server/src/docs/upgrades/56.0.0-beta.2.md).
1232
+
1233
+ Second beta of the Expo SDK 56 line, cut from `master` after merging
1234
+ [`#976`](https://github.com/TerrenoLabs/terreno/pull/976). Publishes under the npm
1235
+ `beta` dist-tag; `npm install @terreno/ui` still resolves to the stable `0.x` line.
1236
+
1237
+ ### Added
1238
+
1239
+ - Expo SDK 56 target for frontend packages: `expo ~56.0.12`, `react-native 0.85.3`,
1240
+ `react 19.2.3`, TypeScript 6
1241
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1242
+ - `@terreno/syncdb` local-first data layer (TinyBase MergeableStore, durable outbox,
1243
+ websocket delta sync, encrypted web persistence) plus `SyncApp` / sync protocol support
1244
+ in `@terreno/api`
1245
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1246
+ - `SyncStatusBanner` and `ConflictSheet` in `@terreno/ui` for sync UX
1247
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1248
+ - `@terreno/comms` with pluggable mail, SMS, push, and verification contracts, console
1249
+ development providers, delivery logging, owner-scoped push-token routes, an admin delivery
1250
+ explorer, and generated RTK Query hooks
1251
+ ([#1037](https://github.com/TerrenoLabs/terreno/pull/1037))
1252
+
1253
+ ### Changed
1254
+
1255
+ - Terreno's version major now tracks the Expo SDK major it targets (`56.x.y` for Expo 56).
1256
+ The stable `0.x` packages are unaffected; this beta does not move `latest`
1257
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1258
+ - Frontend peer/catalog stack moves to Expo 56 / React Native 0.85 / React 19.2 /
1259
+ TypeScript 6 — consuming apps must upgrade Expo before installing this beta
1260
+ ([#976](https://github.com/TerrenoLabs/terreno/pull/976))
1261
+ - `excludeArchivedPlugin` now filters `findOne` the same way as `find`, matching
1262
+ `isDeletedPlugin`
1263
+ - Sync/REST CRUD executors preserve kebab-case error `code` values
1264
+ (`create-not-allowed`, `update-not-allowed`, `delete-not-allowed`,
1265
+ `invalid-request-body`) and status-specific error names on hook-rejection paths
1266
+ - Publish workflow pins `@terreno/ui` to the release version for prerelease
1267
+ `admin-frontend` / `admin-spa` publishes (same as `ui` / `rtk`)
1268
+ - Catalog `@shopify/react-native-skia` bumped to `2.6.5` (drops the postinstall that
1269
+ could hang `bun install --frozen-lockfile`); removed unused payment/native deps from
1270
+ `example-frontend`
1271
+ - The `terreno-5-dialin` plugin skill now owns merge conflicts that appear after the
1272
+ Pour handoff: it checks PR mergeability each cycle, resolves conflicts by merging the
1273
+ base branch without rewriting pushed history, re-runs checks and frontend
1274
+ verification, and treats a conflicted PR as broken rather than mergeable
1275
+ ([#1039](https://github.com/TerrenoLabs/terreno/pull/1039))
1276
+
1277
+ ### Fixed
1278
+
1279
+ - `generateTokens` in `@terreno/api` now logs and falls back to its default expiration when
1280
+ `TOKEN_EXPIRES_IN` or `REFRESH_TOKEN_EXPIRES_IN` is not a valid duration, instead of throwing
1281
+ from `jwt.sign`
1282
+ - Sync seq claims under Mongoose 9: `claimSyncSeqs` passes `updatePipeline: true` for
1283
+ its aggregation-pipeline `findOneAndUpdate` (required after the master mongoose 9 merge)
1284
+ - `RealtimeApp` accepts injectable `SocketServer` / `startChangeStreamWatcher` so setup
1285
+ tests no longer use process-wide `mock.module` (which broke socketAuth + sync integration)
1286
+ - Mongoose 9 `insertMany` probe in `syncFrontier.test.ts` no longer calls removed `next()`
1287
+ - Sync save error middleware: `throw` instead of `next(error)` under Mongoose 9/Kareem 3,
1288
+ and release pending seq claims on any failed claimed save (not only VersionError)
1289
+ - Drop orphaned `ui` consent-history PDF test left after the move to `admin-frontend`
1290
+
1291
+ ## [0.31.0] - 2026-08-11
1292
+
1293
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.31.0.md`](mcp-server/src/docs/upgrades/0.31.0.md).
1294
+
1295
+ ### Added
1296
+
1297
+ - `EditableCard` in `@terreno/ui`: a summary card with an optional icon, badge,
1298
+ description, helper text, edit button, and attention state
1299
+ ([#981](https://github.com/TerrenoLabs/terreno/pull/981))
1300
+ - MIT `LICENSE` file in every published package, plus contribution guide, changelog,
1301
+ and GitHub issue/PR templates ([#985](https://github.com/TerrenoLabs/terreno/pull/985))
1302
+ - Public roadmap generated from `docs/` into GitHub, with `roadmap:generate` /
1303
+ `roadmap:check` scripts and roadmap discussion setup
1304
+ ([#986](https://github.com/TerrenoLabs/terreno/pull/986), [#991](https://github.com/TerrenoLabs/terreno/pull/991))
1305
+ - `check:upgrade-docs` release gate that fails a tagged publish when a release
1306
+ documents breaking, changed, deprecated, or removed behavior without an
1307
+ `mcp-server/src/docs/upgrades/<version>.md` note ([#987](https://github.com/TerrenoLabs/terreno/pull/987))
1308
+ - Deployment foundation for GCP, deploy guides, and agentic SDLC plugin phase 1
1309
+ ([#988](https://github.com/TerrenoLabs/terreno/pull/988))
1310
+ - Reference documentation for `@terreno/ai`, `@terreno/admin-spa`, and `@terreno/test`,
1311
+ plus full READMEs for `admin-backend`, `admin-frontend`, `ai`, and `api-health`
1312
+ ([#995](https://github.com/TerrenoLabs/terreno/pull/995))
1313
+
1314
+ ### Changed
1315
+
1316
+ - Mongoose 9 support: the workspace runs on Mongoose 9.7.4 and every published
1317
+ package widens its `mongoose` peer dependency to `^8.0.0 || ^9.0.0`, so Mongoose 8
1318
+ consumers keep working ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1319
+ - All published packages are MIT licensed; they were previously Apache-2.0
1320
+ ([#985](https://github.com/TerrenoLabs/terreno/pull/985))
1321
+ - `modelRouter` accepts a wider Mongoose model generic, so models carrying custom
1322
+ query helpers, methods, or virtuals no longer need a cast ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1323
+ - `findOneOrNoneFor` takes `ModelQuery<T>` instead of Mongoose's `FilterQuery<T>`
1324
+ ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1325
+ - `@terreno/admin-backend` reads array field metadata through Mongoose's public
1326
+ `getEmbeddedSchemaType()` (falling back to the Mongoose 8 `caster`) and only emits
1327
+ `itemEnum` when the embedded enum is an array ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1328
+ - Local development requires Node >= 20.19.0 ([#984](https://github.com/TerrenoLabs/terreno/pull/984))
1329
+ - Positioning copy blocks and an honest framework comparison table
1330
+ ([#993](https://github.com/TerrenoLabs/terreno/pull/993))
1331
+ - Implementation plans and program docs for the infrastructure MCP server, the B2B
1332
+ platform program, the IP + roadmap flow, and the RTK-to-syncdb migration strategy,
1333
+ with merged plans linked to their roadmap tracking issues
1334
+ ([#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))
1335
+ - Test coverage, rule alignment, and explicit-any remediation across `api`, `ui`, and `rtk`
1336
+ ([#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))
1337
+
1338
+ ### Fixed
1339
+
1340
+ - CI queues `terreno-example` EAS native builds only for new fingerprints
1341
+ ([#965](https://github.com/TerrenoLabs/terreno/pull/965))
1342
+ - Keep the Dial In PR loop active through slow or pending CI, and only return
1343
+ broken checks when no autonomous action can advance them or user direction is
1344
+ required ([#1027](https://github.com/TerrenoLabs/terreno/pull/1027))
1345
+ - Dial In preserves existing PR descriptions instead of overwriting them
1346
+ ([#1030](https://github.com/TerrenoLabs/terreno/pull/1030))
1347
+
1348
+ ## [0.30.0] - 2026-08-03
1349
+
1350
+ 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).
1351
+
1352
+ ### Added
1353
+
1354
+ - `ThumbsUpDownFeedback` UI component ([#948](https://github.com/TerrenoLabs/terreno/pull/948))
1355
+
1356
+ ### Fixed
1357
+
1358
+ - Pass `APIError` context through `modelRouter` without re-wrapping errors raised
1359
+ inside hooks or Mongoose middleware ([#967](https://github.com/TerrenoLabs/terreno/pull/967))
1360
+ - `modelRouter` no longer drops `status`, `title`, `detail`, `code`, and `meta`
1361
+ when re-throwing an `APIError` from create/update/delete handlers, populate,
1362
+ `queryFilter`, list serialization, or array-operation paths
1363
+ - Mongoose validation and cast errors from `modelRouter` writes surface per-field
1364
+ messages via `meta.fields` again instead of generic wrapper titles
1365
+ - `queryFilter` wrapper details use readable message extraction instead of
1366
+ `String(error)`
1367
+ - Framework-thrown errors follow the 0.28.0 contract: stable `title`, per-occurrence
1368
+ text in `detail`, kebab-case `code`, and status subclasses where applicable
1369
+ - `apiUnauthorizedMiddleware` only matches plain `Error("Unauthorized")`, so a
1370
+ `ForbiddenError` titled `"Unauthorized"` stays 403
1371
+
1372
+ ### Changed
1373
+
1374
+ - Add `errorDetail(error)` helper for nested `APIError` detail in framework wrappers
1375
+ - 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))
1376
+
1377
+ ## [0.29.0] - 2026-07-31
1378
+
1379
+ ### Added
1380
+
1381
+ - Typed and draw/type signature capture fields in `@terreno/ui` ([#958](https://github.com/TerrenoLabs/terreno/pull/958))
1382
+ - Short-attention-span skill for action-first AI output ([#956](https://github.com/TerrenoLabs/terreno/pull/956))
1383
+
1384
+ ## [0.28.0] - 2026-07-30
1385
+
1386
+ ### Added
1387
+
1388
+ - Open source launch program: 15 implementation plans, task lists, and the
1389
+ `build-terreno-app` dogfooding skill ([#942](https://github.com/TerrenoLabs/terreno/pull/942))
1390
+
1391
+ ### Changed
1392
+
1393
+ - Redesign `APIError` to use standard `Error` fields for Sentry grouping ([#949](https://github.com/TerrenoLabs/terreno/pull/949))
1394
+ - Rename `SelectField` `searchable` prop to `disableSearch` ([#954](https://github.com/TerrenoLabs/terreno/pull/954))
1395
+
1396
+ ### Fixed
1397
+
1398
+ - Queue only missing platform dev builds on EAS PR slow path ([#955](https://github.com/TerrenoLabs/terreno/pull/955))
1399
+
1400
+ ## [0.27.0] - 2026-07-29
1401
+
1402
+ ### Added
1403
+
1404
+ - Terreno-native agent skills (`building-terreno-apps`, `terreno-data-fetching`,
1405
+ `terreno-backend-api`, `terreno-ui`) ([#941](https://github.com/TerrenoLabs/terreno/pull/941))
1406
+ - Explicit-any audit script for tracking `any` usage across the monorepo ([#927](https://github.com/TerrenoLabs/terreno/pull/927))
1407
+ - RBAC permissions API design doc ([#887](https://github.com/TerrenoLabs/terreno/pull/887))
1408
+
1409
+ ### Changed
1410
+
1411
+ - Document custom icon registration in `@terreno/ui` ([#910](https://github.com/TerrenoLabs/terreno/pull/910))
1412
+ - Change example-backend seed admin user to `admin@example.com` (password
1413
+ unchanged) ([#935](https://github.com/TerrenoLabs/terreno/pull/935))
1414
+ - Require frontend app login, feature exercise, and PR evidence in agent
1415
+ workflows ([#915](https://github.com/TerrenoLabs/terreno/pull/915))
1416
+
1417
+ ### Fixed
1418
+
1419
+ - Fix `Modal` Confirm button on native Android tablets ([#952](https://github.com/TerrenoLabs/terreno/pull/952))
1420
+ - Guard `@terreno/rtk` web tests against `IsWeb` platform mock leakage ([#934](https://github.com/TerrenoLabs/terreno/pull/934))
1421
+
1422
+ ## [0.26.0] - 2026-07-15
1423
+
1424
+ ### Added
1425
+
1426
+ - `IconButton` gains an `active` interaction state via `state?: "default" | "active"`
1427
+ ([#885](https://github.com/TerrenoLabs/terreno/pull/885))
1428
+
1429
+ ### Changed
1430
+
1431
+ - Remove internal barrel imports; ban new internal barrel `index.ts` files via
1432
+ Biome lint and `check:no-barrel-imports` ([#907](https://github.com/TerrenoLabs/terreno/pull/907))
1433
+ - Order Terreno planning skills by workflow step (`terreno-1-blend` through
1434
+ `terreno-5-dialin`) ([#900](https://github.com/TerrenoLabs/terreno/pull/900))
1435
+
1436
+ ### Fixed
1437
+
1438
+ - Fix `Table`/`DataTable` preview cards on the demo home page rendering as a
1439
+ floating overlay ([#911](https://github.com/TerrenoLabs/terreno/pull/911))
1440
+ - Fix demo, docs, and example-frontend production Netlify deploys silently
1441
+ no-opping on push to `master` ([#903](https://github.com/TerrenoLabs/terreno/pull/903))
1442
+ - Fix duplicate `if` keys in example-app E2E and Admin SPA integration workflows
1443
+ ([#901](https://github.com/TerrenoLabs/terreno/pull/901))
1444
+ - Stabilize AI and admin frontend test suites ([#902](https://github.com/TerrenoLabs/terreno/pull/902))
1445
+
1446
+ ## [0.25.0] - 2026-07-12
1447
+
1448
+ ### Added
1449
+
1450
+ - `@terreno/api`: HTTP client layer — `createAuthenticatedClient`,
1451
+ `normalizeApiError`, and `withApiErrorHandling` ([#870](https://github.com/TerrenoLabs/terreno/pull/870))
1452
+ - `SelectField` / `WebDropdownMenu`: type-to-filter searchable dropdown
1453
+ ([#615](https://github.com/TerrenoLabs/terreno/pull/615))
1454
+ - Demo AI palette generator (`/palette`) with WCAG contrast checks ([#863](https://github.com/TerrenoLabs/terreno/pull/863))
1455
+
1456
+ ### Changed
1457
+
1458
+ - Add repo subagents and upgrade rulesync to v9 ([#892](https://github.com/TerrenoLabs/terreno/pull/892))
1459
+ - Include run evidence (screenshots/videos) in PRs via submit and pour skills
1460
+ ([#871](https://github.com/TerrenoLabs/terreno/pull/871))
1461
+
1462
+ ### Fixed
1463
+
1464
+ - `@terreno/api`: return `401` (not `500`) for auth failures in `bun build --compile`
1465
+ binaries ([#894](https://github.com/TerrenoLabs/terreno/pull/894))
1466
+ - Website: prune docs versions correctly so `versions.json` stays valid ([#866](https://github.com/TerrenoLabs/terreno/pull/866))
1467
+
1468
+ ## [0.24.0] - 2026-07-03
1469
+
1470
+ ### Added
1471
+
1472
+ - `AiSuggestionBox`: `hidden` suggestion status, condensed collapsed states, race-safe
1473
+ expansion, and refreshed sparkles/thumbs visuals ([#865](https://github.com/TerrenoLabs/terreno/pull/865))
1474
+
1475
+ ### Changed
1476
+
1477
+ - 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))
1478
+
1479
+ ## [0.23.1] - 2026-07-02
1480
+
1481
+ ### Added
1482
+
1483
+ - Architectural PR review workflow ([#844](https://github.com/TerrenoLabs/terreno/pull/844))
1484
+
1485
+ ### Changed
1486
+
1487
+ - Document Cursor Cloud dev-environment setup for example-backend MongoDB ([#845](https://github.com/TerrenoLabs/terreno/pull/845))
1488
+ - Demo Appium CI non-blocking unless a mobile build feature changes ([#846](https://github.com/TerrenoLabs/terreno/pull/846))
1489
+ - Consolidate shared dependencies into Bun catalog ([#848](https://github.com/TerrenoLabs/terreno/pull/848))
1490
+ - Align dev API port with docs; clear required on warning ([#842](https://github.com/TerrenoLabs/terreno/pull/842))
1491
+
1492
+ ### Fixed
1493
+
1494
+ - Correct example-backend consent enum snapshot ordering ([#628](https://github.com/TerrenoLabs/terreno/pull/628))
1495
+ - Fix lint regression and iOS Appium smoke timeout ([#843](https://github.com/TerrenoLabs/terreno/pull/843))
1496
+ - Fix E2E todos web server startup in CI ([#811](https://github.com/TerrenoLabs/terreno/pull/811))
1497
+ - Resolve UI and demo formatting CI failures ([#789](https://github.com/TerrenoLabs/terreno/pull/789))
1498
+ - Stop Expo fingerprint churn forcing a native build on every PR ([#849](https://github.com/TerrenoLabs/terreno/pull/849))
1499
+ - `Button`: use transparent background for ghost variant ([#858](https://github.com/TerrenoLabs/terreno/pull/858))
1500
+
1501
+ ## [0.23.0] - 2026-06-26
1502
+
1503
+ ### Added
1504
+
1505
+ - `@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))
1506
+ - Admin Script Runner CLI (`runScriptCli`, declared `args` on scripts) ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
1507
+ - Consent response list/read populate `userId`; `ConsentResponseViewer` user section
1508
+ ([#833](https://github.com/TerrenoLabs/terreno/pull/833))
1509
+
1510
+ ### Changed
1511
+
1512
+ - Document `GCP_SA_*` service account secrets in cloud agent instructions ([#829](https://github.com/TerrenoLabs/terreno/pull/829))
1513
+ - Add example-backend Script Runner CI workflow and CLI docs ([#828](https://github.com/TerrenoLabs/terreno/pull/828))
1514
+
1515
+ ### Fixed
1516
+
1517
+ - Equalize `MarkdownEditor` edit and preview pane heights ([#831](https://github.com/TerrenoLabs/terreno/pull/831))
1518
+
1519
+ ## [0.22.2] - 2026-06-24
1520
+
1521
+ ### Fixed
1522
+
1523
+ - `@terreno/api`: declare `@terreno/test` as a runtime dependency so
1524
+ `@terreno/api/testing` resolves under isolated installs ([#827](https://github.com/TerrenoLabs/terreno/pull/827))
1525
+
1526
+ ### Changed
1527
+
1528
+ - Publish `@terreno/feature-flags` for the first time since `0.21.0`; all packages
1529
+ move to `0.22.2` to stay in lockstep
1530
+
1531
+ ## [0.22.1] - 2026-06-24
1532
+
1533
+ ### Fixed
1534
+
1535
+ - `@terreno/test`: pin `qs` to `^6.14.1` instead of a missing `catalog:` entry so
1536
+ the publish workflow succeeds ([#824](https://github.com/TerrenoLabs/terreno/pull/824))
1537
+
1538
+ ### Changed
1539
+
1540
+ - Publish `@terreno/test`, `@terreno/ai`, `@terreno/admin-backend`, and
1541
+ `@terreno/feature-flags` for the first time at `0.22.1`; all packages move to
1542
+ `0.22.1` to stay in lockstep
1543
+
1544
+ ## [0.22.0] - 2026-06-24
1545
+
1546
+ ### Added
1547
+
1548
+ - New `@terreno/test` package: shared Bun test helpers and in-memory MongoDB fixtures
1549
+ ([#822](https://github.com/TerrenoLabs/terreno/pull/822), [#823](https://github.com/TerrenoLabs/terreno/pull/823))
1550
+ - `Button`: `ghost` variant and `sm` size; fix `outline` button height ([#816](https://github.com/TerrenoLabs/terreno/pull/816))
1551
+ - `OpenApiMiddlewareBuilder.withOperationId()` for custom OpenAPI `operationId` ([#815](https://github.com/TerrenoLabs/terreno/pull/815))
1552
+
1553
+ ### Changed
1554
+
1555
+ - Add SyncDB local-first data layer plan and tasks ([#739](https://github.com/TerrenoLabs/terreno/pull/739))
1556
+
1557
+ ## [0.21.0] - 2026-06-22
1558
+
1559
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.21.0.md`](mcp-server/src/docs/upgrades/0.21.0.md).
1560
+
1561
+ ### Changed
1562
+
1563
+ - **Breaking:** `setupServer` removed — use `TerrenoApp` instead ([#795](https://github.com/TerrenoLabs/terreno/pull/795))
1564
+ - **Breaking:** JSON object responses include `requestId` in the body ([#793](https://github.com/TerrenoLabs/terreno/pull/793))
1565
+
1566
+ ### Added
1567
+
1568
+ - Admin UI v2 backend: `schemaVersion: 2` config, bulk-patch, background tasks ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
1569
+ - Traceable API logging: `createScopedLogger`, `createFeatureFlaggedLogger` ([#799](https://github.com/TerrenoLabs/terreno/pull/799))
1570
+ - `Card` redesign: `container` and `display` variants ([#375](https://github.com/TerrenoLabs/terreno/pull/375))
1571
+ - MCP documentation search: `terreno_search_docs`, `terreno_get_component_docs` ([#796](https://github.com/TerrenoLabs/terreno/pull/796))
1572
+ - `@terreno/rtk` exports `devStore` utilities ([#782](https://github.com/TerrenoLabs/terreno/pull/782))
1573
+
1574
+ ### Fixed
1575
+
1576
+ - Unblock Dependabot by upgrading `@terreno/ai` multer dependency ([#803](https://github.com/TerrenoLabs/terreno/pull/803))
1577
+ - Fix CI: install workspace deps before Trigger EAS Workflow dispatch ([#805](https://github.com/TerrenoLabs/terreno/pull/805))
1578
+
1579
+ ## [0.20.2] - 2026-06-17
1580
+
1581
+ ### Changed
1582
+
1583
+ - Normalize `Badge` height to 20px; add Badge vs SelectBadge demo ([#751](https://github.com/TerrenoLabs/terreno/pull/751))
1584
+
1585
+ ## [0.20.1] - 2026-06-16
1586
+
1587
+ ### Added
1588
+
1589
+ - `@terreno/ui`: support registering custom icons ([#771](https://github.com/TerrenoLabs/terreno/pull/771))
1590
+ - Expo skills for AI agents ([#777](https://github.com/TerrenoLabs/terreno/pull/777))
1591
+ - Admin UI v2 IP with Django-style `home.slots` ([#775](https://github.com/TerrenoLabs/terreno/pull/775))
1592
+ - `@terreno/ai`: export `./parseAiJson` subpath ([#783](https://github.com/TerrenoLabs/terreno/pull/783))
1593
+
1594
+ ### Changed
1595
+
1596
+ - Normalize skill descriptions to single-line UI summaries ([#772](https://github.com/TerrenoLabs/terreno/pull/772))
1597
+
1598
+ ### Fixed
1599
+
1600
+ - Restore demo CI ordering checks ([#770](https://github.com/TerrenoLabs/terreno/pull/770))
1601
+ - Fix Appium dev-client smoke tests on Android and iOS ([#773](https://github.com/TerrenoLabs/terreno/pull/773))
1602
+
1603
+ ## [0.16.0] - 2026-06-02
1604
+
1605
+ Historical `@terreno/api` release notes preserved from the former
1606
+ `api/CHANGELOG.md` (packages now version in lockstep from 0.20.0 onward).
1607
+
1608
+ ### Added
1609
+
1610
+ - **`modelRouter` actions** — `instanceActions` and `collectionActions` on
1611
+ `ModelRouterOptions` for named operations at `/resource/:id/action` and
1612
+ `/resource/action`
1613
+ - **`loadDocOr404`** — shared document loader used by permission middleware and
1614
+ instance actions (soft-delete-aware 404 metadata preserved)
1615
+
1616
+ ### Changed
1617
+
1618
+ - Permission middleware doc loading delegates to `loadDocOr404` (behavior-preserving)
1619
+
1620
+ ### Dependencies
1621
+
1622
+ - Added `@asteasolutions/zod-to-openapi` ^8.5.0
1623
+ - Added **`zod` ^4.3.6 as a peer dependency** for backends defining action Zod
1624
+ schemas
1625
+
1626
+ ### Migration
1627
+
1628
+ - Regenerate frontend SDKs after adding actions; `operationId` values follow
1629
+ `{tag}_{actionName}` (e.g. `todos_markComplete`)
1630
+
1631
+ ## [0.20.0] - 2026-06-14
1632
+
1633
+ Upgrade note: [`mcp-server/src/docs/upgrades/0.20.0.md`](mcp-server/src/docs/upgrades/0.20.0.md).
1634
+
1635
+ ### Changed
1636
+
1637
+ - **Breaking:** `@terreno/api` `ConfigurationApp`: `POST {basePath}/list-secrets` is
1638
+ now read-only validation/status — it no longer resolves or returns secret values;
1639
+ `PATCH {basePath}` strips `secret: true` fields ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
1640
+ - **`configurationPlugin` no longer adds the `_singleton` unique index by default**
1641
+ — opt in via `enforceSingletonIndex: true`
1642
+ - **`configurationPlugin` singleton semantics are soft-delete aware**
1643
+ - **`configurationPlugin.updateConfig` applies updates via `findOneAndUpdate({$set})`
1644
+ with dotted paths** instead of `Object.assign` + `doc.save()`
1645
+ - Prefix `terreno-planning` Cursor plugin skills with `terreno-` ([#764](https://github.com/TerrenoLabs/terreno/pull/764))
1646
+ - Submit skill: merge-first PR body updates ([#765](https://github.com/TerrenoLabs/terreno/pull/765))
1647
+
1648
+ ### Added
1649
+
1650
+ - `@terreno/api` configuration and secret upgrades: `CompositeSecretProvider`,
1651
+ `CachingSecretProvider`, pluggable `permissions`, `preUpdate`/`postUpdate` hooks,
1652
+ optional `version` on secret resolution ([#768](https://github.com/TerrenoLabs/terreno/pull/768))
1653
+ - OpenFeature migration for feature flags: `MongoFeatureFlagProvider`,
1654
+ `GET …/flagConfiguration`, `useTerrenoFeatureFlags` hook ([#761](https://github.com/TerrenoLabs/terreno/pull/761))
1655
+ - `SecretProvider.getSecret(secretName, version?)` and `flattenToDotPaths` export
1656
+
1657
+ ### Deprecated
1658
+
1659
+ - Legacy `GET …/evaluate` feature-flag endpoint (sends `Deprecation`/`Sunset` headers)
1660
+ ([#761](https://github.com/TerrenoLabs/terreno/pull/761))
1661
+
1662
+ [57.0.0]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0
1663
+ [57.0.0-beta.1]: https://github.com/TerrenoLabs/terreno/releases/tag/57.0.0-beta.1
1664
+ [56.0.0-beta.2]: https://github.com/TerrenoLabs/terreno/releases/tag/56.0.0-beta.2
1665
+ [0.30.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.30.0
1666
+ [0.29.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.29.0
1667
+ [0.28.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.28.0
1668
+ [0.27.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.27.0
1669
+ [0.26.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.26.0
1670
+ [0.25.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.25.0
1671
+ [0.24.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.24.0
1672
+ [0.23.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.1
1673
+ [0.23.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.23.0
1674
+ [0.22.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.2
1675
+ [0.22.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.1
1676
+ [0.22.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.22.0
1677
+ [0.21.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.21.0
1678
+ [0.20.2]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.2
1679
+ [0.20.1]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.1
1680
+ [0.20.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.20.0
1681
+ [0.16.0]: https://github.com/TerrenoLabs/terreno/releases/tag/0.16.0