@akanjs/devkit 3.0.0 → 3.0.2-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +1832 -0
  2. package/akanConfig/akanConfig.test.ts +3 -0
  3. package/akanConfig/akanConfig.ts +3 -1
  4. package/akanContext.ts +18 -1
  5. package/artifact/implicitRootLayout.test.ts +2 -1
  6. package/artifact/implicitRootLayout.ts +2 -1
  7. package/artifact/routeSeedIndex.test.ts +0 -14
  8. package/biome.base.json +12 -1
  9. package/commandDecorators/command.ts +3 -2
  10. package/commandDecorators/commandDecorators.test.ts +7 -0
  11. package/csrE2e/csrE2eHarness.fixture.ts +3 -2
  12. package/csrE2e/csrPageStack.e2e.test.ts +25 -3
  13. package/executors.test.ts +329 -4
  14. package/executors.ts +49 -47
  15. package/frontendBuild/csrArtifactBuilder.test.ts +34 -0
  16. package/frontendBuild/csrArtifactBuilder.ts +14 -8
  17. package/frontendBuild/csrDevModuleCompiler.ts +0 -1
  18. package/frontendBuild/csrDevResolver.ts +0 -2
  19. package/frontendBuild/devGeneratedIndexSync.test.ts +13 -1
  20. package/frontendBuild/devGeneratedIndexSync.ts +21 -4
  21. package/frontendBuild/frontendBuild.test.ts +1 -1
  22. package/frontendBuild/hmrWatcher.ts +6 -0
  23. package/integration/devStability.integration.test.ts +8 -5
  24. package/integration/devStabilityHarness.ts +2 -0
  25. package/jsonFormatter.test.ts +64 -0
  26. package/jsonFormatter.ts +78 -0
  27. package/lint/__fixtures__/README.md +12 -3
  28. package/lint/__fixtures__/no-double-brace-placeholder/bad.ts +22 -0
  29. package/lint/__fixtures__/no-double-brace-placeholder/fixture.json +1 -0
  30. package/lint/__fixtures__/no-double-brace-placeholder/good.ts +15 -0
  31. package/lint/__fixtures__/no-inline-color/bad.tsx +17 -0
  32. package/lint/__fixtures__/no-inline-color/good.tsx +13 -0
  33. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +43 -0
  34. package/lint/__fixtures__/no-static-in-object-light-model/fixture.json +1 -0
  35. package/lint/__fixtures__/no-static-in-object-light-model/good.ts +42 -0
  36. package/lint/__fixtures__/no-throw-raw-error/bad.tsx +8 -0
  37. package/lint/__fixtures__/no-throw-raw-error/good.tsx +8 -1
  38. package/lint/__fixtures__/no-unguarded-endpoint/bad.ts +20 -0
  39. package/lint/__fixtures__/no-unguarded-endpoint/fixture.json +1 -0
  40. package/lint/__fixtures__/no-unguarded-endpoint/good.ts +28 -0
  41. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -7
  42. package/lint/gritRules.test.ts +23 -15
  43. package/lint/no-double-brace-placeholder.grit +15 -0
  44. package/lint/no-inline-color.grit +21 -5
  45. package/lint/no-static-in-object-light-model.grit +27 -0
  46. package/lint/no-throw-raw-error.grit +18 -33
  47. package/lint/no-unguarded-endpoint.grit +73 -0
  48. package/manifestDependencies.ts +65 -0
  49. package/mobile/desktopBin.test.ts +1 -1
  50. package/mobile/desktopServerStage.test.ts +7 -4
  51. package/mobile/nativeApp.test.ts +57 -8
  52. package/mobile/nativeApp.ts +27 -9
  53. package/mobile/nativeConfig.test.ts +26 -4
  54. package/mobile/nativeConfig.ts +19 -1
  55. package/package.json +2 -2
  56. package/platformTest/LinuxDockerTarget.ts +1 -1
  57. package/platformTest/PlatformTestTarget.ts +3 -1
  58. package/platformTest/WindowsSshTarget.ts +1 -1
  59. package/qualityScanner.test.ts +51 -0
  60. package/qualityScanner.ts +22 -1
  61. package/scanInfo.ts +14 -2
  62. package/subspace.test.ts +10 -1
  63. package/testHelpers.ts +17 -0
  64. package/transforms/transforms.test.ts +13 -0
  65. package/tsconfig.json +4 -15
  66. package/workspaceLayout.test.ts +24 -1
  67. package/workspaceLayout.ts +19 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1837 @@
1
1
  # @akanjs/devkit
2
2
 
3
+ ## 3.0.1
4
+
5
+ ### Minor Changes
6
+
7
+ - 4871ae5: New lint rule `no-static-in-object-light-model` for `*.constant.ts`: a `static` on an `XObject` or `LightX` class is an error. The full model `via(XObject, LightX, …)` mixes in their prototypes only, so such a static never reaches `X`; declare it on `XInput` (or a scalar), `X`, or `XInsight` instead.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [4871ae5]
12
+ - Updated dependencies [4871ae5]
13
+ - Updated dependencies [4871ae5]
14
+ - akanjs@3.1.0
15
+
16
+ ## 3.0.0
17
+
18
+ ### Minor Changes
19
+
20
+ - ccf2ae2: A CSR dev page — `?csr=true`, `/__csr`, and every native dev build — updates in place on save instead of reloading.
21
+
22
+ - `akan start` serves the CSR page as a module registry: each module is its own factory, and a save sends only the
23
+ modules it changed. A component edit keeps hook and DOM state (React Fast Refresh), a store edit keeps the store,
24
+ and a build error shows the overlay and recovers without a reload.
25
+ - A page or layout edit swaps that route in place through `replacePages`: the page stack, history and stores stay,
26
+ and so does the state of the components under the edited page or layout. A `*.constant.ts`, an added or removed
27
+ route, a new npm dependency or a signal/dictionary change still reloads.
28
+ - `AKAN_DEV_CSR=artifact` brings back the single-file dev bundle, which reloads on every save. `akan build` and a
29
+ release mobile build are unchanged.
30
+ - HMR sockets now say what they are: a CSR page connects as `/_akan/hmr?client=csr` and gets `csr-update` instead of
31
+ the SSR `reload` and `rsc-refresh`, and a page that missed an update while disconnected reloads
32
+ when it reconnects.
33
+
34
+ - ccf2ae2: A Windows desktop app can share its screen without the picker
35
+
36
+ `native.desktop.screenCapture: "auto"` in `akan.config.ts`, or `desktop.screenCapture` on one target, makes the
37
+ Windows app answer the page's `getDisplayMedia()` with the first screen at once, without the picker and without a user
38
+ gesture, for remote support on a screen nobody attends. It is Chromium's switch for automated media tests and covers
39
+ every media request, so leave it off in an app whose pages ask for a camera or a microphone. The default `"picker"`
40
+ keeps the picker; macOS and Linux ignore it.
41
+
42
+ - ccf2ae2: `akan build-desktop --installer` makes a Windows setup program
43
+
44
+ On Windows the build adds `<file>-<version>-<arch>-setup.exe`, made with NSIS (`winget install NSIS.NSIS`). It installs
45
+ for the current user under `%LOCALAPPDATA%\Programs\<app name>`, where the updates plugin swaps the app without an
46
+ administrator, with a Start menu shortcut and an uninstall entry. `/S` installs silently and `/RUN` starts the app
47
+ afterwards, and a PC without the WebView2 Runtime gets it. The program is not code-signed yet.
48
+
49
+ - What can fail without touching the installed app comes first: WebView2, then the new files, unpacked beside the
50
+ folder. Only then is a copy of the app running from the folder stopped, found by its path, and the folder swapped by
51
+ two renames. A swap that fails puts the old folder back, and with `/RUN` the app left in place starts again.
52
+
53
+ - The uninstaller lives beside the app folder (`<folder>.uninstall.exe`), not in the folder an update replaces, so
54
+ Settings › Apps can still remove an updated app. It finds the folder from the uninstall entry and removes it with
55
+ whatever updates left beside it (`.previous`, unpacked or failed releases) and itself, the launch-at-login entry and
56
+ the updates state; the server's data stays. An uninstaller that cannot be
57
+ written there (a `/D=` right under a drive root) fails the install.
58
+ - The Start menu shortcut and the app `/RUN` or the last page starts work in `%LOCALAPPDATA%`: Windows cannot rename a
59
+ folder some process works in, which is what applying an update does.
60
+ - `/RUN` starts the app after a silent install only; an interactive one offers it on its last page.
61
+ - Run again without `/D=`, it installs into the folder the uninstall entry names, so a reinstall replaces the copy
62
+ already installed wherever it was put, and a setup started while another one runs refuses to start.
63
+ - A silent install that cannot finish exits with 2: files it could not write (the script is long-path aware, and the
64
+ build warns when the deepest file nears 260 characters under `%LOCALAPPDATA%\Programs`), a WebView2 Runtime still
65
+ missing after its setup ran (an interactive install asks), or a `/D=` folder holding someone else's files.
66
+ - An update the app confirms sets the version Settings › Apps shows.
67
+
68
+ - ccf2ae2: `akan start-desktop <app>` (alias `sd`) runs a native target as a desktop app on this computer — macOS, Windows or
69
+ Linux, whichever it is — the way `start-ios` and `start-android` run it on a phone: a debug build whose pages come
70
+ from `akan start` through the dev gateway, so every save shows up, or with `--release` a release build of its own
71
+ bundle. It takes `--target`, `--env`, `--release` and `--write`. `NativeApp` accepts the desktop platforms, and
72
+ `NativeApp.desktopPlatform()` names this computer's.
73
+ - ccf2ae2: An app and a lib ship native plugins of their own from a `native/` folder
74
+
75
+ A plugin the runtime has no builtin for sits in `apps/<app>/native/<id>/` (or `libs/<lib>/native/<id>/`), the folder
76
+ named after the id in its `native-plugin.json`. It is listed nowhere: every mobile and desktop target of the app
77
+ ships it, and its manifest says what runs on each platform. A lib's plugins reach only the apps that depend on it; an
78
+ app's own plugin wins an id a lib also uses, and two libs claiming one id stop the build. `akan sync`, `akan doctor`
79
+ and `akan quality scan` accept the folder.
80
+
81
+ The page API is written with `definePlugin` from `akanjs/client/native` (which also exports `defineWebPlugin`,
82
+ `createLiveValue`, `useLiveValue` and `usePluginEvent`), and the desktop part with `defineDesktopPlugin` from the new
83
+ `akanjs/native/desktop`. A `native/` folder is out of the scope of `no-throw-raw-error` (a plugin throws
84
+ `AkanNativeError`) and of `no-web-only-api-outside-webkit`. A plugin a target names by folder in `native.plugins` is now
85
+ granted by its id instead of its path, and ships once when it is also the app's own `native/<id>`. A desktop plugin's
86
+ `ctx.launch.exit(code)` after its setup ran past the launch phase's 3 s quits the app, since its window exists by then.
87
+
88
+ - ccf2ae2: An installed app updates itself: a phone its web bundle over the air, a desktop app the whole app.
89
+
90
+ - `native.updates: { url, publicKey, channel?, readyTimeout? }` in `akan.config.ts`, per target too, the target's
91
+ fields winning. It brings the runtime's `updates` plugin. A channel left unnamed is the backend env the binary is
92
+ built for (`main`, `develop`, `debug`, …), so a `build-desktop` app (`debug` unless `--env` names another) takes
93
+ only the releases published for that env.
94
+ - The CSR frame confirms a release on trial once the first page is on screen, in every native shell (an unconfirmed
95
+ one is rolled back at the next launch). On a phone it also keeps the bundle current by itself: at start and on each
96
+ return to the front it looks for a newer one and downloads it, and it runs from the next cold start. A desktop
97
+ release is the whole app and a relaunch, so the app checks, downloads and applies it on its own schedule. A dev
98
+ build's pages are left alone. `akanjs/client/native` exports `updates`, `markReady` and `useUpdateState`.
99
+ - `akan update-keygen <app> [--platform]` makes the signing key of the app's id once and prints its public half.
100
+ `akan publish-update <app>` builds a release on this machine and signs it: the whole app for this computer's desktop
101
+ OS and CPU (with the app's server when `desktop.server` says so for the target, as for `build-desktop`), or the
102
+ web bundle for `--platform android|ios`. It writes the manifest and its files under `.akan/native/<target>/updates`,
103
+ to upload to `updates.url`, on the channel of its `--env` (`main` unless named). `--channel` names only the manifest
104
+ written, not the channel the release follows, so a pilot group gets a target whose `updates.channel` is the pilot's.
105
+ - The updates folder holds only what is uploaded, and whoever can reach `updates.url` reads it: the updater sends no
106
+ credentials, and a desktop release is the whole app, with the carried server's `private/` (the app's and its libs')
107
+ and its env file.
108
+ - `akan pack-update <app> --platform ios|android [--target] [--env] [--out]` writes an unsigned phone update instead,
109
+ for a signer that keeps the key elsewhere: `files/<sha256>`, `bundle.json` and `manifest.template.json`, the
110
+ manifest with `channel`, `sequence` and `bundle` left for the signer. `--against <store bundle.json>` also checks
111
+ the bundle runs in that store build, writes `compat.json`, and fails when it needs a new binary. The signing
112
+ contract is in the native runtime's architecture notes: fill the three fields, sign exactly the bytes you upload,
113
+ upload `files/` first.
114
+ - `publish-update` refuses a `--channel` outside the names `updates.channel` accepts (lowercase letters, digits, `.`,
115
+ `_`, `-`) before it builds, and numbers a release past the one its channel already has in the updates folder,
116
+ warning when this computer's clock is behind it. Apps compare against the time their own build was made, so keep the
117
+ building and the publishing computers' clocks in step.
118
+ - While a desktop release is on trial, `updates.check()` answers `available: false` for it and `updates.apply()`
119
+ rejects with `NOT_ALLOWED`: applying would replace the app a failed trial goes back to.
120
+ - A release of a desktop app that carries its server is confirmed only once that server answered ready and stayed up
121
+ for 5 s, and only while it is up; its trial clock (`readyTimeout`) starts then, so a server slow on its first run
122
+ (its new files being scanned) costs no rollback. A server that gives up, or is not up within 120 s of the start,
123
+ rolls the release back at once; the release stays downloaded for the next apply, and its third such failure
124
+ excludes it for good.
125
+ - A desktop release's manifest says whether it carries a server, and an installed app refuses one that differs from
126
+ itself before downloading anything (`check()` answers `available: false`, `download()` rejects with
127
+ `NOT_ALLOWED`), since either way its pages' backend would change place. `publish-update` refuses, before it builds,
128
+ a desktop release whose server presence differs from the channel's previous one: publish it on another channel
129
+ (`updates.channel`), or remove that `<channel>.json` from the output folder to start the channel over. Turning
130
+ `native.desktop.server` on or off for an app already installed takes a reinstall.
131
+ - On Windows an app started from its install folder (the installer, the Start menu, Explorer) applies and rolls back
132
+ updates: Windows renames no folder a process works in, so the app moves to its local data folder before the webview
133
+ starts, and the update helper and the app it starts work elsewhere.
134
+ - The update state and the unpacked releases live in the app's local data (`%LOCALAPPDATA%\<app id>\akan-native-updates`
135
+ on Windows), and plugins get that folder as `ctx.appLocalDataDir`.
136
+ - An app installed under another folder name (`/D=`, a renamed `.app`) takes updates; a download older than a
137
+ reinstalled build is dropped; a Windows swap that did not happen (a file stayed locked, the helper never ran) keeps
138
+ the release downloaded for the next apply until its third such failure excludes it. Reinstalling the app clears the
139
+ list of releases the earlier install refused.
140
+ - `publish-update` checks every target before anything builds — its `updates` settings, its native config, the
141
+ signing key on this computer, and whether its channel's previous release carries a server as it would — so a CI
142
+ without the key stops at once and a publish never releases only the first targets. It writes `<channel>.json`
143
+ together with its signature.
144
+ The channels `bundle`, `manifest.template` and `compat` are refused. `akan start` keeps `.akan/native`, where
145
+ releases wait to be uploaded, and the `bin` downloads in `.akan/cache/bin`.
146
+
147
+ - ccf2ae2: An app image carries the server env of the environment it runs and no other.
148
+
149
+ - `env/env.server.ts` imports every environment's file, so `akan build` used to bundle all of them into `server.js`:
150
+ anyone holding the image of one environment could read the keys of every other. The backend build now keeps only
151
+ `env.server.<AKAN_PUBLIC_ENV>.ts`, the environment the generated Dockerfile fixes, and swaps each other
152
+ environment's file (`local`, `testing`, every branch) for exports that refuse to be read. Booting such an image under
153
+ another `AKAN_PUBLIC_ENV` stops with `env/env.server.<name>.ts is not in this build` instead of running on a
154
+ config it does not have. Nothing in `env/` needs to change.
155
+ - An app's generated `server.ts` no longer re-exports `env` from `env.server.testing.ts`, which put the testing env in
156
+ every image. Signal tests read it from the file. A script or test that imported `env` from an app's `server.ts`
157
+ imports `./env/env.server.testing` instead. A lib's `server.ts` still exports it, since each app's
158
+ `env.server.type.ts` spreads it as the lib's defaults.
159
+ - Keys that already shipped in an image stay readable in it. Rotate them.
160
+ - A build for a named `--env` (a mobile or desktop build) writes that env into the Dockerfile too, so the image boots
161
+ the environment its `server.js` carries.
162
+
163
+ **Breaking for an image started under another `AKAN_PUBLIC_ENV`, and for code importing `env` from an app's
164
+ `server.ts`:** the image stops at boot until it runs the environment it was built for, and the import moves to
165
+ `./env/env.server.testing`.
166
+
167
+ - ccf2ae2: A desktop app nobody attends recovers its page, relaunches itself and opens as a kiosk
168
+
169
+ `native.desktop.recovery: "reload"`, for the app or one target, loads a page whose web process ended (a crash, a hang,
170
+ out of memory) again, once per end, instead of showing an error page after the second end, waiting longer after each
171
+ end in a row (1 s, doubling to a minute). It relaunches the app when the webview's browser process ends, where the app
172
+ used to quit: at once the first time, then after 1 s doubling to a minute while each relaunched app dies within a
173
+ minute, and after ten in a row it exits with code 1 instead of restarting for good.
174
+ `native.desktop.window: { fullscreen, skipTaskbar }` opens the main window borderless fullscreen and without a taskbar
175
+ button (Windows, Linux) from its first frame; a plugin's launch phase decides the same per launch with
176
+ `ctx.launch.setWindow`. `app.relaunch()` ends the app and starts it again in a new process on the desktop and Android,
177
+ and reloads the page on the web; one process starts one new app, however many windows or calls asked for it.
178
+ `native.android.autoplay: true` lets media play with sound without a tap first, as it already does on iOS and the
179
+ desktop.
180
+
181
+ An app relaunched by an update on Windows no longer quits the moment it starts when `akan start-desktop` had started
182
+ the one before it.
183
+
184
+ - ccf2ae2: A desktop app can carry the app's own server: with `native: { desktop: { server: true } }` in `akan.config.ts`, or
185
+ `desktop.server` on one target, `akan build-desktop`, `akan start-desktop --release` and `akan publish-update` put the
186
+ backend `akan build` made into the app, so it works on one computer with no backend elsewhere.
187
+
188
+ - The build stages what the backend build wrote into `apps/<app>/.akan/desktop/server` — `main.js`, `server.js`, the
189
+ chunks, `akan.build.json`, `private/`, and any `.node`, `.wasm` or file asset a bundled package brought — without the
190
+ Dockerfile, the RSC worker, the console, `csr/` and `public/`. Its packages install there first
191
+ (`bun install --production --prefer-offline`, without the RSC renderer and the drivers of every database mode but
192
+ `single`), before `akan build` runs, so a computer that cannot reach the registry or find them in Bun's cache stops at
193
+ once. The app needs `single` in `database.modes`; the command says so before it builds.
194
+ - The backend is built for the command's own `--env`, not the workspace's `AKAN_PUBLIC_ENV` (the root `.env` usually
195
+ names `local`), so `server.js` holds that environment's `env.server.<env>.ts` and no other.
196
+ - What it carries is readable in plain text by anyone with the app: `private/` (each lib's too, under
197
+ `private/libs/<lib>`), that `env.server.<env>.ts` and the server env defaults of the libs it uses (each lib's
198
+ `env.server.testing.ts`). Keep deployment secrets out of them. It has no `public/`, and its working
199
+ folder is its data folder: read a runtime file from the app folder (`AKAN_APP_DIR`, else the folder of `Bun.main`),
200
+ never from `process.cwd()`.
201
+ - At launch the shell starts it as a child on its own Bun (`BUN_BE_BUN`, with `.env`, `bunfig.toml` and
202
+ auto-install off) on a loopback port, and hands the page its URL as `PUBLIC_AKAN_SERVER_URL`. The port of the last
203
+ session comes first and a fresh one is picked only when it is taken, so an address registered somewhere usually
204
+ stays valid from one launch to the next. The window waits for it up to 8 s.
205
+ - It runs API only, `operationMode` edge, database mode `single`, SSR, CSR and MCP off, bound to 127.0.0.1 with every
206
+ other Host refused. Any program on the computer can still call that port, so guard its endpoints as a network
207
+ server's. It keeps its SQLite data, files, logs and a per-install JWT secret in the app data folder's
208
+ `server/`, and a debug build, which has the release app's id, in its own `server-debug/`. On Windows that folder is
209
+ `%LOCALAPPDATA%\<app id>\server`, not the Roaming folder a profile copies on every sign-in and an organisation may
210
+ put on a file share, where SQLite's WAL does not work. Bun's transpiler cache goes to
211
+ `<server data>/runtime/transpiler-cache`.
212
+ - It trusts the operating system's CAs (`--use-system-ca`), as the page does, and gets the proxy and CA variables
213
+ (`HTTP(S)_PROXY`, `NO_PROXY`, `NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `NODE_USE_SYSTEM_CA`) and the desktop session's
214
+ (`DISPLAY`, `WAYLAND_DISPLAY`, the session bus, the audio server, `XDG_*`, and Windows' `ProgramFiles`, `ComSpec`,
215
+ `PATHEXT` and the like), so a `bin` tool reaches what the page reaches and can open the screen, the audio server or a
216
+ shell.
217
+ - `AkanApp` takes `BUN_BE_BUN` off `process.env` at boot and gives it back only to a spawn of its own executable (the
218
+ ops snapshot, which also gets the server's runtime flags), so a `bin` tool built with `bun build --compile` starts as
219
+ itself rather than as the Bun CLI. A child started with `Bun.spawn` and no `env` still gets the environment the
220
+ process started with, so pass `env: process.env`.
221
+ - A crash restarts it on the same port (1 s doubling to 30 s; the fifth in a row gives up with an alert). A server that
222
+ exits before its first ready starts again 250 ms later, on the same port while it is still free, so one that cannot
223
+ boot gives up while the window still waits for it and the window opens at once with the alert. A server
224
+ that cannot start at all (an unreadable `jwt.secret`, a data folder that cannot be made) still hands the page a
225
+ loopback URL and shows the same alert once the window is up, so the page never falls back to the backend its bundle
226
+ was built for. A restart that throws counts as one failure. Its standard error, and its standard output until it is
227
+ ready, also go to `server/runtime/logs/server-output.log`, where a server that fails before it listens leaves its
228
+ reason.
229
+ - Quitting the app stops it within its 1 s shutdown budget, inside the launcher's 1.5 s grace, and a server still
230
+ there after the grace is killed. On macOS and Linux it leads its own process group, and whatever it started ends
231
+ with it: the launcher ends the group when the app quits, and when the app was killed or crashed — even while the
232
+ server was still starting — the server sees its parent gone and ends the group itself, SIGTERM first and SIGKILL a
233
+ second later. A process started `detached` leaves the group. On Windows the job object ends them all with the app.
234
+ - It runs in one process: a `main.ts` or an env asking for a gateway and replicas (`replica`, `solo: false`,
235
+ `AKAN_SOLO=false`, `AKAN_REPLICA`, `AKAN_COMMAND_TYPE=start`) does not boot in the app, and says which.
236
+ - A native plugin follows the server with `ctx.server.state` and `ctx.server.onState` (`starting`, `up`,
237
+ `restarting`, `gaveUp`, `stopped`), and a page with the `app` plugin's `serverState` event, so an app nobody attends
238
+ can act on a server that gave up (`app.relaunch()`, say).
239
+ - `XAUTHORITY`, `PULSE_COOKIE`, `PULSE_RUNTIME_PATH` and `XDG_DATA_DIRS` reach it too. A second launch that hands
240
+ over to the first starts none. The single-instance plugin comes with it.
241
+ - `akan start-desktop` without `--release`, for such a target, follows this checkout's dev server already answering on
242
+ the app's dev port, or starts `akan start` in the same command and opens the app once it serves; it asks for one
243
+ target before it starts one, needs `single` as a release does, and stops at once when that dev server fails to boot
244
+ (a replica's crash loop included) instead of waiting it out. Ctrl+C or closing the app stops the app, then the dev
245
+ server it started, then the local database, one after another, and exits 130.
246
+ - `akan start-desktop --release` of such a target shows the carried server's lines at the level its logger wrote them.
247
+ - `AkanAppConfig.getProductionEnv()` is the env the image runs with; the Dockerfile's `ENV` lines come from it (same
248
+ keys and order, without the two blank lines the template used to leave).
249
+
250
+ - ccf2ae2: `akan build-desktop --arch arm64|x64` picks the CPU a Windows or Linux desktop app runs on
251
+
252
+ A desktop app is still built on its own OS, now for either of its CPUs: the Rust library is built for that target,
253
+ the executable with `bun build --compile --target=bun-<os>-<arch>`, the carried server installs with
254
+ `bun install --cpu=<arch>` so its addons' prebuilt binaries are that CPU's, and each `bin` entry is that platform's.
255
+ The setup program and the AppImage name the CPU they hold. A macOS app is Apple silicon only: Intel Macs are not a
256
+ target, so `--arch x64` on macOS is refused.
257
+
258
+ - ccf2ae2: `bin` in `akan.config.ts` puts executables in a desktop app, and the build says what the image installs that the app does not
259
+
260
+ A desktop app ran whatever `ffmpeg` the user's computer had on its PATH, if any, and a Finder-launched app sees only
261
+ `/usr/bin:/bin:/usr/sbin:/sbin`. `bin` names an executable and, per platform (`darwin-arm64`, `win32-x64`, …), where it
262
+ comes from: a download with its `sha256` (an archive takes `file`, the executable inside it) or a path next to the
263
+ declaring `akan.config.ts`. `build-desktop` and `start-desktop` fetch the file for the computer they build on, check it,
264
+ and carry it in the app's `bin/`, which the app puts first on its PATH: the carried server's `spawn("ffmpeg")` runs
265
+ that file, and a native plugin finds it in `ctx.binDir`. A download is kept as `download` plus its URL's extension
266
+ (`.zip`, `.tar.xz`, `.exe`, …), never under a name the URL spells. Bun's own `spawn` without `env` reads the environment the app
267
+ started with, so a plugin passes `env: process.env` to run one by name. A lib's entries reach the apps that depend on
268
+ it, and an app's own entry of the same name wins. macOS builds sign every executable the app carries, in `bin/` and
269
+ in the carried server, found by its Mach-O header rather than its name, so one a package ships without an extension
270
+ passes Developer ID signing and notarization too. The image does not read `bin`: it still installs through `docker`.
271
+
272
+ `build-desktop` of a target that carries its server now warns when the image runs `docker` steps (or the app writes
273
+ its own Dockerfile) and the app carries no `bin`, since the carried server runs none of those steps.
274
+
275
+ - ccf2ae2: `desktop.server: { omit }` carries the desktop app's server without packages only the image needs
276
+
277
+ `native: { desktop: { server: { omit: ["rclnodejs"] } } }` carries the server as `server: true` does, minus the named
278
+ packages and whatever only they pull in: an addon tied to the image's system (a ROS install), or code only a process
279
+ the desktop app never starts loads. The image's `package.json` keeps them, `externalLibs` included; the desktop
280
+ server's `package.json`, its `bun install --production` and the addon check do not see them. A package another
281
+ dependency still installs stops the build, naming the dependents, and the targets that carry one build's server must
282
+ omit the same packages.
283
+
284
+ - ccf2ae2: A desktop app's server reads the files the user picks, with no copy and no path in the page
285
+
286
+ `filePicker.pickFiles({ forServer: true })` (also `pickDirectory` and `saveFile`; `filePicker` is now exported from
287
+ `akanjs/client/native`) copies nothing on the desktop, whatever the file's size: its FileRefs serve the originals,
288
+ and each result carries a grant. The server exchanges the grant with `NativeFile.resolve(grant, "read" | "write" |
289
+ "folder")` from `akanjs/server`, or `NativeFile.resolveIn(folderGrant, relative)`. The carried server asks the shell
290
+ that showed the dialog over its IPC channel, so it reaches what the user picked and nothing else; a debug build
291
+ that carries its server does the same. Behind a dev build without one, `akan start` checks the grant's signature
292
+ instead, in `operationMode` local only. A phone or the web refuses `forServer`.
293
+
294
+ `resolveIn` judges a relative path by where it lands once the links on it are resolved — through its nearest existing
295
+ folder when the file is about to be written — so a link inside the granted folder cannot take it elsewhere, and a
296
+ dangling link is refused. A name that starts with two dots (`..cache/x`) is inside the folder, not above it.
297
+
298
+ - ccf2ae2: `akan build-desktop --installer true` on Linux adds an AppImage
299
+
300
+ The app folder gains an `AppRun`, a `.desktop` entry naming the app's icon and its `deepLinks.schemes`, and a 256px icon,
301
+ and is packed with `mksquashfs` behind the AppImage type 2 runtime, pinned to a dated release and checked by its
302
+ digest. The AppImage needs only the WebKitGTK 4.1 and GTK 3 the folder already needs, and runs without FUSE through
303
+ `--appimage-extract-and-run`. It runs from a read-only image, so the updates plugin cannot replace it: an app with
304
+ `updates` is warned to publish a new AppImage for each release.
305
+
306
+ - ccf2ae2: A macOS desktop release can be downloaded: Developer ID signing, the hardened runtime, notarization and a dmg
307
+
308
+ `akan build-desktop` signs with a Developer ID when `AKAN_NATIVE_MACOS_IDENTITY` names one in a keychain, or
309
+ `AKAN_NATIVE_MACOS_CERTIFICATE` + `_CERTIFICATE_PASSWORD` a `.p12` (imported into a keychain made for the build and
310
+ deleted after it, for CI). Every Mach-O file is signed inside out with the hardened runtime and a secure timestamp, and
311
+ the executable gets Bun's JIT entitlements plus the camera's or microphone's when a usage text asks for them;
312
+ `native.desktop.entitlements` adds the app's own. With `AKAN_NATIVE_MACOS_NOTARY_KEY` + `_KEY_ID` + `_ISSUER` (an App
313
+ Store Connect API key) or `_NOTARY_PROFILE`, the app is notarized, stapled and checked with `spctl`.
314
+ `--installer true` on macOS adds a dmg with an Applications link, signed, notarized and stapled the same way.
315
+ `publish-update` signs a macOS release with the same identity, since the updater checks the installed app's signature.
316
+ A release that is not signed with a Developer ID, or not notarized, warns that Gatekeeper blocks a downloaded copy.
317
+
318
+ - ccf2ae2: A native platform section takes its own `indexPath`
319
+
320
+ `ios.indexPath`, `android.indexPath` and `desktop.indexPath` win over the section's (or the target's) `indexPath` on
321
+ that platform, so one target opens the phones on `/mobile` and the desktop app on `/` with no `--target`. It moves
322
+ both the dev build's first page and the `indexPath` a release bundle carries.
323
+
324
+ - ccf2ae2: Mobile apps build and run on `@akanjs/native` instead of Capacitor.
325
+
326
+ The runtime ships inside `akanjs` (vendored, not published on its own), so an app installs nothing extra for it.
327
+ It generates each target's native projects under `.akan/native/<target>/build/<platform>` and loads the web
328
+ root it assembles in `.akan/native/<target>/web` from the production CSR build — nothing native is committed.
329
+
330
+ - **Configuration (`akan.config.ts`).** A `native` section, and each of its `targets`, takes `appName`, `appId`,
331
+ `fileName`, `version`, `buildNum`, `basePath`, `indexPath`, `icon`, `splash`, `permissions`, `plugins` (builtin
332
+ ids such as `iap`), `deepLinks`, `ios.{infoPlist, entitlements, files}` and
333
+ `android.{manifest, application, activity, googleServices, files}`; a target overrides the section field by field.
334
+ `files` maps where a file lands (a path in the iOS app bundle, `res/<type>/<file>` or `assets/<path>` on Android)
335
+ to its source. A Capacitor-era key fails, naming the keys the section takes.
336
+ - **Plugins declare, they do not edit projects.** `AkanPlugin.native` names the permission it serves and what the
337
+ target then needs: native plugins, usage texts, plist and entitlement entries, Android permissions and features.
338
+ It replaces `capacitor.configureNative`, `editIosAppDelegate` and `runtimePackages`. A release build names each
339
+ plugin's default permissions as its capabilities.
340
+ - **Commands.** `build-`, `start-` and `release-ios|android` go through the runtime. `start-*` follows `akan start`
341
+ (it refuses when no dev server answers), runs one target at a time and takes `--device` (and `--team`/`-T` on
342
+ iOS); `--release` runs a release build of its own bundle. `release-ios` takes `--team` and `--ad-hoc`;
343
+ `release-android` builds an `.aab` by default (`--assemble-type apk` for an `.apk`), signed from
344
+ `MYAPP_RELEASE_STORE_FILE`, `MYAPP_RELEASE_STORE_PASSWORD` and `MYAPP_RELEASE_KEY_ALIAS` (plus
345
+ `MYAPP_RELEASE_KEY_PASSWORD` when the key has its own), which it checks before anything builds. `build-*` take
346
+ `--debug`, and
347
+ `akan doctor --ios` adds the native toolchain checks.
348
+ - **Pages.** `akanjs/client/native` wraps the native plugins a page uses, and `isNativeApp()` is true only inside
349
+ a shell. `Device` reads the runtime's device plugin and safe-area variables, `storage` the shell's preferences,
350
+ and the new `secretStorage` keeps the JWT and refresh tokens in the OS credential store (cleared on the first
351
+ launch after a reinstall). `useCamera` is one hook — the native sheet asks camera or library and answers an
352
+ upright JPEG data URL — and `useGeoLocation` answers the flat native position. A native release bundle opened at
353
+ `/` starts on its target's home, and a target opened in a browser is a web device.
354
+
355
+ **Breaking, with a migration path.**
356
+
357
+ - Delete the app's `ios/`, `android/`, `mobile/` folders and `capacitor.config.*`: `akan sync`, `akan doctor` and
358
+ `akan quality scan` now refuse them in an app root and say why.
359
+ - Move Capacitor settings to the keys above; `google-services.json` goes to `native.android.googleServices`, and
360
+ `GoogleService-Info.plist` is no longer needed.
361
+ - Removed: `-g/--regenerate`, `--open`, `--allow-provisioning-updates`, `configure-app`, `codepush`,
362
+ `release-source`, `capacitor.base.config`, `useContact`, `useCodepush`, the native path of `useSpeech`, and every
363
+ `@capacitor/*`, `@capacitor-community/*`, `@capgo/*`, `capacitor-plugin-safe-area`, `cordova-plugin-purchase`
364
+ and `@trapezedev/project` dependency — `akanjs` no longer lists them as peers.
365
+ - `baseSt` drops `deviceToken`; the notification store owns it.
366
+
367
+ - ccf2ae2: `native` in `akan.config.ts` replaces `mobile`, with each platform's settings in a section of its own
368
+
369
+ **Breaking:** `mobile` is gone with no fallback; a config that still has it, or a key that moved, stops and names the
370
+ new place. `native` and each of `native.targets` take the same fields, a target merging objects key by key and
371
+ replacing lists and every other value, and without `targets` the app has one target, `default`. The next `akan start`
372
+ removes the old `.akan/mobile`.
373
+
374
+ The agent guide `akan agent install` writes now covers native apps: the `native` section, the `start-*` / `build-*`
375
+ commands and updates, with the full contract in the `runtimeRule` guideline.
376
+
377
+ | Before | Now |
378
+ | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
379
+ | `mobile` | `native` |
380
+ | `mobile.native.X`, `targets.<t>.native.X` (`plugins`, `ios`, `android`, `desktop`) | `native.X`, `native.targets.<t>.X` |
381
+ | `native.push.android` | `android.push` |
382
+ | `native.privacy` | `ios.privacy` |
383
+ | `assets.icon`, `assets.splash` | `icon`, `splash` |
384
+ | `files["ios/<path>"]` | `ios.files["<path>"]` |
385
+ | `files["android/res/…"]`, `files["android/assets/…"]` | `android.files["res/…"]`, `android.files["assets/…"]` |
386
+ | `deepLinks.ios.teamId` | `ios.teamId` |
387
+ | `deepLinks.android.sha256CertFingerprints` | `android.sha256CertFingerprints` |
388
+ | `indexPath` on a target only | on `native` too, inherited by every target |
389
+ | root and target `plugins` joined, Android XML lists appended | the target's list replaces |
390
+ | `.akan/mobile/<target>/native/<platform>` | `.akan/native/<target>/build/<platform>` |
391
+ | `.akan/mobile/<target>/{dev,updates,web,bin}` | `.akan/native/<target>/{dev,updates,web,bin}` |
392
+ | `AkanMobileConfig`, `AkanMobileTargetConfig` | `AkanNativeAppConfig` (resolved: `AkanNativeAppResult`), `AkanNativeTarget`; `AkanNativeSettings` is what `native` and a target share |
393
+ | `AkanMobileUpdatesConfig`, `AkanMobileAppId`, `AkanMobileTargetDeepLinks` | `AkanNativeUpdatesConfig`, `AkanNativeAppId`, `AkanNativeDeepLinks` |
394
+ | `AkanMobileNativeConfig` | `AkanNativeIosConfig`, `AkanNativeAndroidConfig`, `AkanNativeDesktopConfig` |
395
+ | `AkanMobileTargetAssets`, `AkanMobileTargetFiles` | removed: `icon`, `splash` and each platform's `files` |
396
+ | `MobilePermission`, `MobileEnv`, `AppConfigResult.mobile` | `NativePermission`, `NativeEnv`, `AppConfigResult.native` |
397
+
398
+ - 5f53462: `akan subspace upload-env <name>` sends one subspace's env to the cloud workspace it deploys from
399
+
400
+ A subspace's `env/` values were the one thing the mirror had no way to move. Push holds them back and pull
401
+ leaves them alone — deliberately, because the values belong to the repo that deploys — so getting a changed
402
+ value into a customer's deployment meant running `akan upload-env` from somewhere that had both the values and
403
+ that customer's `AKAN_WORKSPACE_ID`, which is nowhere: a workspace has every customer's values and exactly one
404
+ id, its own.
405
+
406
+ `SubspaceDeclaration` now carries an optional `workspaceId`, the cloud workspace that subspace deploys from,
407
+ and `akan subspace upload-env <name>` uploads against it. `akan.subspace.ts` is a workspace-only entry that no
408
+ push ever ships, so declaring every customer's id there puts none of them in a customer's repo.
409
+
410
+ What is uploaded is the slice a push ships — the declared apps, the libraries their closure pulls in, and those
411
+ apps' `secrets` globs — and nothing else. The whole workspace would be the simpler thing to send, and it is the
412
+ wrong one: an env archive is replaced whole on arrival, so one customer's cloud workspace would end up holding
413
+ every other customer's secrets. `CloudRunner.gatherEnvFiles` takes that scope and an archive path (the slice
414
+ lands in `local/env.<name>.tar`, never over the full `local/env.tar`), while the `akan:secrets` block it
415
+ maintains in the root `.gitignore` keeps being written from every app in the workspace — narrowing that file to
416
+ one slice would silently un-ignore every other app's secrets.
417
+
418
+ Because the upload replaces an archive in a repo you are not standing in, three things are refused rather than
419
+ guessed: a subspace with no `workspaceId`, a subspace declaring this workspace's own id (a slice uploaded there
420
+ would leave the workspace's next `download-env` short of every other app), and two subspaces declaring the same
421
+ id. The command asks for confirmation before it replaces anything, naming the subspace and the cloud workspace;
422
+ `-F, --force` skips the question, and with no terminal to ask in the command refuses instead of assuming yes.
423
+ (`-f` is already `--format` and `-y` already `--verify`.)
424
+
425
+ The direction is one-way on purpose. There is no `subspace download-env`: it would write a customer's values
426
+ over the workspace's own copies, and which of the two is right is not something a command can know.
427
+
428
+ - ccf2ae2: `trustedDependencies` in `akan.config.ts` names the packages whose install scripts run where the app is installed
429
+
430
+ `bun install --production` skips every dependency's install and postinstall scripts unless the package is trusted, so
431
+ a native addon that builds itself at install time (one that ships no prebuilds) reached the image unbuilt and failed
432
+ at its first call. An app or a lib now lists those packages in `trustedDependencies`; the built `package.json`
433
+ carries the list, and both the image and a desktop app's server run their scripts. A lib's list reaches every app.
434
+
435
+ - ccf2ae2: Three grit rules keep app and lib code off the browser APIs that behave differently inside a native app, and each
436
+ message names what to use instead.
437
+
438
+ - `no-document-cookie` (error): an `app://localhost` page on iOS, macOS and Linux keeps no cookies, so
439
+ `document.cookie` reads empty and drops writes in the app. Use `getCookie` / `setCookie` / `removeCookie` from
440
+ `akanjs/client`.
441
+ - `no-web-storage` (error): `localStorage` / `sessionStorage` work in the shells but bypass the store akanjs picks
442
+ per platform and throw during SSR. Use `storage`, or `secretStorage` for credentials, from `akanjs/client`.
443
+ - `no-web-only-api-outside-webkit` (warning): `navigator.share` / `canShare`, `navigator.serviceWorker`,
444
+ `Notification.requestPermission` / `.permission`, `navigator.geolocation` and `navigator.vibrate` belong in a
445
+ `webkit/` hook that branches on `isNativeApp()`.
446
+
447
+ The first two apply to `apps/**` and `libs/**` outside tests, the third to the same files outside `webkit/`.
448
+
449
+ **Breaking for a workspace that reads Web Storage or cookies directly:** `akan lint` now fails there. Move the
450
+ call to the `akanjs/client` helpers, or keep it with `// biome-ignore lint/plugin: <reason>`.
451
+
452
+ - ccf2ae2: A Windows desktop release is signed with Authenticode
453
+
454
+ With `AKAN_NATIVE_WINDOWS_CERTIFICATE` + `_CERTIFICATE_PASSWORD` (a `.pfx`), `AKAN_NATIVE_WINDOWS_THUMBPRINT` (a
455
+ certificate in the store) or `AKAN_NATIVE_WINDOWS_SIGN_COMMAND` (a JSON array run once per file with `{file}`, for Azure
456
+ Trusted Signing or a cloud HSM), `akan build-desktop` signs every PE file of the app — the executable, its DLL, the
457
+ server's addons and `bin` — SHA-256 with an RFC 3161 timestamp (`AKAN_NATIVE_WINDOWS_TIMESTAMP_URL`), and verifies them.
458
+ `--installer` signs the setup program and, through makensis's `!uninstfinalize`, the uninstaller it writes; the
459
+ settings reach the signer through the environment only. `publish-update` signs a Windows release the same way, and an
460
+ unsigned release build warns that SmartScreen flags a downloaded copy.
461
+
462
+ ### Patch Changes
463
+
464
+ - 5f53462: fix(tunnel): reach the tunnel control plane at its real path, and report the status instead of parsing the error page
465
+
466
+ `akan tunnel` and `akan start --share` could not open a share at all: `CloudApi` called `/api/requestTunnel`,
467
+ `/api/tunnelListInSelf` and `/api/revokeTunnel`, none of which exist. An endpoint's path carries the model's
468
+ refName — `fetch.serializer.ts` takes it from `sliceCls.srv.cnst?.refName` — so every database module's endpoints
469
+ sit under a segment of their own and the service modules beside them do not. `tunnel` is a database module; its
470
+ neighbours in the same file, `uploadEnv` and `getRemoteSelf`, belong to the `_cloud` service module and have no
471
+ `cnst` to take a prefix from. The three paths now carry `/tunnel/`, with a note saying why they differ from the
472
+ methods above them.
473
+
474
+ **The 404 was invisible, which is the more expensive half.** `HttpClient.get` and `post` called `response.json()`
475
+ without looking at `response.ok`, so the control plane's HTML error page reached the user as
476
+ `JSON Parse error: Unrecognized token '<'` — a parse error where the status line had already said `404 Not Found`.
477
+ Both now read the status first and throw with it, matching `getFile`, which has always done so.
478
+
479
+ Three failures were being swallowed the same way and now surface:
480
+
481
+ - `uploadEnv` returned the parsed error body, which is a truthy object, so **a failed env upload reported success**
482
+ and the project went on to build against env that never arrived.
483
+ - `getRemoteSelf` and `getRemoteAuthToken` are written to catch and answer `null`; with nothing thrown they
484
+ answered an error object instead.
485
+ - `refreshAuthToken` fed a rejected response straight into `toAccessToken`, minting a token from it.
486
+
487
+ - ccf2ae2: fix: a dev server whose replica crash-loops reports it at once
488
+
489
+ When an app's `init` threw at every boot, the gateway gave up on the replica and waited for a code change, but the dev
490
+ host kept reporting the app as starting: `akan start` showed no reason and held its next wave of apps for 180 s, and
491
+ `akan start-desktop` waited 180 s before it gave up. The host now reports the app `failed` with the gateway's message
492
+ as soon as it gives up; `akan start` keeps running and shows the app ready again after the fix is saved.
493
+
494
+ - ccf2ae2: A CSR page or layout whose async render throws says so, a cached page whose layout redirected renders again, and a
495
+ dev client module that imports a macro builds.
496
+
497
+ - An async render that rejected used to leave its layer blank with nothing in the console, since the promise's
498
+ failure was dropped. It is now logged with the stack and the layer it belongs to (`render of page 2 of
499
+ /:lang/home failed: …`).
500
+ - `akan:debug:frame` takes `memory` besides `1`: the frame trace goes to `window.__AKAN_FRAME_TRACE__` (the last
501
+ 2000 events) instead of the console. A native dev build mirrors each console call over the bridge, which slows the
502
+ frame enough to hide a timing bug; `memory` does not. Async page and layout renders add `layer.render`,
503
+ `layer.settle` and `layer.fail` events.
504
+ - A cached page whose layout redirected renders that layout again when it is back on screen. Signed out, a
505
+ layout that sends the person to the sign-in page kept its empty result, so the page stayed blank after the sign-in
506
+ brought them back (a native app opens its home first, so it always did). A render that issued a redirect while it
507
+ ran is now rerun the next time its page becomes current; `router.redirectCount()` is what it compares.
508
+ - `window.__AKAN_DUMP_FRAME__()` reports the frame's location, stack and phase and every recent async page and
509
+ layout render: settled or pending, still awaited, drawn. It records only the latest state, so it does not change
510
+ the timing a trace would.
511
+ - `akan start` no longer fails a dev client build with `ReferenceError: __akanMeta is not defined` when a file
512
+ imports a macro (`useClient.ts`'s `with { type: "macro" }`) whose modules read `import.meta` at the top level.
513
+ Bun applied the dev build's `import.meta` rewrite to the macro's modules too, and ran them where nothing declared
514
+ the rewritten name; such a file now has its macros run by a build of its own first.
515
+
516
+ - ccf2ae2: A native dev build's API calls and sockets go through the dev gateway, so a phone on the same network — or the
517
+ simulator, the emulator and a desktop app — reaches `akan start` without a reversed port.
518
+
519
+ - A page the gateway served calls its own origin: `getEnv().serverHttpUri` is `<page origin><api prefix>` and the
520
+ websocket follows it, unless `AKAN_PUBLIC_SERVER_URL` names a server. A release build is unchanged.
521
+ - The iOS, Android and desktop hosts carry the method, the body and the request headers, `authorization`
522
+ included, to the gateway; the gateway relays the app's `<prefix><websocketPrefix>` socket. Android hands the
523
+ host no request body, so a call that carries one goes to the gateway's `http://localhost` origin directly and
524
+ the server answers its CORS.
525
+ - A desktop dev build opens the target's start page instead of `/`, and builds when the web root has no
526
+ `index.html` yet.
527
+ - `NativeWebDir` lists the web root in `/`-separated paths on Windows too.
528
+
529
+ - ccf2ae2: A native app can sign in through the system browser and hear the callback on every desktop OS.
530
+
531
+ - `akanjs/client/native` exports `authSession` and its `AuthSessionApi` type, the runtime's system-browser sign-in,
532
+ and `isNativeShell()`, true in any native shell; `isNativeApp()` stays iOS and Android only.
533
+ - `getServerOrigin()` in `akanjs/base` is the server's origin as a browser outside the page opens it. For a page the
534
+ native dev gateway served, that is the dev server (`http://localhost:<dev port>`), not the app origin the page
535
+ calls; otherwise it is the origin of `getEnv().serverHttpUri`.
536
+ - A native dev build on Android reverses the dev server's port as well as the gateway's, so a browser on the device
537
+ reaches it.
538
+ - A target with `deepLinks.schemes` ships `single-instance`. Windows and Linux open a link by starting the app again,
539
+ and the hand-over is how the link reaches the running app. A second launch of such an app now hands over and exits
540
+ instead of opening a second window, and a desktop debug build keeps the release app id, so it does the same while
541
+ the release app runs.
542
+ - In an app with basePaths whose only target names none, `start-*` and `build-*` without `--target` ask for a basePath
543
+ instead of opening the root, which has no CSR page.
544
+
545
+ - 3ebd8e1: fix(agent): stop a workspace's agent guide and lint config from freezing at the release that created it
546
+
547
+ `createWorkspace` copies the whole `templates/workspaceRoot` tree — 21 `.cursor/rules/*.mdc`, `AGENTS.md`,
548
+ `biome.json`, `tsconfig.json` — but the only maintenance path, `generateAgentRules`, rewrites three of them, and
549
+ `akan agent install` refreshes only the `akan:agent` block inside `AGENTS.md`. Twenty-one files created, three
550
+ maintained: everything else was a one-time copy that no framework release could ever reach. Adding one lint rule
551
+ meant writing the same sentence into `AGENTS.md`, `.cursor/rules/lint-enforced-rules.mdc`, its `.mdc.template`,
552
+ `AGENTS.md.template`, `biome.json`, and `biome.json.template` — and even then, no existing workspace saw it.
553
+
554
+ Four changes, ordered by how much drift each removes.
555
+
556
+ **Lint config now extends a config shipped in the package.** `@akanjs/devkit/biome.base.json` carries the
557
+ formatter, the rule set, and every grit plugin registration; a workspace `biome.json` is `extends` plus its own
558
+ `files.includes`. Biome resolves the specifier through node*modules (it does not consult the package `exports`
559
+ map), and `plugins` paths inside an extended config resolve from the entry config's directory, so the
560
+ `./node_modules/@akanjs/devkit/lint/*.grit` form the template already used is correct. Rule changes now reach a
561
+ workspace on `bun update` with no command to run. Two merge behaviours decided the split: `overrides` concatenate,
562
+ so the generated-file opt-out moved there from `files.includes`, which \_replaces* and would silently drop the base
563
+ list the moment a workspace added one exclusion of its own. Because Biome moves rules between groups across minors
564
+ (`noUnnecessaryConditions` is `nursery` at 2.4 and `suspicious` at 2.5, and the old position is a hard error), the
565
+ base config and the Biome version travel together: `biomeBase.ts` pins the version `createWorkspace` installs
566
+ instead of resolving `latest`.
567
+
568
+ **The conventions body moved into the managed block.** It now ships as the `conventions` guideline and renders
569
+ between the `akan:agent` markers, so `akan agent install` refreshes it. `AGENTS.md.template` carried a _second_,
570
+ independently written 990-line convention guide — not a copy of the repo's 791-line one, a parallel fork — which
571
+ became the `workspaceOnboarding` guideline. Onboarding renders only outside the framework monorepo, where it would
572
+ otherwise double an always-loaded file for readers who are changing Akan rather than building on it. The two
573
+ guides still overlap and would repay a merge.
574
+
575
+ **The 20 duplicate Cursor rules are gone.** `akan.mdc` is `alwaysApply: true` plus `@AGENTS.md`, so the per-topic
576
+ `.mdc` files were a second copy of the same rules drifting on their own schedule. This repo had all 20 and no
577
+ `akan.mdc` at all; it now has the pointer and nothing else.
578
+
579
+ **`akan doctor` reports drift.** The block is stamped with the `@akanjs/devkit` version that rendered it, and
580
+ doctor warns `agent-guide-stale` (or `agent-guide-unstamped`) with `akan agent install agents-md` as the repair.
581
+
582
+ **`repoName` no longer comes from the folder name.** `WorkspaceExecutor.fromRoot` defaulted to
583
+ `path.basename(process.cwd())`, so every generated file that names the repo — the `AGENTS.md` title, its `- Repo:`
584
+ line — depended on what each person called the directory they cloned into, and the committed diff never settled.
585
+ It now resolves from the `origin` remote, falling back to the directory name when there is no git or no remote.
586
+ `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes, cache keys,
587
+ secret paths) which a monorepo hosting several products legitimately points elsewhere.
588
+
589
+ Two bugs surfaced on the way. `## Validation` listed `akan quality scan` and `akan quality ssr` in a _generated_
590
+ section — hand-added, and due to be wiped by the next install; they are in the generator's list now. And
591
+ `no-throw-raw-error` exempted `apps/akan/env/**` by name, so `apps/minimal/env/*` failed a rule the guide said did
592
+ not apply to `env/`; the exemption is `**/env/**`. Lint output over the whole repo is otherwise byte-identical
593
+ before and after, and formatter output is unchanged.
594
+
595
+ Biome itself moves to **2.5.8** in the same change, so the shared config targets the current release rather than
596
+ freezing the ecosystem at 2.4.4 — workspaces created before this already installed a 2.5.x, and pinning backwards
597
+ would have made adoption a downgrade. The jump adds 263 diagnostics across the monorepo, but only **32 of them are
598
+ in `apps/` and `libs/`** (30 `noUnnecessaryConditions`, 2 `useArrayFind`) and the rest are in the framework's own
599
+ `pkgs/`. No scope regressed from passing to failing: `akan lint minimal`, the one green scope, is still green, and
600
+ `apps/akan`, `libs/util`, and `libs/shared` were already failing on 2.4.4 with pre-existing errors. Every grit
601
+ plugin was re-verified against 2.5.8. `noUnnecessaryConditions` stays at `error` and now reports 220 repo-wide,
602
+ 190 of them in `pkgs/` — a real backlog, and a deliberate one to leave visible rather than downgrade to a warning.
603
+
604
+ Existing workspaces keep working untouched — they simply stay frozen at the release that created them, which
605
+ `akan doctor` now reports. The migration guide walks through adopting the change in four steps, only one of which
606
+ needs a human decision.
607
+
608
+ - ccf2ae2: `akan.config.ts` reaches the native settings a store release needs, and an app keeps a different id on each store.
609
+
610
+ - `native.android.push` (channel, `smallIcon` relative to the app folder, `color`) and `native.ios.privacy` (the iOS
611
+ privacy manifest) pass through, per target too.
612
+ - `native.icon` takes `{ image, backgroundColor }` and `native.splash` the runtime's `{ image, backgroundColor,
613
+ autoHide, timeout }`, besides a path.
614
+ - `appId` is a string or one per platform — `{ ios, android, default }` — for an app whose listings already carry
615
+ different ids. The app-link files serve each platform's own id.
616
+
617
+ - 2d8de1c: fix(build): a barrel's own export wins over the same name a star re-export brings when imports are flattened
618
+
619
+ The `barrelImports` rewrite turns `import { X } from "pkg"` into an import of the file `X` lives in. It read a barrel
620
+ top to bottom and kept the first file it met for each name, so in `export * from "./a"; export { X } from "./c";`
621
+ it sent `X` to `./a` — while in JavaScript a module's own export, named or declared, shadows whatever its stars
622
+ bring, and `import("pkg").X` is `./c`'s. A flattened import could therefore bind a different value than the
623
+ unflattened one. Each module's own exports are now mapped before its stars are walked, which is the ES rule; a name
624
+ two stars both bring still goes to the first. No barrel in this repository had such a name, so its builds are
625
+ unchanged.
626
+
627
+ - c45ac6c: perf(build): bundle every route's client entries in one pass instead of one per route
628
+
629
+ `AllRoutesBuilder` ran a `RouteClientBuilder` per route, and each one calls `Bun.build`. Entries were already
630
+ deduplicated globally through `knownEntries` — 169 routes produced 128 client entries — but **chunk splitting is
631
+ scoped to a single `Bun.build` invocation**, so any dependency shared by entries that landed in different
632
+ invocations was emitted once per invocation.
633
+
634
+ In `apps/akan` that meant four complete copies of mermaid's parser layer. Six client entries reach
635
+ `import("mermaid")` — `pkgs/akanjs/ui/Constant/Mermaid.tsx`, `Constant/Doc.tsx`, `apps/akan/ui/ModalTests.tsx`,
636
+ `Docs/ConstantDocsDemo.tsx`, `Docs/Mermaid.tsx`, `SampleSchema.tsx` — and they were first discovered by four
637
+ different routes, so each pass emitted its own 32-chunk, 868KB copy. The macro payload in `lib/useClient.ts` was
638
+ duplicated the same way.
639
+
640
+ Discovery now runs for every route first and the collected entries go through one build. Discovery does no
641
+ bundling and is already cached per file, so the extra pass costs almost nothing.
642
+
643
+ | | before | after |
644
+ | --------------------------- | ------ | ------ |
645
+ | `.akan/artifact/client` | 26 MB | 8.1 MB |
646
+ | `.akan/artifact/client-ssr` | 17 MB | 4.7 MB |
647
+ | chunks | 605 | 258 |
648
+ | mermaid copies | 4 | 1 |
649
+ | build time | 12.9s | 10.1s |
650
+
651
+ Client entries stay at 128 and the emitted manifest is unchanged: same 169 `routeIds` in the same order, the same
652
+ 151 `clientManifest` keys, and the same 114 `ssrManifest.moduleMap` entries. mermaid is still reached through
653
+ `await import(...)`, so its chunk stays lazy and out of the initial load.
654
+
655
+ **Only the production path changes.** `AllRoutesBuilder` is constructed once, by `applicationBuildRunner` with
656
+ `command: "build"`. Dev keeps one build per route through `incrementalBuilder`, which is what makes an incremental
657
+ rebuild cheap, and `discoveredEntries` / `clientDepsByEntry` — consumed only there — are untouched.
658
+ `RouteClientBuilder` takes an optional `entries` list that skips discovery; dev never passes it.
659
+
660
+ - 2d8de1c: fix(build): each reason a failed bundle gives names the file and line it points at
661
+
662
+ `akan build` and `akan start` listed the bundler's reasons under "Bundle failed" as bare messages, so
663
+ `Could not resolve: "./not-there"` never said which file imported it. Each reason now carries the location Bun
664
+ reports, relative to the workspace root:
665
+
666
+ ```
667
+ Bundle failed
668
+ Could not resolve: "./not-there" (apps/demo/ui/Broken.tsx:2:22)
669
+ ```
670
+
671
+ A reason Bun gives no location for prints as before, and a file outside the workspace keeps its absolute path. An
672
+ error a plugin raised while loading a file names that file without a line, unless its message already names it.
673
+
674
+ - ccf2ae2: fix(dev): the incremental builder's default memory ceiling is 2GB, up from 1.2GB
675
+
676
+ - With no `AKAN_BUILDER_MAX_RSS_MB` set and no memory limit to take a share of, `akan start` now recycles its
677
+ builder past 2GB instead of 1.2GB. apps/akan's builder peaks near 950MB holding the SSR registry's patcher through
678
+ a route build, and arming CSR adds a second patcher, which left 1.2GB little room above ordinary work.
679
+ - A container limit or `AKAN_MEMORY_LIMIT` still sets the ceiling at 35% of it, and `AKAN_BUILDER_MAX_RSS_MB=0` still
680
+ leaves the builder unbounded.
681
+
682
+ - 5f53462: fix(cli): scaffolds name their guards, and the mobile and package commands do what they say
683
+
684
+ Guards and templates:
685
+
686
+ - The module scaffold's slice is `{ root: Admin, get: Public, cru: Admin }` when the workspace mounts libs/shared,
687
+ and `{ root: None, get: Public, cru: None }` — closed until you name a guard — when it does not; its sample slice
688
+ and the sample app's `inPublic` take `init({ guards: [Public] })`. `cru: Public` is gone.
689
+ - `akan add-mutation` / `add-slice` write `mutation(Boolean, { guards: [None] })` / `init({ guards: [None] })` and
690
+ import `None`, and their surface step returns a review note instead of stopping as unsupported.
691
+ - The sample app's `SignedIn` admits only a caller with an identity (`id`, or libs/shared's `self` / `me`); a guest
692
+ carries an account object too and used to pass. `CurrentUserId` reads the same three places, and the sample's
693
+ task fixtures sign a user in through libs/shared when it is installed. Its spec holds no assertions.
694
+ - Every scaffolded abstract has the current shape — a title, one sentence and `## Rules` — instead of six headings
695
+ of placeholder text: `create-module` writes `# <model> Abstract` with its guard and soft-removal rules,
696
+ `create-service` writes `# <service> Service Abstract` and `create-scalar` `# <scalar> Abstract`, each with a
697
+ placeholder sentence and two placeholder rules. The sample app's task, noti and workHistory abstracts state their
698
+ real invariants instead of describing the module layout. The dead `module/__model__.abstract.md` template is
699
+ removed.
700
+ - `create-crud-page` imports from the module's own client (`@libs/<lib>/client` for a lib module), and
701
+ `create-package` writes a tsconfig that extends the workspace root from any depth and references no missing file.
702
+ - `akan add-field --default now` (and every Date default) writes a thunk, `() => dayjs()`.
703
+
704
+ CLI and mobile:
705
+
706
+ - iOS usage-description keys map to Apple's real Info.plist keys (`NSPhotoLibraryAddUsageDescription`,
707
+ `NSPhotoLibraryUsageDescription`, both location-always keys) instead of `NS` + the option name.
708
+ - A non-main server's `assetlinks.json` also lists `<appId>.debug`, so a debug build's App Links can verify.
709
+ - `start-ios` takes `--no-allow-provisioning-updates` (short flag `-a`); every boolean option that defaults to
710
+ `true` now accepts `--no-<name>`.
711
+ - `akan update` installs `@akanjs/cli@<tag>`, the package that provides the `akan` binary.
712
+ - `sync-package` reads package dependencies again (the tsconfig paths start with `./pkgs/`).
713
+ - The quality scanner no longer recommends the nonexistent `akan compact`; `AbstractDoc.compactMinLines` is gone.
714
+ - `--role` lists `federation`, and `--since` names its units.
715
+
716
+ - 3ebd8e1: feat(lint): enforce the client/server import boundary in both directions
717
+
718
+ The layering was documented but nothing checked it. A `.tsx` could `import * as db from "../db"`, a `*.service.ts`
719
+ could `import { st } from "@libs/shared/client"`, and a `*.constant.ts` could reach either side — each one a value
720
+ edge that drags the whole opposite graph along. In the client direction that means the database driver,
721
+ `node:crypto`, and any secret resolved from `process.env` land in the browser bundle; in the server direction it
722
+ means React, the store, and the browser globals underneath them load into every CLI command, worker, and migration
723
+ that touches a service. Both usually fail the build rather than failing at runtime, and the failure names a
724
+ transitive module, never the import that caused it.
725
+
726
+ Two rules now hold the line, symmetric by design:
727
+
728
+ - `no-import-server-in-client.grit` runs on client files (`ui/`, `webkit/`, `page/`, `*.store.ts`, every `.tsx`) and
729
+ bans `*.document` / `*.dictionary` / `*.service` / `*.signal` modules, `srvkit/`, package `server` entrypoints
730
+ including `akanjs/server`, and the `db` / `srv` / `sig` / `dict` / `option` / `useServer` barrels.
731
+ - `no-import-client-in-server.grit` runs on server files (those four suffixes plus `srvkit/`) and bans `*.store`
732
+ modules, module components (`*.Template` / `*.Unit` / `*.Util` / `*.View` / `*.Zone`), `ui/`, `webkit/`, package
733
+ `client` entrypoints including `akanjs/client`, and the `st` / `store` / `useClient` barrels.
734
+
735
+ Shared files — `common/` and `*.constant.ts` — are wired into **both** overrides, so they reach neither side, which
736
+ is what makes them safe for either to import.
737
+
738
+ `import type` is exempt in both directions: it is erased before bundling, so it emits no edge, and a shared file
739
+ that only needs to name a server-side type (`libs/shared/lib/summary/summary.constant.ts` names `UserFilter` from
740
+ `user.document`) stays legal. A mixed value-and-type import is not exempt — it emits a real edge. Test files are
741
+ excluded, as are `pkgs/akanjs/**`, which implements the boundary and is where the two graphs legitimately meet.
742
+
743
+ Both rules land green: no `apps/**` or `libs/**` file crosses the boundary today, so this is a ratchet, not a
744
+ migration.
745
+
746
+ - 2d8de1c: fix(cloud): an `akan` command started while another is refreshing the cloud session waits for it
747
+
748
+ A command that read `~/.akan/config.json` while another process was mid-refresh saw the session with its refresh
749
+ token already taken and went ahead without signing in. It now waits for that refresh under the same lock and uses the
750
+ session it stored.
751
+
752
+ - 2d8de1c: fix(cloud): a cloud session in its last hour is refreshed, once across every running `akan` process
753
+
754
+ A stored cloud session within an hour of expiry, or past it, was treated as signed out: nothing ever called the
755
+ refresh endpoint, so `akan login` reopened the browser about once a week and `akan tunnel` answered a raw 401 in
756
+ that window. Every command that reaches the cloud — `login`, `upload-env`, `download-env`, `tunnel` and
757
+ `start --share` — now refreshes such a session before using it, and `akan login` only opens the browser when the
758
+ refresh fails.
759
+
760
+ The refresh is written for a cloud that rotates the refresh token on every use and treats a second use as theft,
761
+ revoking every session of the account, browser included:
762
+
763
+ - It runs under a lock beside `~/.akan/config.json`, held across CLI processes, and re-reads the file once it has
764
+ the lock, so of two commands started together only one refreshes and the other uses its result. A lock whose
765
+ holder has exited is taken over; one whose holder is still running is waited for, and a refresh is never made
766
+ without it.
767
+ - The refresh token leaves the file before it is sent, and the new pair is written before it is used. A command
768
+ killed mid-refresh, or a refresh that fails, therefore leaves no token that could be presented a second time;
769
+ the cost is one browser sign-in.
770
+ - Every other write to the file (hosts, remote env servers, test targets) re-reads under the same lock, so none can
771
+ put back a token that was rotated away, and the file is replaced atomically with mode 0600, so a concurrent
772
+ reader never sees half of it.
773
+
774
+ - 8c31d54: fix: `akan code` says when compaction failed, leaves the Claude Code-sized buffer, and resolves its default model
775
+
776
+ **A failed compaction read as a successful one.** The engine reports a compaction that failed or was cancelled on
777
+ the same `compaction_end` event as one that worked, with `errorMessage` and `aborted` beside the reason. The wire
778
+ kept only the reason, so every end printed "Compacted the conversation" — including an automatic compaction that
779
+ had failed, which nobody else reports, leaving the next sign of it an overflow error turns later. The `compaction`
780
+ frame now carries `error` and `aborted`, and the transcript prints the engine's own sentence as a warning, or
781
+ "Compaction cancelled.". A failed `/compact` still reports once, through the caller it threw to.
782
+
783
+ **The buffer is 32,768 tokens, up from the engine's 16,384.** On a million-token window that moves the threshold
784
+ from 98.4% to 96.7% — where Claude Code compacts. The reserve is all the room the response at the threshold gets
785
+ (the request clamps its output to the window less the prompt less 4,096), so at 16k a reasoning model ran out
786
+ mid-thought there and the recovery re-sent a near-full window to retry. It is also the summary's output cap at 80%.
787
+ The small-window warning now reads the settings the session actually runs with rather than the engine's defaults.
788
+
789
+ **The default model is `deepseek/deepseek-flash`.** `deepseek-v4-flash` is gone from both the engine's catalogue
790
+ and DeepSeek's own model list, so the default resolved to nothing and `akan code` silently ran on whichever model
791
+ sorted first among those with a key.
792
+
793
+ - 5f53462: fix(console): a production `akan console` brings services up and starts no schedule
794
+
795
+ The `console.js` an `akan build` writes never marked its process as a console, so `server.start()` took the replica
796
+ branch: it registered every `interval` / `cron` / queue worker, ran every internal `init()`, and opened the log
797
+ transport — a second scheduler inside a running deployment. The shim now marks the process before it loads the
798
+ server, the same way `akan console` does in development, so a console runs service and adaptor `onInit` and nothing
799
+ the internals schedule.
800
+
801
+ `assertAkanConsoleAllowed()` was handed `server.env`, which carries neither `environment` nor `operationMode`, so it
802
+ effectively checked `NODE_ENV` alone. Called with no argument it now reads the deployment from `AKAN_PUBLIC_ENV` /
803
+ `AKAN_PUBLIC_OPERATION_MODE` (deriving the mode the way `getEnv()` does): a `main` environment, a `cloud` or `edge`
804
+ operation mode, or `NODE_ENV=production` refuses the console unless `AKAN_CONSOLE=1`.
805
+
806
+ - ccf2ae2: fix(dev): a CSR dev save reaches the open tabs before `app.js` is rewritten, and a tab opened in between boots once
807
+
808
+ A dev CSR save used to rewrite the whole `app.js` before telling the open tabs about the patch, although only a tab
809
+ that boots later needs it. The patch is now announced first and `app.js` follows. The manifest records which
810
+ generation `app.js` holds (`appGeneration`), and the server holds a booting tab's `app.js?g=N` for up to two seconds
811
+ until the file holds `N`, instead of booting it one generation behind and reloading it again. A build that died in
812
+ that gap leaves `app.js` behind; the next update rewrites it even when nothing else changed.
813
+
814
+ - ccf2ae2: perf(dev): a CSR patch reaches the tabs before its module files and graph.json are written
815
+
816
+ The dev registry announced a patch only after writing each changed module's file and the whole `graph.json`, which
817
+ only the next build reads. The patch file and the manifest now go first, and the rest follows the announcement; a
818
+ build that dies in between recompiles those files on its next run, since their mtimes no longer match the graph. The
819
+ dev entry also stopped parsing every page with TypeScript on each save to see whether its default export is async:
820
+ the result is kept per file until the file changes. On apps/akan a component save now reaches the tabs 72ms after the
821
+ builder picks it up, down from about 200ms.
822
+
823
+ - ccf2ae2: perf(dev): a CSR dev save is patched by the resident builder, without spawning a build worker
824
+
825
+ - The dev builder keeps the CSR registry's graph, manifest and module factories in memory and compiles a save itself.
826
+ A save no longer pays for a build worker's spawn and imports (about 130ms on minimal) or a pass over every module
827
+ file, and `app.js` is rebuilt from memory after the patch is announced.
828
+ - A save that needs a whole-app build still goes to a build worker, which exits and returns its memory: a first build,
829
+ a config or signal/dictionary change, a new npm module, and an import no recorded resolution answers.
830
+ - The builder works in two lanes. A save's codegen and CSR patch run in one, and the file watcher waits only for it;
831
+ build workers, route builds and client-entry discovery run in the other, where a batch queued behind another folds
832
+ into it. Consecutive saves no longer wait behind each other's pages and css builds, and css now builds before pages.
833
+ - `AKAN_DEV_CSR_PATCHER=off` sends every CSR save to a build worker as before. A builder that dies mid-patch turns the
834
+ patcher off until the dev server next replaces its builder and backend together (a config, signal or dictionary
835
+ change), and says so.
836
+
837
+ - 69f7178: fix(css): fail the build on a stylesheet import that resolves to nothing, and let a lib own its design tokens
838
+
839
+ Two halves of the same problem, reported together. An `@import` the pipeline could not resolve was allowed to
840
+ produce nothing, and a lib had nowhere to declare the colours its own components pin — so the workaround was to
841
+ copy one vendor palette into every app's stylesheet, where a single missed import broke that app alone, silently.
842
+ Since vocabulary closure (`@theme { --color-*: initial }`) makes a component whose token declaration never
843
+ arrived render unstyled rather than wrong, the quiet half is the dangerous one.
844
+
845
+ **Every specifier is verified, path-shaped ones included.** `@import "./missing.css"` and
846
+ `@import "../../../libs/shared/ui/brand.css"` used to be turned into a path without asking whether a file was
847
+ there, surfacing later as a bare `ENOENT` that names no importer — or, in the dev server, as one log line while
848
+ the previous CSS kept serving. They now raise the same `[css] failed to resolve stylesheet import "…" from …`
849
+ error a bare specifier already did, naming the path that was tried.
850
+
851
+ **A package subpath resolves literally or not at all.** `@import "@libs/shared/ui/brand.css"` fell back to the
852
+ package's own `style` entry (`pkg.exports["."].style`, `pkg.style`, `index.css`) when the subpath was not in
853
+ `exports` — loading a _different_ stylesheet than the author asked for and reporting success. The fallback now
854
+ applies only to the bare package name.
855
+
856
+ **`libs/<lib>/ui/tokens.css` is the one CSS file a lib owns.** Plain `:root` custom properties for colours pinned
857
+ by someone else's brand guide — Kakao `#fee500`, Naver `#1ec800` — or by a fixed surface. Every app whose page
858
+ graph reaches that lib compiles the file automatically, ordered ahead of the app's own stylesheets so the app
859
+ stays the last word on any variable both declare. Nothing is imported by hand and adding an app cannot forget it.
860
+ Reference them as `bg-[var(--kakao)]`, which `no-arbitrary-color` allows by design; `@theme` extensions stay in
861
+ the app stylesheet, because the colour vocabulary is closed per stylesheet.
862
+
863
+ **A stylesheet under `page/` that no route imports now warns.** It compiles to nothing and reports success, which
864
+ reads exactly like an empty theme — the failure the two fixes above cannot see, because nothing ever asked for
865
+ the file.
866
+
867
+ **An `@import` that resolved, was read, and still contributed nothing now warns twice over.** Custom properties
868
+ declared outside `@theme` pass through Tailwind verbatim, so a stylesheet whose every declaration is missing from
869
+ the output did not make it in, whatever dropped it — the last shape of this bug that no check upstream of the
870
+ output can see. The compiled text is checked per compiled stylesheet, and the written asset is checked again
871
+ against the imports of its own base path, because those are two different places a declaration can go missing: a
872
+ build that ships a token to the CSR bundle and not to `styles/<base>-<hash>.css` renders unstyled on a
873
+ server-rendered page while looking correct in the browser bundle. Each resolution also logs at `verbose` with the
874
+ specifier, the importer, the resolved path, and the byte count.
875
+
876
+ A name that survives only because something else declares it — an app overriding a lib token, which is the
877
+ ordering this release introduces — keeps the check quiet: the warning fires only when _no_ declaration of the
878
+ imported file's own names is present anywhere in the asset.
879
+
880
+ - 22aa04f: fix(devkit): stop a labelled tuple type from wiping every variant out of the build
881
+
882
+ Since arbitrary variants were kept whole, a candidate could open with `[`, which also made every
883
+ labelled tuple type in the tree look like a Tailwind arbitrary property —
884
+ `[key: string, make: () => unknown]` in `constant/getDefault.ts`,
885
+ `[setQueryArgs: (...) => QueryArgs, options?: FetchPolicy]` in `store/action.ts`. Tailwind compiled each
886
+ into a rule whose declaration was TypeScript, and the lightningcss pass in `optimize()` could not parse
887
+ it. Its error recovery does not stop at the offending rule: on `apps/minimal` it took 33KB of the
888
+ stylesheet with it, and what it took was **every variant** — no `hover:`, no `focus-visible:`, no
889
+ `disabled:`, no `sm:`/`md:`, no `dark:`, in a build that reported no error.
890
+
891
+ What that looked like: buttons and badges with no hover or press feedback, inputs with the browser's
892
+ own blue focus ring because `focus-visible:outline-none` was gone, a Switch whose knob never moved, a
893
+ Tooltip that never appeared, a Popconfirm that lost its offset, and modals stretched to full width with
894
+ their `sm:`/`md:` sizing missing. All of it from two type annotations.
895
+
896
+ Brackets in a candidate may no longer contain whitespace. Tailwind spells a space in an arbitrary value
897
+ `_`, so nothing legitimate is lost — `[&_td]:px-3` and `[&>*]:gap-2` still scan whole — while a tuple
898
+ type, which always has a space after its `:`, no longer reaches the compiler. The cache version is
899
+ bumped so extractions cached under the old pattern are re-read.
900
+
901
+ - 5f53462: `akan start` sizes its boot wave against the machine instead of always booting one app at a time.
902
+
903
+ - With no `--concurrency`, the wave is `min(apps, half the memory budget / ~1.8GB per app, cores / 4)` and never
904
+ below one, so a laptop boots its apps together while a small container still staggers them — which is the case
905
+ the old default of 1 existed for. 1.8GB is a cold boot's peak: the SSR registry's boot build adds a build worker
906
+ to every boot, and apps/akan's whole dev host peaks at about 1.76GB.
907
+ - The next wave starts once each app of the previous one serves and its builder's boot builds have settled — the SSR
908
+ registry's, and CSR's when `AKAN_DEV_CSR_REBUILD=1` arms it — or 30 seconds after it serves. The app shows as ready,
909
+ and `--open` opens it, as soon as it serves.
910
+ - The memory budget is the smaller of the host's RAM and `AKAN_MEMORY_LIMIT` / the cgroup limit. `os.freemem()` is
911
+ not consulted: it counts free pages rather than reclaimable ones and reports ~0.3GB on an idle 48GB laptop, which
912
+ would pin every machine to one app at a time.
913
+ - The session prints the wave size and what set it, so a boot that still waits app by app says why.
914
+ - `--concurrency <n>` is unchanged as an override and is clamped to the number of apps selected.
915
+
916
+ - ccf2ae2: feat(dev): a dev save now says where its HMR latency went
917
+
918
+ - Each save carries epoch-ms marks from the file watcher to the page: the first fs event and the watcher's flush,
919
+ the builder picking the batch up, the build worker's spawn, start and imports, the update on disk, the send, the
920
+ backend's broadcast, and the page receiving and applying it. The page keeps the last 64 in `__AKAN_HMR_TRACES__`.
921
+ - `AKAN_DEV_WATCH_DEBOUNCE_MS` sets how long the watcher collects changes before it hands a batch over.
922
+
923
+ - ccf2ae2: The dev module registries load packages that used to take the whole vendor file down with them
924
+
925
+ - A module that reads `import.meta` (jotai's and zustand's ESM builds) no longer makes the vendor file a SyntaxError:
926
+ `import.meta` is the module's own `{ url }`, as in a browser module, with no `env`, so an `import.meta.env` check
927
+ branches as it does in the ESM bundle. `import.meta.url` is an http URL instead of this machine's `file://` path.
928
+ - A package esbuild already bundled (the `@mermaid-js/parser` chunks) keeps its own `__reExport` / `__toESM` helpers
929
+ instead of stopping the registry build, and a package file's `#!` line no longer breaks its vendor file.
930
+ - A file the browser build tree-shook away (vfile, behind a side-effect-free barrel) resolves its imports with the
931
+ browser condition, and a package `browser` field mapping a file or a module to `false` gives an empty module;
932
+ neither reaches a Node built-in stub that throws at startup.
933
+ - Every module factory is parsed as a classic script before it is written. A package file that cannot be one (a
934
+ top-level await, or anything else a script cannot hold) is left out and named in the dev log, and requiring it
935
+ throws why, where it used to fail the whole registry or drop every module of its vendor file in the browser; app
936
+ code that cannot be one still fails the build. A registry built before this rebuilds once.
937
+
938
+ - 2d8de1c: fix(start): a route whose client code fails to bundle says why
939
+
940
+ `akan start` bundles a route's client components the first time the route is requested. When the bundler refused
941
+ them — an import that does not resolve, a syntax error — the builder reported only the bundler's own message,
942
+ "Bundle failed", in the request's error, the build status and the dev log alike. It now lists the bundler's reasons
943
+ under it, the way `akan build` and the rest of the dev build already did:
944
+
945
+ ```
946
+ Bundle failed
947
+ Could not resolve: "./not-there" (apps/demo/ui/Broken.tsx:2:22)
948
+ ```
949
+
950
+ - 69f7178: fix(dev): `akan start` is the dev server even when the environment says `NODE_ENV=production`
951
+
952
+ An ambient `NODE_ENV=production` turned every route of the dev server into a 500:
953
+
954
+ ```
955
+ [SSR] render failed scope=/en/: [SSR] route /:lang missing from production artifact — rebuild with `akan build` to include it
956
+ ```
957
+
958
+ `WebRouter` picked its mode from `NODE_ENV` alone, so it installed the production route cache — the one whose
959
+ `buildRoute` throws because `akan build` was supposed to have written a routes manifest already. A dev artifact
960
+ never has one, so nothing could render, and the same ambient value also sent the gateway's runtime directory to
961
+ `<workspace>/runtime` and baked production React into the dev bundles.
962
+
963
+ The value does not have to be exported to arrive: Bun auto-loads the workspace `.env`, so a `NODE_ENV=production`
964
+ line in a downloaded env file reaches the CLI while `env | grep NODE_ENV` in the shell stays empty. A CI image
965
+ default or a container base image does the same.
966
+
967
+ **The command now outranks the environment.** `akan start` pins `NODE_ENV=development` for the processes it
968
+ spawns and for the in-process builder that bakes the value into dev bundles; `AkanApp` resolves a child's
969
+ `NODE_ENV` from `AKAN_COMMAND_TYPE` before falling back to the inherited value; and `WebRouter` refuses the
970
+ production branch outright while dev-hosted, warning once that it did rather than failing every request. Running
971
+ a built artifact (`bun main.js`, Docker) is untouched — it carries no command type and keeps honouring
972
+ `NODE_ENV`.
973
+
974
+ - bf27773: fix(cli): stop `akan start` from leaving the terminal in raw mode, which killed Ctrl+C
975
+
976
+ A dev server that had been running for a few minutes stopped responding to Ctrl+C. The cause was not
977
+ signal handling — the terminal had been left in **raw mode**, so with `isig` off the tty never turned
978
+ `^C` into SIGINT at all. `kill -INT <pid>` still ended the process instantly, which is the tell.
979
+
980
+ A Bun child snapshots the controlling terminal's termios when it is spawned and writes that snapshot
981
+ back when it exits. ora's `discardStdin` holds the terminal raw for as long as a spinner runs, and the
982
+ dev host spawns the builder and the backend under the "Preparing backend..." spinner — so those
983
+ children snapshot a raw terminal. The spinner then restores it and Ctrl+C works, which is why a fresh
984
+ `akan start` behaved. The first dev-host recycle (an `akan.config.ts` change, or regenerated runtime
985
+ metadata such as `dict.ts` / `sig.ts`) SIGTERMs the builder, and its exit writes the stale raw termios
986
+ back over a terminal nobody restores again. `Spinner` no longer takes stdin.
987
+
988
+ Two nearby hazards with the same symptom are fixed as well.
989
+
990
+ `Spinner` refuses to animate against a tty that reports **0 columns** — an unsized pty, which CI
991
+ runners, `expect`/`script` harnesses and some detached panes produce. ora sizes its clear loop as
992
+ `ceil(lineWidth / stream.columns)`, so zero made it `Infinity`: measured 750MB of cursor moves and
993
+ 8.7GB RSS inside a minute, in a loop past the point where SIGINT or SIGTERM could be handled, leaving
994
+ SIGKILL as the only way out. Such a terminal now gets plain lines.
995
+
996
+ `akan start`'s local-database teardown no longer hangs the exit. Registering a SIGINT listener
997
+ replaces the kernel's "terminate now", so that handler became the only thing that could end the
998
+ process — and it exited only if `docker compose down` succeeded. The teardown is now bounded
999
+ (`ApplicationScript.dbShutdownTimeoutMs`, 20s), the exit runs even when it fails or times out, and a
1000
+ second Ctrl+C abandons it instead of queueing behind the first.
1001
+
1002
+ - ccf2ae2: perf(dev): a save starts building 50ms sooner
1003
+
1004
+ The dev file watcher collected changes for 80ms before handing a batch over, the largest fixed wait in a save once the
1005
+ build is fast. It now waits 30ms. Measured on minimal, a CSR component save went from 340ms to 278ms, and a second write
1006
+ 30ms after the first (a format-on-save) still produced one update, because the build reads the file's latest content.
1007
+ `AKAN_DEV_WATCH_DEBOUNCE_MS=80` restores the old window.
1008
+
1009
+ - 2d8de1c: fix(devkit): executor, doctor, barrel and code-agent fixes from the devkit/cli bug sweep
1010
+
1011
+ - **Workspace apps, libs and packages are listed in name order**, not in the order the file system answered. It
1012
+ shows in pickers, `akan context` / `akan doctor` listings, the `## Workspace` lines `akan agent install` writes,
1013
+ and the order a lib's `docker.preRuns` / `postRuns` land in a generated Dockerfile, which is now the same on every
1014
+ build.
1015
+ - **Barrel import rewriting no longer drops re-exports when two barrels are analyzed at once.** Concurrent analyses
1016
+ shared one regex position, so a build could leave an import on the whole barrel, differently from build to build.
1017
+ - **`akan doctor`, `akan workflow validate` and MCP `doctor_workspace` ignore a malformed workflow artifact** instead
1018
+ of throwing a `TypeError`.
1019
+ - **`SysExecutor`'s module listings filter on each module's own file** — they returned every folder — and find a
1020
+ service module at `lib/_<name>/<name>.service.ts`. `getScalarDictionaryFiles` reads `lib/__scalar/`, and
1021
+ `getTsConfig({ refresh: true })` re-reads the config it extends.
1022
+ - `akan code`: a prompt typed instead of answering drops the questions and approvals it replaced, a session past
1023
+ 512 KiB still shows in `/sessions`, and an MCP token refreshed without `expires_in` is kept until the server
1024
+ refuses it instead of being refreshed on every connect.
1025
+
1026
+ - 2d8de1c: fix(devkit): stop re-exporting `lowerlize` from `@akanjs/devkit/workflow`
1027
+
1028
+ The re-export was CLI plumbing that leaked through the facet's `export *`: it existed so one CLI script could
1029
+ import it, and that script has imported it from `akanjs/common` for a while. Nothing else used it. The function
1030
+ itself is unchanged — import `lowerlize` from `akanjs/common`, which is where it always lived.
1031
+
1032
+ - ccf2ae2: fix(dev): a page that quotes `export default function` in a code snippet builds for the CSR dev page
1033
+
1034
+ The Fast Refresh rewrite of `export default function Name(` also rewrote the same text inside a template literal or a
1035
+ block comment, and appended a second `export default` for it. The CSR bundle refused the result with `Multiple
1036
+ exports with the same name "default"`, so `?csr=true` failed on any page quoting such a component — eight docs pages
1037
+ in the akan app — while SSR rendered them. Only a declaration the parser reports as an export is rewritten now.
1038
+
1039
+ - 69f7178: fix: the first field run's findings — request context, module identity, and an honest agent surface
1040
+
1041
+ Six fixes from running the in-page agent on a real product app, plus the one its own docs app hit.
1042
+
1043
+ **Server components see the request again.** The RSC worker wrapped a render in `requestStorage.run(...)` only,
1044
+ and the flight stream pumps components after that scope has exited — every `getSelf()` read no cookies and
1045
+ bounced authed pages to `/signin`. The worker now keeps a request fallback pushed until the render settles, the
1046
+ same discipline `ssrFromRscRenderer` already used. The stack is global and last-push-wins across concurrent
1047
+ renders; pumping the flight render inside the ALS scope itself is the follow-up that removes the caveat.
1048
+
1049
+ **`akanjs/fetch` is a vendor shared module.** It was bundled separately into the app and the vendored
1050
+ `akanjs/store` chunk, so `FetchClient`'s static serialized-signal registry split in two and the agent catalogue
1051
+ silently read the empty copy — every endpoint-named action refused as undescribed. The registry also moved to a
1052
+ `globalThis` symbol (the anchoring the shared client proxy already had), so a future duplicated module instance
1053
+ degrades to nothing instead of an empty surface.
1054
+
1055
+ **The relay's return scalar registers on the client graph.** `agentTurn` lived in `akanjs/signal`, which no
1056
+ client bundle loads, so the chat's first probe of `fetch.runAgentTurn` threw `No scalar constant model for
1057
+ agentTurn`. The scalar now lives in `akanjs/fetch` and `FetchClient` itself force-registers it — Bun links a
1058
+ barrel's `export *` modules lazily per used binding and its transpiler drops unused imports, so only a real call
1059
+ survives to do that.
1060
+
1061
+ **`labelOf` reads the `text: "title"` role as the `Set` it is.** It asserted `readonly string[]` and called
1062
+ `.find`, so every screen-context label on a search-indexed model threw.
1063
+
1064
+ **`akan build` relaunches itself once with the app env in place.** Bun macros snapshot `process.env` at process
1065
+ start, so the env the build resolves and publishes later is invisible to them — and any hand-written
1066
+ `@libs/<lib>/server` import (the sanctioned cross-lib entrypoint) puts that lib's env files, which read
1067
+ `getEnv()` at module scope, into the `getSerializedSignal` macro graph. `AKAN_PUBLIC_APP_NAME` is the one
1068
+ required key a workspace root `.env` cannot carry, so the CLI build now spawns itself again with it set and the
1069
+ child does the real build — the `AKAN_PUBLIC_APP_NAME=<app> akan build <app>` workaround is built in. The
1070
+ relaunch decision compares the env the process was **born** with, never the live `process.env`: the command
1071
+ dispatcher writes the resolved app name into `process.env` before any command method runs, which satisfies
1072
+ runtime readers but not macros — a guard reading the live value never fires (that inert shape is what alpha.19
1073
+ shipped). A `Relaunching with AKAN_PUBLIC_APP_NAME=…` line in the build output is the observable proof. The
1074
+ generated `sig`/`dict`/`srv`/`db` barrels also stopped walking parent _server_ barrels (they import the lib's
1075
+ own `@libs/<lib>/lib/<facet>` modules after `akan sync`), and `FetchClient.from` resolves its origin tolerantly,
1076
+ so a lib whose graph stays clean serializes with no env at all.
1077
+
1078
+ **The agent surface stops publishing two lies.** Form setters for the base document fields (`setIdOnX`,
1079
+ `setCreatedAtOnX`, `setUpdatedAtOnX`, `setRemovedAtOnX`) are no longer tools — the server stamps those. And an
1080
+ action named after an endpoint is published with the endpoint's schema only when it declares at least as many
1081
+ parameters: one declaring fewer would silently drop the borrowed tail and read stale form state, which shipped a
1082
+ resident SMS with the wrong unit number in the field run. Such an action is refused by name and warned once in
1083
+ the console; trailing extras beyond the endpoint's arguments stay legal, which is the generated
1084
+ `create<Model>(data, options?)` shape.
1085
+
1086
+ - 5f53462: The guideline set drops what only the removed code generator read
1087
+
1088
+ Six guidelines existed for `AiSession`'s codegen contract and nothing else, and are gone:
1089
+ `docPageRule`, `docSyncRule`, `moduleCodegen`, `enumConstant`, `sharedUiUsage` and `utilUiUsage`. Nothing in the
1090
+ workspace or in any skill named them, and their bodies asked the model for a `// File: <path>` block contract that
1091
+ an agent with edit tools cannot use. `akan-module` no longer offers `moduleCodegen` as a deeper read.
1092
+
1093
+ Every `<name>.generate.json` is deleted, along with `Prompter.getGuideJson` and the `GuideGenerateJson` /
1094
+ `GuideScan` / `GuideUpdate` types `@akanjs/devkit` re-exported. The `codegenPriority`, `scans` and `update.rules`
1095
+ fields they carried were read by nothing after the generator left; the only consumer was `akan guideline show
1096
+ --format json` echoing them back.
1097
+
1098
+ `akan guideline` therefore loses its `--format` option — `list` prints names and `show` prints one instruction,
1099
+ which is what both did in their default mode anyway. Guideline names drop from 35 to 29. What remains is the
1100
+ deep-dive set the `AGENTS.md` table points at, plus the three documents `akan agent install` composes the
1101
+ workspace guide from (`conventions`, `workspaceOnboarding`, `framework`). MCP `get_guideline`, the
1102
+ `akan://guidelines/<name>` resources and the `akan guideline list` / `show` commands are unchanged otherwise.
1103
+
1104
+ - 69f7178: feat(config): a lib's `externalLibs` reaches every app in the workspace
1105
+
1106
+ `libs/<lib>/akan.config.ts` has always accepted `externalLibs` — it typechecks, `AkanLibConfig` resolves it, and the
1107
+ lib scan result carries it — but nothing ever read the value back. All three consumers (the backend and rsc-worker
1108
+ bundle externals, the SSR pages bundle, and the production `package.json` dependencies) read the _app_ config, so a
1109
+ lib wrapping a runtime-sensitive dependency had to ask every app to repeat the declaration. Forgetting one surfaced
1110
+ as a native package bundled into the app rather than as an error.
1111
+
1112
+ An app's `externalLibs` is now its own declaration plus every lib's, deduped with the app's entries first. A
1113
+ workspace where no lib declares one resolves exactly as before.
1114
+
1115
+ The libs are read workspace-wide rather than narrowed to the app's own dependencies. That set comes from the
1116
+ dependency scan, and the incremental page rebundle re-reads this config on every file change from a child process
1117
+ that never scans — so narrowing it would put a full dependency scan in the hot path. The trade is that a lib's
1118
+ declaration also lands in the production dependencies of an app that does not use that lib.
1119
+
1120
+ `LibExecutor.getConfig({ refresh: true })` now busts the module import cache the way the app config already did, so
1121
+ a `libs/<lib>/akan.config.ts` edited while the dev server runs is re-read instead of returning the first-loaded
1122
+ module.
1123
+
1124
+ - 69f7178: fix(sync): hold libraries to the root layout allowlist, and check a library's own `page/` tree
1125
+
1126
+ `apps/<app>/base` was refused by `akan sync`, `akan doctor` and `akan quality scan`; `libs/<lib>/base` passed all
1127
+ three. The rule lives in one file — `workspaceLayout.ts` exists precisely so the three tiers cannot disagree — but
1128
+ the call sites read it behind `exec.type === "app"` / `for (const app of context.apps)` / `segments[0] === "apps"`,
1129
+ so a library root was never compared to anything at all. A lib could carry a `base/`, a `helper.ts`, a stray
1130
+ `script/`, and every command stayed green.
1131
+
1132
+ Libraries now answer to their own allowlist, and it is the app's minus what only an app can use: no `main.ts`, no
1133
+ `capacitor.config.ts`, no `.akan` / `android` / `ios` / `mobile` / `script` / `secrets`, because a library is never
1134
+ booted or packaged. `index.ts`, `README.md` and `tsconfig.spec.json` are added, since a library ships as a package.
1135
+ `public/` and `private/` stay, because `syncAssets` symlinks them into each consuming app.
1136
+
1137
+ Three more places the same gate was one-sided:
1138
+
1139
+ - `akan doctor` walked `context.apps` only. It now walks libraries too and reports `lib-root-unknown-entry` beside
1140
+ the existing `app-root-unknown-entry`.
1141
+ - `akan quality scan` warned on an unexpected app root _file_ and nothing else — not a library root file, and not a
1142
+ root _folder_ on either side, which is the shape `base/` actually takes. It now reports all four
1143
+ (`akan.layout.{app,lib}-root-{file,folder}`). Its `lib/` facet rule kept its own copy of the allowlist and flagged
1144
+ a `<model>.signal.test.ts` that `akan sync` explicitly permits; both now read the shared helper, and the rule is
1145
+ renamed `akan.layout.lib-facet-file` so its name no longer collides with the library root it was never about.
1146
+ - A library's `page/` folder was validated only once an app opted into it with `syncPageLibs`. An app checks route
1147
+ filenames in `getPageKeys`, which a library has no equivalent of, so `libs/<lib>/page/Component.tsx` synced clean
1148
+ in the library it belongs to and failed in somebody else's app. `akan sync <lib>` now reports it as a scan
1149
+ violation, from the same rule `validatePageSourceFile` throws — split out as `getPageSourceFileViolation` so the
1150
+ reason is available where throwing is the wrong answer.
1151
+
1152
+ - 69f7178: fix(lint): scope the module plugins to apps/libs, downgrade `noUnnecessaryConditions`, and stop truncating at 20
1153
+
1154
+ Four reports from one workspace migration (5 apps, 11 libs, ~2,950 linted files), all in what the shared config
1155
+ and `akan lint` do rather than in what a rule finds.
1156
+
1157
+ **`noUnnecessaryConditions` ships as `warn`.** Biome 2.5.8's inference, with `domains.types: "all"` on, is wrong
1158
+ on three shapes this framework uses everywhere — reproduced here, not taken on report: `useRef(false).current`
1159
+ narrowed to its initial literal, `useRef<HTMLTextAreaElement>(null).current` treated as non-null, a `#private`
1160
+ boolean flag narrowed the same way, and `RegExp.prototype.exec()` treated as non-nullable. Of 86 findings in the
1161
+ reporting workspace 65 were of those shapes, where deleting the guard is a null dereference. The 21 real findings
1162
+ are worth keeping, so the rule stays on as a warning rather than being dropped.
1163
+
1164
+ **The akan-module plugins apply to `apps/**`and`libs/**` only.** `no-import-external-library` was scoped to a
1165
+ bare `**/index.ts`, so it fired on any package entry in the workspace — a Hardhat package whose public surface
1166
+ genuinely _is_ third-party types had no way out but a nested `biome.json` for the whole subtree, which replaces
1167
+ the parent's config rather than merging. Same scoping for `no-deep-internal-import`,
1168
+ `no-redeclare-predefined-endpoint`, `no-return-in-store-action`, `no-js-private-class-method`, and the
1169
+ server-component plugins. `no-bang-comment-in-client` stays broad on purpose: it is about browser-reachable code,
1170
+ not about akan module conventions, and a `//!` shipping to every visitor is no better in a plain package.
1171
+
1172
+ **`**/lib/\_\_lib/**` joins the generated-file override.** It is scaffolded, gitignored, regenerated by `akan sync`,
1173
+ and unused-import-clean by construction — so `biome check --write` stripped the imports and the next sync put
1174
+ them back, 116 diagnostics per lap.
1175
+
1176
+ **`akan lint` prints up to 200 diagnostics** and takes `--max-diagnostics <n>` (`0` for no limit). Biome's default
1177
+ is 20; a run that shows 20 of 44 reads as "20 problems left", so a change in the _mix_ of findings looks like
1178
+ progress.
1179
+
1180
+ **`akan lint` pins the config path.** `biome.json` is strict JSON, and a JSONC comment in it makes Biome 2.5.8
1181
+ fall back to config discovery instead of reporting the parse error — it then aborts on whatever nested root
1182
+ config the walk finds, typically inside a directory `files.includes` excludes, naming a file the author never
1183
+ touched. With `--config-path` the parse error lands on the offending line. `Linter` and the workspace runner also
1184
+ accept `biome.jsonc`, which is where a comment is legal, and the workspace template now excludes `.claude` so an
1185
+ agent worktree's stale config is not in the walk to begin with.
1186
+
1187
+ Grit plugin diagnostics _are_ suppressible, contrary to the report: the category is `lint/plugin`, so
1188
+ `// biome-ignore lint/plugin: <reason>` and `// biome-ignore-all lint/plugin: <reason>` both work. The bare
1189
+ `// biome-ignore plugin:` form Biome's own diagnostic label suggests does not. Written down in the agent guide.
1190
+
1191
+ - 5f53462: A server-side loopback fetch follows the port the process was actually run with, so a container started with
1192
+ `PORT=80` renders instead of failing every page with `base.error.serverUnreachable`.
1193
+
1194
+ - `getEnv().serverPort` fell back to a literal `8282` on the server side, and only `akan start` ever set
1195
+ `AKAN_PUBLIC_SERVER_PORT`. A production image overriding the Dockerfile's `ENV PORT=8282` therefore bound one
1196
+ port and pointed the RSC worker's `fetch.*` at another; the SSR render failed on every route, with no other
1197
+ symptom than the transport error. When the origin resolves to `localhost` — the self-call — the port now comes
1198
+ from `PORT`. A `SERVER_HOST` naming another host is not a self-call and keeps the explicit port, and
1199
+ `AKAN_PUBLIC_SERVER_PORT` still outranks both.
1200
+ - `AkanApp` publishes the port it resolved as `PORT` to the replica it runs in-process and to every child it
1201
+ spawns, so `new AkanApp({ port })` reaches the tree the same way the env var does. In solo mode that is also
1202
+ what makes the option bind: `AkanServer` reads `PORT`, so an explicitly configured port was previously ignored.
1203
+ - `akan build` no longer publishes `AKAN_PUBLIC_CLIENT_PORT` / `AKAN_PUBLIC_SERVER_PORT` into the build process's
1204
+ own env. Bundling `define`s every `AKAN_PUBLIC_*` into a literal, so with `PORT_OFFSET` set the builder's dev
1205
+ port was baked into the artifact, where no deployment env could override it.
1206
+
1207
+ - 2d8de1c: fix(code): an MCP server that refuses a stored token gets one refresh before `akan code` asks for a sign-in
1208
+
1209
+ `akan code` refreshed an MCP server's token only when the stored expiry said it had run out. A server that refused
1210
+ the token earlier — it was revoked, or it was a refreshed token the server issued with no lifetime, which is kept
1211
+ until refused — sent the session straight to `sign-in needed · /mcp login <name>`, even though the stored refresh
1212
+ token would still have worked.
1213
+
1214
+ On a 401 at connect, a server holding a refresh token now gets exactly one refresh (or the token another session
1215
+ already stored since), and the connect is retried with it. Only when that refresh is refused, or the new token is
1216
+ refused too, is the server listed as needing a sign-in. A 401 is never retried a second time, so a server that
1217
+ refuses every token costs one refresh, not a loop.
1218
+
1219
+ - 2d8de1c: fix(code): an MCP tool call refused mid-session refreshes the token once and retries
1220
+
1221
+ `akan code` refreshed an MCP server's token only while connecting. A token that expired or was revoked during a
1222
+ long session made every call to that server fail with `MCP server "<name>" requires authentication`, which named no
1223
+ way out, until the session was reloaded.
1224
+
1225
+ A call answered 401 now gets one refresh (or the token another session stored since), reopens the server with it —
1226
+ on a new streamable HTTP session, since a server may bind its session to the token that opened it — and is retried
1227
+ once. If the refresh is refused, or the new token is refused too, the call fails with `MCP server "<name>" needs
1228
+ signing in — /mcp login <name>`, and later calls to that server answer the same without refreshing again.
1229
+
1230
+ Calls refused together share one refresh, and so does every connection in the process that would present the same
1231
+ refresh token — a sub-agent's, or one opening with an expired token — because a server that rotates refresh tokens
1232
+ may revoke the whole grant when one is presented twice. The token file's format and location are unchanged.
1233
+
1234
+ - 2d8de1c: fix(code): two `akan code` sessions refreshing the same MCP token make one refresh between them
1235
+
1236
+ Every `akan code` process refreshes an MCP server's token itself, from the one token file they all share. Two
1237
+ sessions whose calls were refused at the same moment each presented the same refresh token, and a server that
1238
+ rotates refresh tokens refuses the second use — signing that session out, or revoking the whole grant.
1239
+
1240
+ A refresh now runs under a lock file beside the token file (`mcpAuth.json.lock`, the same kind of lock `akan login`
1241
+ takes on `~/.akan/config.json`). The token is read again once the lock is held: if another session already replaced
1242
+ it, that token is used and no refresh is made. A lock left by a process that died is taken over; one a live process
1243
+ holds for more than 30 seconds reads as a failed refresh. The token file's format and location are unchanged.
1244
+
1245
+ - 2d8de1c: fix(code): `/mcp remove` says when the home file still declares the server, and honours `--local`
1246
+
1247
+ `akan code` reads MCP servers from two files — `~/.akan/code/mcp.json`, which every repo reads, and the repo's
1248
+ `.akan/code/mcp.json`, which wins a name both declare. `/mcp remove <name>` takes the repo's entry first, so on a
1249
+ name both files declare the home file's declaration took over after the reload, possibly with a different command
1250
+ or url, while the notice read only "removed X from the workspace file". The notice now adds that the global file
1251
+ still declares it, so it still applies, and that running `/mcp remove X` again removes that one too.
1252
+
1253
+ `--local` was accepted by `/mcp remove` and ignored. It now means what it means for `/mcp add`: this repo's file
1254
+ only. A name only the home file declares is left alone, and the reply says where it is declared.
1255
+
1256
+ `McpServerConfig.remove(workspaceRoot, name, only?)` takes the scope as an optional third argument; without it the
1257
+ lookup order is unchanged. The `akan-code` skill's manual now names both files and both flags.
1258
+
1259
+ - 2d8de1c: fix(code): `/mcp` says `sign-in needed` for a server whose token stopped working mid-session
1260
+
1261
+ An MCP server whose token could not be renewed during a session answered every tool call with `needs signing in —
1262
+ /mcp login <name>`, but `/mcp` kept listing it as `signed in` — the state from when the session connected. Its row
1263
+ now turns to the same `sign-in needed · /mcp login <name>` a failed connect shows, and `/mcp <name>` says it needs
1264
+ signing in instead of listing tools that cannot answer; a server that failed to connect for want of a sign-in no
1265
+ longer claims it "connected and published no tools" there either. `/mcp login <name>` (or `/mcp reload` once a
1266
+ usable token is stored) reopens the session, and the server reads as signed in again.
1267
+
1268
+ - ccf2ae2: fix: a native target's dev build has its own output folder
1269
+
1270
+ `akan start-ios`, `start-android` and `start-desktop` without `--release` built into the same
1271
+ `.akan/native/<target>/build/<platform>` as `build-ios`, `build-android`, `build-desktop` and `--release`, so a dev
1272
+ session deleted the release build there, a desktop installer included, and put a dev build under its name. Dev builds
1273
+ now go to `.akan/native/<target>/dev/<platform>`; the first dev session after the update builds from scratch once.
1274
+
1275
+ - ccf2ae2: `akan start-desktop`, `start-ios` and `start-android` print a page's log once, at the level it was logged, and say a
1276
+ boot came up in one line
1277
+
1278
+ - A boot that comes up prints `cmdc ios ready · iPhone 16 · http://localhost:8283 · 12.4s` and nothing else; its steps
1279
+ (prepare, build, install, launch) are debug. A boot that fails prints every step it took ahead of the error. A native
1280
+ config warning still prints as it happens.
1281
+ - A page line reads `WARN [page:WsClient] WebSocket message process failed …`: stamped once by the CLI's logger, at
1282
+ the page's own level, with no `[default]` label, no empty line after it, and a multi-line message kept together. A
1283
+ page logger writes `[Name] message` at the console method of its level inside a native shell, and the desktop host
1284
+ and iOS tag each line `[page<+><#window> <level>]` for the CLI to read back.
1285
+ - Android forwards the page console through the bridge in a dev build too, so logcat carries each line at its level
1286
+ and a `console.error("%s", …)` substitution the way the devtools show it; iOS writes os_log at the page's level. An
1287
+ uncaught error forwards its message along with WebKit's frames, which carry none.
1288
+ - Frame tracing (`[akan:frame:…]`) is opt-in (`?akanFrameDebug=1` or `localStorage["akan:debug:frame"] = "1"`) and
1289
+ logs at debug, once per event; a mobile or desktop target no longer turns it on for every visibility change.
1290
+ - Lines no page or host wrote (the simulator, WebKit) and where the host's pages come from are debug; `--verbose` shows
1291
+ the former. `start-desktop` and friends ask a dev server whose builder idled out for its health, not its home page,
1292
+ so they no longer report it missing while it wakes.
1293
+
1294
+ - c45ac6c: fix(lint): stop `//!` markers from shipping to the browser
1295
+
1296
+ Bun's bundler classifies `//!` and `/*!` as legal comments — the `@license` / `@preserve` class it must preserve
1297
+ for licence compliance — and keeps them through `minify: true`. Everything else goes: `//`, `/* */`, `//*`, `//?`,
1298
+ and `// TODO:` are all stripped. But the workspace `AGENTS.md` prescribes `//!` as in-code marker 4, "disabled or
1299
+ must-fix code", so the convention itself guaranteed that internal notes about broken and unfinished code shipped
1300
+ verbatim to every visitor. A deployed app served eight of them in one chunk, including `//!env로 옮겨야함`,
1301
+ `//!need to change`, and `//! Need to fix after bunjs migration`.
1302
+
1303
+ There is no bundler-side fix: `legalComments` is not a `Bun.build` option, and `Bun.Transpiler.transformSync`
1304
+ preserves `//!` too. The source is the only place to stop it.
1305
+
1306
+ `no-bang-comment-in-client.grit` now bans the marker in browser-reachable code — `ui/`, `webkit/`, `common/`,
1307
+ `page/**/*.tsx`, `*.constant.ts`, `*.store.ts`, and the five module component suffixes — while leaving it legal in
1308
+ server, `srvkit/`, and CLI files, which never reach a bundle. The five existing occurrences in `akanjs` and the
1309
+ libs became `// FIXME:`.
1310
+
1311
+ The rule anchors the marker to line start or to whitespace after code, so a literal like `"https://host//!path"`
1312
+ does not trip it. Comments are trivia rather than nodes, so the diagnostic spans the module and a marker on a
1313
+ file's very first line is invisible to the pattern — the one case the rule cannot see.
1314
+
1315
+ Rebuilding `apps/akan` takes the bundle from two `//!` comments to zero, with the third-party `/*!` licence
1316
+ blocks left intact.
1317
+
1318
+ - f882bbc: feat(lint): `no-init-fetch-in-client` — keep slice hydration on the server
1319
+
1320
+ `fetch.init<Model><Suffix>` is not a request, it is a hydration snapshot: `#registerSlice` composes it out of the
1321
+ slice's list and insight queries and returns a `ServerInit` whose only consumer is the `init` prop of
1322
+ `Load.Units` / `Load.View`, which seeds the store from it before React renders. `fetch.get<Model>Init<Suffix>` is
1323
+ the same call returning the payload alone. From a route both resolve before the first byte and the markup ships
1324
+ populated; from a client component they are two extra round-trips for a shell the browser already painted empty,
1325
+ landing in a local variable nothing reads — `Load.*` seeds state, so a value held in a client closure reaches
1326
+ nowhere. Nothing in the framework flagged this, and the mistake reads as an ordinary load.
1327
+
1328
+ The fix the message points at is the route: `await fetch.initXInY(...)` in `page/**` and pass the result down as
1329
+ an `init` prop, or hand the unawaited promise to a `Zone`. The client is not missing the load either — every
1330
+ slice also generates `st.do.init<Model><Suffix>()`, which runs the same two queries and commits them to state.
1331
+
1332
+ The gate is the file, not the call site. `"use client"` is anchored to `JsDirective`, so the same text as an
1333
+ ordinary string literal or inside a template-literal code sample — both common in docs pages — is not mistaken
1334
+ for the directive. `*.store.ts` is added by name, being client-only by role while carrying no directive.
1335
+
1336
+ The name is matched by shape, a lint rule having no way to know which slices exist. The generated one is `init`
1337
+
1338
+ - `Capitalize<refName>` + `Capitalize<suffix>`, so it always carries two capital-led segments; requiring the
1339
+ second is what keeps a hand-written `initPayment` / `initSession` endpoint out, and `initializeSomething` was
1340
+ never at risk since `init` is followed by a lowercase letter there. A custom endpoint spelling the generated
1341
+ shape exactly — `initPaymentSession`, `get<X>Init<Y>` — is the residue, suppressible with
1342
+ `// biome-ignore lint/plugin: <reason>`.
1343
+
1344
+ `view` / `edit` hydrate the same way but are deliberately unmatched: `edit<X>` is a plausible custom endpoint
1345
+ name.
1346
+
1347
+ A runtime guard was considered instead and rejected: CSR and Capacitor builds render route modules in the
1348
+ browser (`RootRenderLayer` in `pkgs/akanjs/webkit/bootCsr.tsx` awaits the page's own async render), so
1349
+ `typeof window !== "undefined"` holds for a perfectly correct page there and nothing at runtime can tell that
1350
+ apart from a client component calling the same thing after hydration.
1351
+
1352
+ - 69f7178: feat(lint): ban `cnst` model types in `*.Util.tsx` / `*.Zone.tsx` props
1353
+
1354
+ `Util` and `Zone` are always client components, so a `cnst.Banner` / `cnst.LightBanner` prop is a hydrated class
1355
+ instance the server has to hand across the boundary — the functions are stripped on the way and what arrives is a
1356
+ plain object wearing the model's type. `no-model-type-in-util-zone.grit` reports it and points at the two shapes
1357
+ that work: take `bannerId: string` and read the model from the store, or take the payload the framework already
1358
+ serializes.
1359
+
1360
+ Three exemptions, because none of them is an instance. `cnst.<Enum>["value"]` is an indexed access that resolves to
1361
+ a string union, which is how every enum prop in the codebase is already written (`roles: cnst.AdminRole["value"][]`).
1362
+ A `ClientInit` / `ClientView` / `ClientEdit` type argument is mapped through `GetStateObject<…>` before it reaches
1363
+ a prop, which is the sanctioned server-to-client handoff. A `ModelsProps<cnst.Setting>` type argument spends the
1364
+ model on `onClickItem?: (model: M) => unknown` and nowhere else, so it is the function-typed-prop exemption reached
1365
+ through a generic — whoever passes the callback is a client component already holding the value.
1366
+ `ModelProps<"setting", cnst.LightSetting>` stays reported: it spreads the model onto the props themselves, and the
1367
+ `Unit` / `View` files that take it are server components outside this rule's scope. Any _other_ indexed access is
1368
+ still reported — `cnst.Banner["image"]` is a `File`.
1369
+
1370
+ The rule keys on the `cnst.` qualifier in a type position, so it never touches a value expression, and it is scoped
1371
+ to those two filename suffixes: `Unit` and `View` are server components and keep taking the model itself.
1372
+
1373
+ Only prop positions are read: a `*Props` interface or type alias, and the inline object type on the component's own
1374
+ parameter. A `cnst` type that never leaves the file is not a boundary crossing and is left alone — a local
1375
+ annotation, a callback parameter the framework itself types with the model (`renderItem`, `renderList`), a
1376
+ module-scope helper, a non-`Props` local shape, and the props of a component nested inside another one. A
1377
+ function-typed prop (`onPick?: (t: cnst.LightTicket) => void`) is exempt too: a closure cannot cross the RSC
1378
+ boundary at all, so whoever passes it is a client component that already holds the value.
1379
+
1380
+ - 5f53462: fix(lint): `non-scalar-props-restricted` covers `*.Unit.tsx` and `*.View.tsx`
1381
+
1382
+ The guide has always said a server component may not hand a function prop across the client boundary in a page, a
1383
+ Unit or a View, but the rule was scoped to `page/**` only. Units and Views are always server components, so a
1384
+ function prop there fails at the boundary at runtime; lint now says so first.
1385
+
1386
+ - ccf2ae2: `akan start` replaces its backend one restart at a time, so saves in quick succession can no longer leave a second
1387
+ gateway holding the port.
1388
+
1389
+ - A save whose restart came due while the previous one still waited for the old backend to exit used to signal the
1390
+ same process again and then spawn a second backend beside the first. The host kept the one that lost the port,
1391
+ restarted it on every save, and the untracked one went on answering. A restart that comes due now runs after the
1392
+ one in flight, and a config or metadata recycle and `akan start`'s shutdown wait for it too.
1393
+ - A gateway that cannot bind its port no longer reports itself ready while it waits for its replicas to exit.
1394
+
1395
+ - 2d8de1c: fix(release): the published packages no longer carry test-run files from `local/`
1396
+
1397
+ `akanjs` 3.0.0-beta.19 shipped 65 files (SQLite databases and logs, 820 KB) and `@akanjs/devkit` 2 from the
1398
+ gitignored `local/` directory the test suites write into, because both builds copied the package directory whole.
1399
+ `build-package` and `akanjs`'s own build now leave `local/` out, and `verify-akan-publish-packages` refuses a package
1400
+ whose pack list still contains it.
1401
+
1402
+ - c45ac6c: perf(build): precompress the CSS bundle, and serve brotli sidecars ahead of gzip
1403
+
1404
+ The compiled stylesheet went out uncompressed in production. `precompressArtifacts` only walked
1405
+ `.akan/artifact/client`, so every JS chunk got a `.gz` sidecar while the CSS — written to
1406
+ `.akan/artifact/styles` — got none, and `#fileResponse` fell through to streaming the raw file. Nothing was
1407
+ wrong on the serving side: `#isCompressible` already accepts `text/css` and the sidecar lookup already ran.
1408
+ The file simply was not there. On one deployed app that was 319KB on the wire per cold load, against 45KB
1409
+ gzipped. The CSS is and always was minified — Lightning CSS runs in `prepareCssAsset` — so this is purely the
1410
+ transfer encoding.
1411
+
1412
+ Sidecars are now written for `styles/` too, and a `.br` sidecar is written alongside every `.gz`.
1413
+
1414
+ Brotli is tried first and gzip remains the fallback, because browsers only advertise `br` on secure origins.
1415
+ Quality is 11 for CSS and 9 for everything else: there is one CSS asset per basePath and it is the largest
1416
+ single file an app ships, worth ~20% over gzip for ~0.2s, while spending 11 on several hundred JS chunks costs
1417
+ ~12s of build time for a few hundred KB. Measured on `apps/akan`: 371 files, 14.5MB raw, 4.2MB gzip, 3.9MB
1418
+ brotli, and the CSS bundle 103KB raw → 16.2KB gzip → 13.0KB brotli. The compress phase went from ~0.3s to
1419
+ ~0.64s. Gzip sidecars moved to level 9, which is free at build time and was previously left at the default.
1420
+
1421
+ Encoding negotiation moved out of `WebRouter` and `AkanApp` into `resolveEncodedSidecar`. Both classes carried
1422
+ their own copy of the gzip lookup plus `#acceptsGzip` / `#isCompressible`, and a gateway that disagrees with
1423
+ its child about which encoding a request accepts serves a body the caller cannot decode. The shared version
1424
+ also honours `q=0` as the refusal it is, and no longer treats a token merely starting with `br` as brotli.
1425
+
1426
+ **Anything caching in front of the app must pass `Vary: Accept-Encoding` through.** The header was already
1427
+ set, but it only mattered when one encoding was on offer; with two, a proxy that drops or ignores it will
1428
+ hand a brotli body to a client that asked for gzip. Artifacts now carry both sidecars, so a build's static
1429
+ output grows by roughly the brotli total (3.9MB for `apps/akan`).
1430
+
1431
+ - 5f53462: feat(route): a route's head is JSX only, and analytics is the app's own component
1432
+
1433
+ **`.metadata()` is gone, along with the `metadata` / `generateMetadata` exports and the `AkanMetadata` type.** A route
1434
+ had two ways to say the same thing — an object of `title` / `description` / `openGraph` / `twitter` / `alternates`
1435
+ that the server translated into tags, and `.head()` taking the tags themselves — and could use only one of them per
1436
+ module. Every app in the workspace already wrote `.head(<>…</>)`. Rewrite an object as the tags it produced:
1437
+
1438
+ ```tsx
1439
+ .head(({ projectId }) => (
1440
+ <>
1441
+ <title>{`Project ${projectId}`}</title>
1442
+ <meta name="description" content="Project workspace" />
1443
+ <meta property="og:image" content="/og/project.png" />
1444
+ <link rel="canonical" href="https://example.com/project" />
1445
+ </>
1446
+ ))
1447
+ ```
1448
+
1449
+ A legacy route that still exports `metadata` or `generateMetadata` now fails the load as an unsupported export,
1450
+ the same way any other stray export does. The hreflang alternates Akan adds per locale are unchanged; the only thing
1451
+ that could suppress them was an `alternates.languages` object, so leave them out of the JSX.
1452
+
1453
+ **`.gaTrackingId()` and the `gaTrackingId` export / `System.Provider` prop are gone.** Which tags load, in which
1454
+ environment and behind which consent banner are the app's decisions, and the framework's built-in `Gtag` made none
1455
+ of them. Render a `"use client"` component that loads gtag.js from the root layout's `.render()` instead — the
1456
+ routing guide has one. GA4's Enhanced measurement already counts the router's `history.pushState` navigations, so
1457
+ the component only loads the tag.
1458
+
1459
+ The head-snapshot path used by the experimental partial commit (`AKAN_PUBLIC_RSC_PARTIAL_COMMIT=1` with
1460
+ `rscPatchHeadSafe`) was fed only by the metadata object, so it now always falls back to a full navigation.
1461
+
1462
+ - 5f53462: fix(build): the production server bundle keeps its identifiers, so a class still names its own log lines
1463
+
1464
+ `akan build` compiled `main.ts`, `server.ts`, the RSC worker and the console runtime with `minify: true`, which
1465
+ includes `identifiers`. Renaming every class in a server bundle is renaming the only identity several parts of
1466
+ the framework read back at runtime:
1467
+
1468
+ - `serve()` gives each service `new Logger(this.constructor.name)`, so a production log line read
1469
+ `[P6e] ... Updating 0 Floors' Awairs Latest Air Data` — the class the message came from is exactly what a log
1470
+ filter (`akan logs --child`, `--grep`) needs and the one thing the record no longer carried.
1471
+ - `Exception` and `Err` set `this.name = this.constructor.name`, so a 500's logged error name was mangled too.
1472
+ - A guard's `static name = "User";` is what `fetch` serializes, what the API explorer filters on, and what
1473
+ `mcpExposure` compares against `"Public"`. The convention held the framework's own guards together; a guard
1474
+ that omitted it silently published a two-letter name.
1475
+ - Every frame of every stack trace lost its function and class name, on a single-line bundle that has no
1476
+ source map to fall back on.
1477
+
1478
+ Server bytes are read from the image, never sent to a client, and the measurements say identifier mangling was
1479
+ not buying much: on `minimal`, `server.js` grows 5.5MB → 7.4MB, boot moves 202ms → 206ms (median of five), and
1480
+ peak RSS stays inside the noise band. `minify.whitespace` and `minify.syntax` are unchanged, and the browser
1481
+ bundles — CSR, page and client entries — still mangle identifiers, where the bytes actually travel.
1482
+
1483
+ `minify.keepNames` would have been the narrow fix; Bun 1.4.2 accepts the flag in `BuildConfig` and renames
1484
+ classes anyway, which `applicationBuildRunner.test.ts` now pins so the flag can be adopted when it lands.
1485
+
1486
+ - 69f7178: fix(build): target the server when bundling `client-ssr`, so SSR keeps happening
1487
+
1488
+ `ClientEntriesBundler` hardcoded `target: "browser"`, and both bundles go through it: the browser one under
1489
+ `client` and the **server-executed** one under `client-ssr`, which `SsrFromRscRenderer` loads with
1490
+ `await import(chunkId)`. The browser target resolves the `browser` export condition, so a dependency whose
1491
+ `browser` entry touches a DOM global at module scope threw the moment the SSR renderer imported the chunk.
1492
+
1493
+ That throw lands while React is rendering the shell, so the failure is not scoped to the offending subtree — the
1494
+ whole document degrades to client rendering, with one browser console line (`Switched to client rendering because
1495
+ the server rendering errored: document is not defined`) and a ~1KB server body holding nothing but an empty
1496
+ Suspense placeholder. It was reported against `react-markdown` → `micromark` →
1497
+ `decode-named-character-reference`, whose browser entry is `const element = document.createElement('i')`, and it
1498
+ hit **every** route: the generated root layout imports the app store, so the shared UI graph is in every client
1499
+ entry's SSR chunk graph.
1500
+
1501
+ `ClientEntriesBundler` now takes the target as an option — still `"browser"` for the browser bundle — and
1502
+ `resolveSsrClientBundleOptions` (was `resolveSsrClientExternalOptions`) carries `target: "bun"` for `start` and
1503
+ `build` alike, so neither `client-ssr` call site can drift. `conditions: ["node"]` is not a fix: Bun _adds_
1504
+ conditions to the target's defaults, so `browser` still wins. The target itself has to say server.
1505
+
1506
+ - ccf2ae2: feat(dev): SSR pages load their client code from a dev module registry
1507
+
1508
+ - Under `akan start`, an SSR page's `"use client"` code now comes from a module registry under
1509
+ `.akan/artifact/ssr-dev`, the same machinery the dev CSR registry uses, instead of the route chunks. React and the
1510
+ akanjs vendor facets stay the import map's, so the page keeps one React and one store registry. The route chunks and
1511
+ the `client-refresh` that re-imported all of them are gone from dev; `akan build` is unchanged.
1512
+ - A save patches the changed module in place with React Fast Refresh: state stays, and on minimal a component edit
1513
+ shows in about 110ms instead of 900ms. Undoing an edit now shows too; with chunks the browser kept the module it had
1514
+ already imported for that URL. A client module the RSC payload names that also exports constants re-runs in place.
1515
+ - A save that changes nothing the server renders (a `"use client"` module that keeps its export names, or a module only
1516
+ client code imports) keeps the page's build id and refetches no RSC payload. One that does — a component both sides
1517
+ render, a constant — holds its client patch until that save's pages build lands, then sends the patch and the RSC
1518
+ refresh together, so the page never runs new client code against old server props. A constant edit that reloads
1519
+ waits for that build too, and a backend restarted by a server-only save renders from the latest pages build.
1520
+ - The registry builds once in a build worker after the dev server boots, beside the builder's slow lane, and each route
1521
+ build adds the entries its page names that the registry lacks; a page served before the registry exists waits for
1522
+ it in the browser, in short holds. Route builds no longer bundle browser chunks in dev, which took about 240MB off
1523
+ the builder's peak on apps/akan.
1524
+ - Most saves are patched in the resident builder: a new file, a new import from a package something already resolved,
1525
+ a moved or renamed file (a case-only rename, `Foo.tsx` to `Foo/index.tsx`, a file created beside a folder an import
1526
+ resolved to), a deleted module brought back. A whole build in a build worker is left for a new npm package, a signal
1527
+ save (its metadata is inlined into the client runtime; a dictionary save does not rebuild the SSR registry, which
1528
+ inlines none), and a registry whose last whole build was cut short. A save that does not compile — a typo in an
1529
+ import included — is reported by the builder at once, and the save that fixes it patches in about 120ms.
1530
+ - A failed build keeps the registry the tabs and the next save read. Its error stays on the overlay, is shown to a tab
1531
+ opened while it stands, and clears with the save that fixes it — for a route that failed on a shared module too,
1532
+ which is built again once a newer build goes green. A broken file no page imports any more stops failing saves.
1533
+ - A client module whose top level threw, or a store that throws at startup, recovers with the save that fixes it, and
1534
+ the dev server's error page carries the HMR client, so it reloads once the fix lands. A pages bundle that builds but
1535
+ throws while it loads shows its error while the RSC worker keeps serving the bundle it had.
1536
+ - A tab that reconnects after a backend restart loads the patches it missed and refetches its payload instead of
1537
+ reloading; one that reconnects to a new dev session (a new `akan start`, a config restart) reloads onto its registry.
1538
+ A tab that booted beside the vendor file of the build before loads the newer one, so a new npm import in a leaf
1539
+ component works without a manual refresh.
1540
+ - A build worker exits with the builder that spawned it, and a boot build whose worker was killed is retried after 10
1541
+ seconds. On Windows, registry writes retry a rename refused while a reader holds the file, and a build worker hashes
1542
+ the same bundle config as the builder, so saves are not all handed to whole builds.
1543
+
1544
+ - 2d8de1c: fix(code): at most `maxConcurrent` sub-agents run at once across the whole tree
1545
+
1546
+ `/agents` says a profile runs sub-agents "at most N at once", but each `task` pool counted only its own children,
1547
+ so a child agent could open N of its own while its parent's N were still running — with the shipped presets (two
1548
+ levels, three at once) up to 12 sub-agents ran at once instead of 3.
1549
+
1550
+ The running count now travels with the tree's token tally (`SubagentSpend` gained `running`), so every pool of one
1551
+ tree checks the same number. A `task` over the limit is refused, as before, and never queued: every ancestor keeps
1552
+ its slot while it waits on its child, so a queued child would deadlock the tree. The refusal tells an agent that has
1553
+ sub-agents of its own running to wait for one of them or do the work itself, and one that has none to do it itself.
1554
+
1555
+ - 2d8de1c: fix(code): a sub-agent that fails or is stopped still spends from the tree's budget
1556
+
1557
+ A sub-agent's tokens were added to the tree's tally only when its run returned normally. One whose run threw — a
1558
+ provider or engine error after it had already spent tokens — added nothing and said nothing, so a tree of failing
1559
+ sub-agents could spend past the budget without `task` ever refusing; one stopped by its parent's interrupt was
1560
+ counted but announced as "finished".
1561
+
1562
+ Each sub-agent's own tokens are now added exactly once however it ends, and the notice says how it ended:
1563
+ `explore sub-agent finished|failed|stopped, N tokens (M of B spent)`. A sub-agent's own children are still counted
1564
+ by its pool, so nothing is counted twice.
1565
+
1566
+ - 2d8de1c: fix(code): an interrupt that lands before a sub-agent's run is live stops it
1567
+
1568
+ A sub-agent listened for its parent's interrupt only once it had been created, and the engine ignores an abort that
1569
+ arrives before a run is live. So an interrupt pressed while a `task` was still opening its sub-agent — connecting
1570
+ its MCP servers, say — or in the moment between that and the sub-agent's run starting was lost: the sub-agent ran
1571
+ its whole task anyway, and in the second case its notice even said "stopped".
1572
+
1573
+ A sub-agent whose parent was interrupted while it was being created now never starts, and one interrupted after that
1574
+ is stopped as soon as its run is live. Either way it reports `stopped`, and its tokens are added to the tree's tally
1575
+ once.
1576
+
1577
+ - 2d8de1c: fix(code): every sub-agent of one tree draws on one token budget
1578
+
1579
+ A profile's `subagent.budget` is documented, and shown by `/agents`, as the tokens "a whole subagent tree may
1580
+ spend", but each `task` pool kept its own count. A child's pool started from zero against the same ceiling and
1581
+ counted only its own children, so with the shipped presets (two levels, three at once) a tree could spend several
1582
+ times the budget before anything refused.
1583
+
1584
+ The count is now one object for the whole tree: the root pool starts it, hands it to every child agent it opens
1585
+ (`CodeAgentOptions.subagentSpend`, forwarded through `AkanCodePlugins` to the child's pool), and each sub-agent's
1586
+ tokens are added to it when that sub-agent finishes. Once the tree has spent the budget, `task` refuses at every
1587
+ level, and the "sub-agent finished" notice reports the tree's total. Depth and concurrency limits are unchanged.
1588
+
1589
+ - 5f53462: The CLI's AI editor is removed, and with it the `--ai` commands
1590
+
1591
+ `AiSession` — the langchain-backed editor that asked a model to write module constants, dictionaries, scalars,
1592
+ UI components, guideline instructions and abstract files — is deleted, along with every command that drove it.
1593
+ Gone: `akan set-llm`, `akan reset-llm`, `akan ask`, `akan compact`, `akan generate-instruction`, `akan
1594
+ update-instruction`, `akan generate-document`, `akan reapply-instruction`, and the `--ai` flag on `akan
1595
+ create-module` and `akan create-scalar`. The scaffolding halves of those two commands are unchanged and are now
1596
+ what they always do.
1597
+
1598
+ `akan guideline list` and `akan guideline show` are unaffected — guidelines are bundled files the CLI reads and
1599
+ never writes, and MCP `get_guideline` serves the same ones. `akan quality scan` still warns on an abstract past
1600
+ 300 lines; there is no longer a command that rewrites one for you.
1601
+
1602
+ The `@langchain/*` dependencies leave `@akanjs/cli`, `@akanjs/devkit` and the workspace root — they were the
1603
+ heaviest lazy-loaded stack the CLI carried, and `EntryModuleGraph` no longer has to guard against one reaching
1604
+ an entry. The workspace-global config at `~/.akan/config.json` no longer holds an LLM key; an existing `llm`
1605
+ entry is ignored and can be deleted.
1606
+
1607
+ This is the dev-time editor only. The in-page agent's runtime `LlmAdaptorRole`, `option.setLlm(...)` and the
1608
+ shipped adaptors are a different surface and are untouched.
1609
+
1610
+ Step-by-step migration: `akan guideline show workspaceRecipes`, Recipe 8.
1611
+
1612
+ - 2d8de1c: fix(build): `export *` in a `"use client"` module is a build error, and a dev build failure says why
1613
+
1614
+ The server replaces a `"use client"` module with one client reference per name the module exports, and the names
1615
+ come from Bun's export scan — which reports none through `export * from "./x"`. So a star re-export in a client
1616
+ module registered nothing: a star-only file was silently no client boundary at all, and a file mixing named exports
1617
+ with a star lost the star's names on the server, where they were `undefined` in a server component while the same
1618
+ import worked in a client one. `libs/shared`'s `User.Template.tsx` did exactly this with the leave-flow components.
1619
+
1620
+ Like Next.js, the build now refuses it, in `akan build` and `akan start` alike, naming the file, the specifiers and
1621
+ the fix:
1622
+
1623
+ ```
1624
+ libs/shared/lib/user/User.Template.tsx is a "use client" module, so it cannot `export * from "../../ui/UserLeave"`:
1625
+ the server sees only the names a client module declares, and a star re-export declares none. Re-export them by
1626
+ name — `export { … } from "../../ui/UserLeave"`.
1627
+ ```
1628
+
1629
+ `export type * from` and `export * as ns from` stay allowed, and a module without the directive is untouched. A
1630
+ barrel that mixes client-only files with isomorphic ones should drop the directive instead: each client file keeps
1631
+ its own, and the barrel stays importable on the server.
1632
+
1633
+ A failed bundle during `akan start` used to report only the bundler's own message, "Bundle failed". The dev build
1634
+ now prints the reasons nested under it, the way `akan build` already did.
1635
+
1636
+ - 2d8de1c: fix(windows): `akan` commands and the dev build worker no longer stop silently at a missing file
1637
+
1638
+ On Windows, Bun lets a process exit while a `Bun.file` read of a file that does not exist is still pending, if
1639
+ nothing else is keeping it alive. `akan build` stopped right after `sync` without an error, and the dev server's boot
1640
+ build ended with "build worker exited with code 0 before reporting a result" on any app that had no font cache yet.
1641
+ The CLI and its build workers now await their work from the entry module, which keeps the process alive until it
1642
+ settles; a failed command still prints its error once and exits 1.
1643
+
1644
+ - ccf2ae2: fix(cloud): on Windows, a `~/.akan/config.json` write waits for another command's read instead of failing
1645
+
1646
+ Windows refuses to rename over a file another process has open, and every `akan` command reads `config.json`
1647
+ outside its lock. On a Windows VM, a writer racing two readers had 1374 of 1579 renames fail with `EPERM`. A token
1648
+ refresh that lost that race fell back to the stored session. A session in its last hour is sent without
1649
+ `Authorization`, so one of two commands started together saw the cloud as signed out. And when the lost write was
1650
+ the one after the refresh, the rotated refresh token was never saved, so the next command had to sign in again.
1651
+ `GlobalConfig` now retries the rename on `EPERM`, `EACCES` and `EBUSY` for up to about five seconds on Windows.
1652
+ Under the same two readers, every write landed, the slowest after 40 tries (1.9s).
1653
+
1654
+ - Updated dependencies [ccf2ae2]
1655
+ - Updated dependencies [ccf2ae2]
1656
+ - Updated dependencies [ccf2ae2]
1657
+ - Updated dependencies [ccf2ae2]
1658
+ - Updated dependencies [ccf2ae2]
1659
+ - Updated dependencies [ccf2ae2]
1660
+ - Updated dependencies [ccf2ae2]
1661
+ - Updated dependencies [5f53462]
1662
+ - Updated dependencies [ccf2ae2]
1663
+ - Updated dependencies [ccf2ae2]
1664
+ - Updated dependencies [5f53462]
1665
+ - Updated dependencies [ccf2ae2]
1666
+ - Updated dependencies [ccf2ae2]
1667
+ - Updated dependencies [69f7178]
1668
+ - Updated dependencies [69f7178]
1669
+ - Updated dependencies [69f7178]
1670
+ - Updated dependencies [5f53462]
1671
+ - Updated dependencies [5f53462]
1672
+ - Updated dependencies [5f53462]
1673
+ - Updated dependencies [5f53462]
1674
+ - Updated dependencies [5f53462]
1675
+ - Updated dependencies [f882bbc]
1676
+ - Updated dependencies [8c31d54]
1677
+ - Updated dependencies [5f53462]
1678
+ - Updated dependencies [69f7178]
1679
+ - Updated dependencies [5f53462]
1680
+ - Updated dependencies [f882bbc]
1681
+ - Updated dependencies [5f53462]
1682
+ - Updated dependencies [5f53462]
1683
+ - Updated dependencies [f882bbc]
1684
+ - Updated dependencies [f882bbc]
1685
+ - Updated dependencies [5f53462]
1686
+ - Updated dependencies [5f53462]
1687
+ - Updated dependencies [69f7178]
1688
+ - Updated dependencies [5f53462]
1689
+ - Updated dependencies [5f53462]
1690
+ - Updated dependencies [69f7178]
1691
+ - Updated dependencies [f882bbc]
1692
+ - Updated dependencies [5f53462]
1693
+ - Updated dependencies [5f53462]
1694
+ - Updated dependencies [69f7178]
1695
+ - Updated dependencies [69f7178]
1696
+ - Updated dependencies [5f53462]
1697
+ - Updated dependencies [69f7178]
1698
+ - Updated dependencies [5f53462]
1699
+ - Updated dependencies [5f53462]
1700
+ - Updated dependencies [f882bbc]
1701
+ - Updated dependencies [69f7178]
1702
+ - Updated dependencies [69f7178]
1703
+ - Updated dependencies [ccf2ae2]
1704
+ - Updated dependencies [ccf2ae2]
1705
+ - Updated dependencies [ccf2ae2]
1706
+ - Updated dependencies [ccf2ae2]
1707
+ - Updated dependencies [5f53462]
1708
+ - Updated dependencies [ccf2ae2]
1709
+ - Updated dependencies [ccf2ae2]
1710
+ - Updated dependencies [ccf2ae2]
1711
+ - Updated dependencies [ccf2ae2]
1712
+ - Updated dependencies [5f53462]
1713
+ - Updated dependencies [c45ac6c]
1714
+ - Updated dependencies [69f7178]
1715
+ - Updated dependencies [69f7178]
1716
+ - Updated dependencies [5f53462]
1717
+ - Updated dependencies [5f53462]
1718
+ - Updated dependencies [2d8de1c]
1719
+ - Updated dependencies [5f53462]
1720
+ - Updated dependencies [8c31d54]
1721
+ - Updated dependencies [ccf2ae2]
1722
+ - Updated dependencies [5f53462]
1723
+ - Updated dependencies [5f53462]
1724
+ - Updated dependencies [ccf2ae2]
1725
+ - Updated dependencies [ccf2ae2]
1726
+ - Updated dependencies [ccf2ae2]
1727
+ - Updated dependencies [ccf2ae2]
1728
+ - Updated dependencies [ccf2ae2]
1729
+ - Updated dependencies [ccf2ae2]
1730
+ - Updated dependencies [ccf2ae2]
1731
+ - Updated dependencies [ccf2ae2]
1732
+ - Updated dependencies [ccf2ae2]
1733
+ - Updated dependencies [ccf2ae2]
1734
+ - Updated dependencies [ccf2ae2]
1735
+ - Updated dependencies [ccf2ae2]
1736
+ - Updated dependencies [ccf2ae2]
1737
+ - Updated dependencies [ccf2ae2]
1738
+ - Updated dependencies [5f53462]
1739
+ - Updated dependencies [ccf2ae2]
1740
+ - Updated dependencies [ccf2ae2]
1741
+ - Updated dependencies [ccf2ae2]
1742
+ - Updated dependencies [69f7178]
1743
+ - Updated dependencies [69f7178]
1744
+ - Updated dependencies [69f7178]
1745
+ - Updated dependencies [f882bbc]
1746
+ - Updated dependencies [2d8de1c]
1747
+ - Updated dependencies [69f7178]
1748
+ - Updated dependencies [5f53462]
1749
+ - Updated dependencies [22aa04f]
1750
+ - Updated dependencies [69f7178]
1751
+ - Updated dependencies [5f53462]
1752
+ - Updated dependencies [ccf2ae2]
1753
+ - Updated dependencies [2d8de1c]
1754
+ - Updated dependencies [69f7178]
1755
+ - Updated dependencies [2d8de1c]
1756
+ - Updated dependencies [2d8de1c]
1757
+ - Updated dependencies [2d8de1c]
1758
+ - Updated dependencies [f882bbc]
1759
+ - Updated dependencies [69f7178]
1760
+ - Updated dependencies [f882bbc]
1761
+ - Updated dependencies [f882bbc]
1762
+ - Updated dependencies [5f53462]
1763
+ - Updated dependencies [22aa04f]
1764
+ - Updated dependencies [69f7178]
1765
+ - Updated dependencies [2d8de1c]
1766
+ - Updated dependencies [ccf2ae2]
1767
+ - Updated dependencies [5f53462]
1768
+ - Updated dependencies [5f53462]
1769
+ - Updated dependencies [5f53462]
1770
+ - Updated dependencies [ccf2ae2]
1771
+ - Updated dependencies [f882bbc]
1772
+ - Updated dependencies [69f7178]
1773
+ - Updated dependencies [5f53462]
1774
+ - Updated dependencies [5f53462]
1775
+ - Updated dependencies [5f53462]
1776
+ - Updated dependencies [3ebd8e1]
1777
+ - Updated dependencies [326da07]
1778
+ - Updated dependencies [ccf2ae2]
1779
+ - Updated dependencies [ccf2ae2]
1780
+ - Updated dependencies [ccf2ae2]
1781
+ - Updated dependencies [ccf2ae2]
1782
+ - Updated dependencies [5f53462]
1783
+ - Updated dependencies [c45ac6c]
1784
+ - Updated dependencies [2d8de1c]
1785
+ - Updated dependencies [ccf2ae2]
1786
+ - Updated dependencies [ccf2ae2]
1787
+ - Updated dependencies [22aa04f]
1788
+ - Updated dependencies [f882bbc]
1789
+ - Updated dependencies [2d8de1c]
1790
+ - Updated dependencies [5f53462]
1791
+ - Updated dependencies [c45ac6c]
1792
+ - Updated dependencies [f882bbc]
1793
+ - Updated dependencies [2d8de1c]
1794
+ - Updated dependencies [ccf2ae2]
1795
+ - Updated dependencies [2d8de1c]
1796
+ - Updated dependencies [ccf2ae2]
1797
+ - Updated dependencies [f882bbc]
1798
+ - Updated dependencies [326da07]
1799
+ - Updated dependencies [ccf2ae2]
1800
+ - Updated dependencies [5f53462]
1801
+ - Updated dependencies [2d8de1c]
1802
+ - Updated dependencies [5f53462]
1803
+ - Updated dependencies [5f53462]
1804
+ - Updated dependencies [ccf2ae2]
1805
+ - Updated dependencies [2d8de1c]
1806
+ - Updated dependencies [2d8de1c]
1807
+ - Updated dependencies [2d8de1c]
1808
+ - Updated dependencies [f882bbc]
1809
+ - Updated dependencies [f882bbc]
1810
+ - Updated dependencies [69f7178]
1811
+ - Updated dependencies [2d8de1c]
1812
+ - Updated dependencies [5f53462]
1813
+ - Updated dependencies [5f53462]
1814
+ - Updated dependencies [5f53462]
1815
+ - Updated dependencies [ccf2ae2]
1816
+ - Updated dependencies [bf27773]
1817
+ - Updated dependencies [2d8de1c]
1818
+ - Updated dependencies [5f53462]
1819
+ - Updated dependencies [69f7178]
1820
+ - Updated dependencies [5f53462]
1821
+ - Updated dependencies [5f53462]
1822
+ - Updated dependencies [ccf2ae2]
1823
+ - Updated dependencies [ccf2ae2]
1824
+ - Updated dependencies [2d8de1c]
1825
+ - Updated dependencies [2d8de1c]
1826
+ - Updated dependencies [2d8de1c]
1827
+ - Updated dependencies [2d8de1c]
1828
+ - Updated dependencies [ccf2ae2]
1829
+ - Updated dependencies [5f53462]
1830
+ - Updated dependencies [ccf2ae2]
1831
+ - Updated dependencies [5f53462]
1832
+ - Updated dependencies [f882bbc]
1833
+ - akanjs@3.0.0
1834
+
3
1835
  ## 2.4.2
4
1836
 
5
1837
  ### Patch Changes