@akanjs/devkit 3.0.0-beta.9 → 3.0.1

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