@zerwiz/ymir 0.1.33 → 0.1.35

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 (930) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/apps/README.md +4 -0
  3. package/apps/hlidskjalf/README.md +68 -0
  4. package/apps/hlidskjalf/android/app/build.gradle +73 -0
  5. package/apps/hlidskjalf/android/app/capacitor.build.gradle +19 -0
  6. package/apps/hlidskjalf/android/app/proguard-rules.pro +21 -0
  7. package/apps/hlidskjalf/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java +26 -0
  8. package/apps/hlidskjalf/android/app/src/main/AndroidManifest.xml +41 -0
  9. package/apps/hlidskjalf/android/app/src/main/java/org/zerwiz/ymir/MainActivity.java +5 -0
  10. package/apps/hlidskjalf/android/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  11. package/apps/hlidskjalf/android/app/src/main/res/drawable/splash.png +0 -0
  12. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-hdpi/splash.png +0 -0
  13. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-mdpi/splash.png +0 -0
  14. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xhdpi/splash.png +0 -0
  15. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxhdpi/splash.png +0 -0
  16. package/apps/hlidskjalf/android/app/src/main/res/drawable-land-xxxhdpi/splash.png +0 -0
  17. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-hdpi/splash.png +0 -0
  18. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-mdpi/splash.png +0 -0
  19. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xhdpi/splash.png +0 -0
  20. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxhdpi/splash.png +0 -0
  21. package/apps/hlidskjalf/android/app/src/main/res/drawable-port-xxxhdpi/splash.png +0 -0
  22. package/apps/hlidskjalf/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +34 -0
  23. package/apps/hlidskjalf/android/app/src/main/res/layout/activity_main.xml +12 -0
  24. package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  25. package/apps/hlidskjalf/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
  26. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  27. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png +0 -0
  28. package/apps/hlidskjalf/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  29. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png +0 -0
  31. package/apps/hlidskjalf/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  32. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  33. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png +0 -0
  34. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  35. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  36. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png +0 -0
  37. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  38. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  39. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png +0 -0
  40. package/apps/hlidskjalf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  41. package/apps/hlidskjalf/android/app/src/main/res/values/ic_launcher_background.xml +4 -0
  42. package/apps/hlidskjalf/android/app/src/main/res/values/strings.xml +7 -0
  43. package/apps/hlidskjalf/android/app/src/main/res/values/styles.xml +22 -0
  44. package/apps/hlidskjalf/android/app/src/main/res/xml/file_paths.xml +5 -0
  45. package/apps/hlidskjalf/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java +18 -0
  46. package/apps/hlidskjalf/android/build.gradle +29 -0
  47. package/apps/hlidskjalf/android/capacitor.settings.gradle +3 -0
  48. package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  49. package/apps/hlidskjalf/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  50. package/apps/hlidskjalf/android/gradle.properties +22 -0
  51. package/apps/hlidskjalf/android/gradlew +248 -0
  52. package/apps/hlidskjalf/android/gradlew.bat +92 -0
  53. package/apps/hlidskjalf/android/settings.gradle +5 -0
  54. package/apps/hlidskjalf/android/variables.gradle +16 -0
  55. package/apps/hlidskjalf/capacitor.config.ts +26 -0
  56. package/apps/hlidskjalf/dist/apple-touch-icon.png +0 -0
  57. package/apps/hlidskjalf/dist/assets/index-BWmNIyaG.css +1 -0
  58. package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js +88 -0
  59. package/apps/hlidskjalf/dist/assets/index-k7QEfNto.js.map +1 -0
  60. package/apps/hlidskjalf/dist/icon.svg +7 -0
  61. package/apps/hlidskjalf/dist/index.html +64 -0
  62. package/apps/hlidskjalf/dist/manifest.webmanifest +31 -0
  63. package/apps/hlidskjalf/dist/og.jpg +0 -0
  64. package/apps/hlidskjalf/dist/og.png +0 -0
  65. package/apps/hlidskjalf/dist/og.svg +64 -0
  66. package/apps/hlidskjalf/dist/sw.js +39 -0
  67. package/apps/hlidskjalf/dist/ymir-icon.png +0 -0
  68. package/apps/hlidskjalf/dist/ymir-mark.svg +62 -0
  69. package/apps/hlidskjalf/electron/icon.png +0 -0
  70. package/apps/hlidskjalf/electron/main.cjs +236 -0
  71. package/apps/hlidskjalf/electron/preload.cjs +8 -0
  72. package/apps/hlidskjalf/electron/smidja-icon.png +0 -0
  73. package/apps/hlidskjalf/electron/ymir-hlidskjalf.desktop.in +12 -0
  74. package/apps/hlidskjalf/electron/ymir-smidja.desktop.in +12 -0
  75. package/apps/hlidskjalf/index.html +63 -0
  76. package/apps/hlidskjalf/package-lock.json +5398 -0
  77. package/apps/hlidskjalf/package.json +55 -0
  78. package/apps/hlidskjalf/public/apple-touch-icon.png +0 -0
  79. package/apps/hlidskjalf/public/icon.svg +7 -0
  80. package/apps/hlidskjalf/public/manifest.webmanifest +31 -0
  81. package/apps/hlidskjalf/public/og.jpg +0 -0
  82. package/apps/hlidskjalf/public/og.png +0 -0
  83. package/apps/hlidskjalf/public/og.svg +64 -0
  84. package/apps/hlidskjalf/public/sw.js +39 -0
  85. package/apps/hlidskjalf/public/ymir-icon.png +0 -0
  86. package/apps/hlidskjalf/public/ymir-mark.svg +62 -0
  87. package/apps/hlidskjalf/server/accounts.ts +159 -0
  88. package/apps/hlidskjalf/server/index.ts +2045 -0
  89. package/apps/hlidskjalf/src/app/App.tsx +116 -0
  90. package/apps/hlidskjalf/src/app/BottomStream.tsx +128 -0
  91. package/apps/hlidskjalf/src/app/MobileNav.tsx +87 -0
  92. package/apps/hlidskjalf/src/app/Rail.tsx +106 -0
  93. package/apps/hlidskjalf/src/app/Shell.tsx +135 -0
  94. package/apps/hlidskjalf/src/app/Topbar.tsx +130 -0
  95. package/apps/hlidskjalf/src/components/AccentPicker.tsx +207 -0
  96. package/apps/hlidskjalf/src/components/AccountMenu.tsx +115 -0
  97. package/apps/hlidskjalf/src/components/AgentCard.tsx +75 -0
  98. package/apps/hlidskjalf/src/components/EmberBackground.tsx +36 -0
  99. package/apps/hlidskjalf/src/components/Emblem.tsx +48 -0
  100. package/apps/hlidskjalf/src/components/Halls.tsx +155 -0
  101. package/apps/hlidskjalf/src/components/LoginModal.tsx +329 -0
  102. package/apps/hlidskjalf/src/components/Markdown.tsx +10 -0
  103. package/apps/hlidskjalf/src/components/MetricTile.tsx +40 -0
  104. package/apps/hlidskjalf/src/components/ModelPicker.tsx +100 -0
  105. package/apps/hlidskjalf/src/components/Overlay.tsx +134 -0
  106. package/apps/hlidskjalf/src/components/PRCard.tsx +154 -0
  107. package/apps/hlidskjalf/src/components/RecallPanel.tsx +116 -0
  108. package/apps/hlidskjalf/src/components/RuneTag.tsx +23 -0
  109. package/apps/hlidskjalf/src/components/Status.tsx +35 -0
  110. package/apps/hlidskjalf/src/components/TaskChip.tsx +23 -0
  111. package/apps/hlidskjalf/src/components/TraceRow.tsx +28 -0
  112. package/apps/hlidskjalf/src/data/lore.ts +127 -0
  113. package/apps/hlidskjalf/src/data/metadata.ts +147 -0
  114. package/apps/hlidskjalf/src/data/mythology.ts +95 -0
  115. package/apps/hlidskjalf/src/data/realms.ts +167 -0
  116. package/apps/hlidskjalf/src/gates/Cron.tsx +77 -0
  117. package/apps/hlidskjalf/src/gates/Decisions.tsx +67 -0
  118. package/apps/hlidskjalf/src/gates/Files.tsx +138 -0
  119. package/apps/hlidskjalf/src/gates/Fleet.tsx +184 -0
  120. package/apps/hlidskjalf/src/gates/Forge.tsx +486 -0
  121. package/apps/hlidskjalf/src/gates/OmniChat.tsx +217 -0
  122. package/apps/hlidskjalf/src/gates/Processes.tsx +162 -0
  123. package/apps/hlidskjalf/src/gates/Profile.tsx +225 -0
  124. package/apps/hlidskjalf/src/gates/Reviews.tsx +33 -0
  125. package/apps/hlidskjalf/src/gates/Runes.tsx +116 -0
  126. package/apps/hlidskjalf/src/gates/Runtime.tsx +106 -0
  127. package/apps/hlidskjalf/src/gates/Sessions.tsx +109 -0
  128. package/apps/hlidskjalf/src/gates/Stats.tsx +268 -0
  129. package/apps/hlidskjalf/src/gates/Tasks.tsx +173 -0
  130. package/apps/hlidskjalf/src/gates/Trace.tsx +155 -0
  131. package/apps/hlidskjalf/src/gates/Well.tsx +154 -0
  132. package/apps/hlidskjalf/src/gates/Worktrees.tsx +48 -0
  133. package/apps/hlidskjalf/src/hooks/useTenantDef.ts +13 -0
  134. package/apps/hlidskjalf/src/main.tsx +31 -0
  135. package/apps/hlidskjalf/src/services/api.ts +404 -0
  136. package/apps/hlidskjalf/src/services/auth.ts +107 -0
  137. package/apps/hlidskjalf/src/services/stream.ts +35 -0
  138. package/apps/hlidskjalf/src/state/store.ts +648 -0
  139. package/apps/hlidskjalf/src/state/ui.ts +62 -0
  140. package/apps/hlidskjalf/src/styles/auth.css +485 -0
  141. package/apps/hlidskjalf/src/styles/components.css +1259 -0
  142. package/apps/hlidskjalf/src/styles/forge.css +115 -0
  143. package/apps/hlidskjalf/src/styles/global.css +167 -0
  144. package/apps/hlidskjalf/src/styles/menus.css +175 -0
  145. package/apps/hlidskjalf/src/styles/mobile.css +298 -0
  146. package/apps/hlidskjalf/src/styles/overlays.css +205 -0
  147. package/apps/hlidskjalf/src/styles/shell.css +641 -0
  148. package/apps/hlidskjalf/src/types.ts +251 -0
  149. package/apps/hlidskjalf/src/vite-env.d.ts +9 -0
  150. package/apps/hlidskjalf/static/landing.md +6 -0
  151. package/apps/hlidskjalf/tsconfig.json +25 -0
  152. package/apps/hlidskjalf/vite.config.ts +49 -0
  153. package/apps/hlidskjalf-mobile/App.tsx +93 -0
  154. package/apps/hlidskjalf-mobile/app.json +13 -0
  155. package/apps/hlidskjalf-mobile/index.ts +4 -0
  156. package/apps/hlidskjalf-mobile/package.json +35 -0
  157. package/apps/hlidskjalf-mobile/public/icon.svg +7 -0
  158. package/apps/hlidskjalf-mobile/tsconfig.json +4 -0
  159. package/apps/odrerir/.astro/content.d.ts +159 -0
  160. package/apps/odrerir/.astro/dev.json +24 -0
  161. package/apps/odrerir/.astro/settings.json +5 -0
  162. package/apps/odrerir/.astro/types.d.ts +2 -0
  163. package/apps/odrerir/astro.config.mjs +79 -0
  164. package/apps/odrerir/dist/apple-touch-icon.png +0 -0
  165. package/apps/odrerir/dist/favicon-192.png +0 -0
  166. package/apps/odrerir/dist/favicon-32.png +0 -0
  167. package/apps/odrerir/dist/favicon-512.png +0 -0
  168. package/apps/odrerir/dist/favicon.svg +53 -0
  169. package/apps/odrerir/dist/icon.svg +7 -0
  170. package/apps/odrerir/dist/index.html +837 -0
  171. package/apps/odrerir/dist/og-square.png +0 -0
  172. package/apps/odrerir/dist/og-story.png +0 -0
  173. package/apps/odrerir/dist/og.png +0 -0
  174. package/apps/odrerir/dist/safari-pinned-tab.svg +7 -0
  175. package/apps/odrerir/dist/site.webmanifest +16 -0
  176. package/apps/odrerir/dist/styles/cloth.css +112 -0
  177. package/apps/odrerir/dist/styles/livehall.css +278 -0
  178. package/apps/odrerir/electron/icon.png +0 -0
  179. package/apps/odrerir/electron/main.cjs +183 -0
  180. package/apps/odrerir/electron/ymir-odrerir.desktop.in +12 -0
  181. package/apps/odrerir/package-lock.json +4195 -0
  182. package/apps/odrerir/package.json +30 -0
  183. package/apps/odrerir/public/apple-touch-icon.png +0 -0
  184. package/apps/odrerir/public/favicon-192.png +0 -0
  185. package/apps/odrerir/public/favicon-32.png +0 -0
  186. package/apps/odrerir/public/favicon-512.png +0 -0
  187. package/apps/odrerir/public/favicon.svg +53 -0
  188. package/apps/odrerir/public/icon.svg +7 -0
  189. package/apps/odrerir/public/og-square.png +0 -0
  190. package/apps/odrerir/public/og-story.png +0 -0
  191. package/apps/odrerir/public/og.png +0 -0
  192. package/apps/odrerir/public/safari-pinned-tab.svg +7 -0
  193. package/apps/odrerir/public/site.webmanifest +16 -0
  194. package/apps/odrerir/scripts/extract-cloth.mjs +101 -0
  195. package/apps/odrerir/src/index.html +837 -0
  196. package/apps/odrerir/src/styles/cloth.css +112 -0
  197. package/apps/odrerir/src/styles/livehall.css +278 -0
  198. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/bug_report.yml +99 -0
  199. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/config.yml +8 -0
  200. package/apps/sessrumnir/.github/ISSUE_TEMPLATE/feature_request.yml +46 -0
  201. package/apps/sessrumnir/.github/PULL_REQUEST_TEMPLATE.md +56 -0
  202. package/apps/sessrumnir/.github/SUPPORT.md +18 -0
  203. package/apps/sessrumnir/.github/workflows/build.yml +133 -0
  204. package/apps/sessrumnir/AGENTS.md +540 -0
  205. package/apps/sessrumnir/CLA.md +63 -0
  206. package/apps/sessrumnir/CONTRIBUTING.md +233 -0
  207. package/apps/sessrumnir/LICENSE +200 -0
  208. package/apps/sessrumnir/NOTICE +33 -0
  209. package/apps/sessrumnir/README.md +329 -0
  210. package/apps/sessrumnir/bin/sessrumnir-web.sh +117 -0
  211. package/apps/sessrumnir/bin/sessrumnir.js +169 -0
  212. package/apps/sessrumnir/docs/CNAME +1 -0
  213. package/apps/sessrumnir/docs/favicon.ico +0 -0
  214. package/apps/sessrumnir/docs/icon-512.png +0 -0
  215. package/apps/sessrumnir/docs/index.html +441 -0
  216. package/apps/sessrumnir/docs/robots.txt +4 -0
  217. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_181929.png +0 -0
  218. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182005.png +0 -0
  219. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182020.png +0 -0
  220. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182039.png +0 -0
  221. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182117.png +0 -0
  222. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182225.png +0 -0
  223. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182248.png +0 -0
  224. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182322.png +0 -0
  225. package/apps/sessrumnir/docs/screenshots/Screenshot_20260824_182340.png +0 -0
  226. package/apps/sessrumnir/docs/sitemap.xml +9 -0
  227. package/apps/sessrumnir/electron.vite.config.ts +49 -0
  228. package/apps/sessrumnir/eslint.config.mjs +75 -0
  229. package/apps/sessrumnir/i18next.config.ts +33 -0
  230. package/apps/sessrumnir/install.sh +144 -0
  231. package/apps/sessrumnir/out/main/index.js +10881 -0
  232. package/apps/sessrumnir/out/preload/index.js +722 -0
  233. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-C8QS47vb.woff2 +0 -0
  234. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-400-normal-D3EsxgFc.woff +0 -0
  235. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-B7dJQtg-.woff +0 -0
  236. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-500-normal-BLlg2W5x.woff2 +0 -0
  237. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-BcIsDXzC.woff2 +0 -0
  238. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-600-normal-CBymlcNF.woff +0 -0
  239. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-Bgrpe4p1.woff +0 -0
  240. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-400-normal-BlcaxZtM.woff2 +0 -0
  241. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-CdQuyvtc.woff +0 -0
  242. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-500-normal-pZw22qtS.woff2 +0 -0
  243. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-Dr-wLyOm.woff +0 -0
  244. package/apps/sessrumnir/out/renderer/assets/cormorant-cyrillic-ext-600-normal-dLR2-a_v.woff2 +0 -0
  245. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-BGH8Vunh.woff2 +0 -0
  246. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-400-normal-C3_-2Ua-.woff +0 -0
  247. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-Dj3SQ6fR.woff +0 -0
  248. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-500-normal-EBdSCOD3.woff2 +0 -0
  249. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-B254Xisw.woff2 +0 -0
  250. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-600-normal-DUW-TI76.woff +0 -0
  251. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuQ88yz3.woff2 +0 -0
  252. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-400-normal-DuXFa1Dr.woff +0 -0
  253. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-AH9qog1s.woff2 +0 -0
  254. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-500-normal-DAuUCO41.woff +0 -0
  255. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-CD-dlDpD.woff +0 -0
  256. package/apps/sessrumnir/out/renderer/assets/cormorant-latin-ext-600-normal-D3T6BBXG.woff2 +0 -0
  257. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-C-RiYxEf.woff2 +0 -0
  258. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-400-normal-DmUuA7Y2.woff +0 -0
  259. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-DsPuwQHi.woff2 +0 -0
  260. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-500-normal-tGBW_mI7.woff +0 -0
  261. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-Dar_cCT6.woff2 +0 -0
  262. package/apps/sessrumnir/out/renderer/assets/cormorant-vietnamese-600-normal-GIs8CF2x.woff +0 -0
  263. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-Ba-HN6uq.woff +0 -0
  264. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-300-normal-ByB2hQUF.woff2 +0 -0
  265. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  266. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  267. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  268. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  269. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-B8O3uHbr.woff +0 -0
  270. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-300-normal-BbtBtHoB.woff2 +0 -0
  271. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  272. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  273. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  274. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  275. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BPU54Tw3.woff +0 -0
  276. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-300-normal-BRS-C4KA.woff2 +0 -0
  277. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  278. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  279. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  280. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  281. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-BEtQ2wm2.woff2 +0 -0
  282. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-300-normal-CQbCaAQ4.woff +0 -0
  283. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  284. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  285. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  286. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  287. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-B6fSpYYV.woff +0 -0
  288. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-300-normal-CpdbWO_2.woff2 +0 -0
  289. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  290. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  291. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  292. package/apps/sessrumnir/out/renderer/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  293. package/apps/sessrumnir/out/renderer/assets/index-BxqsRq2x.css +4858 -0
  294. package/apps/sessrumnir/out/renderer/assets/index-TaD69rFk.js +99435 -0
  295. package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  296. package/apps/sessrumnir/out/renderer/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  297. package/apps/sessrumnir/out/renderer/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  298. package/apps/sessrumnir/out/renderer/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  299. package/apps/sessrumnir/out/renderer/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  300. package/apps/sessrumnir/out/renderer/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  301. package/apps/sessrumnir/out/renderer/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  302. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  303. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  304. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  305. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  306. package/apps/sessrumnir/out/renderer/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  307. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-italic-DFS986qL.woff2 +0 -0
  308. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-ext-wght-normal-C-3rgBeH.woff2 +0 -0
  309. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-italic-Bxi8ein9.woff2 +0 -0
  310. package/apps/sessrumnir/out/renderer/assets/newsreader-latin-wght-normal-CCVVNp6i.woff2 +0 -0
  311. package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-italic-xAqemagA.woff2 +0 -0
  312. package/apps/sessrumnir/out/renderer/assets/newsreader-vietnamese-wght-normal-Czsa-EzN.woff2 +0 -0
  313. package/apps/sessrumnir/out/renderer/assets/openmoji-color-9qo_2DLG.woff2 +0 -0
  314. package/apps/sessrumnir/out/renderer/assets/pi-logo-B6SVnhuS.svg +7 -0
  315. package/apps/sessrumnir/out/renderer/index.html +19 -0
  316. package/apps/sessrumnir/out/renderer/theme-boot.js +30 -0
  317. package/apps/sessrumnir/package-lock.json +10636 -0
  318. package/apps/sessrumnir/package.json +183 -0
  319. package/apps/sessrumnir/public/apple-touch-icon.png +0 -0
  320. package/apps/sessrumnir/public/icon.svg +7 -0
  321. package/apps/sessrumnir/resources/icons/generate_icons.py +38 -0
  322. package/apps/sessrumnir/resources/icons/icon-128.png +0 -0
  323. package/apps/sessrumnir/resources/icons/icon-16.png +0 -0
  324. package/apps/sessrumnir/resources/icons/icon-256.png +0 -0
  325. package/apps/sessrumnir/resources/icons/icon-32.png +0 -0
  326. package/apps/sessrumnir/resources/icons/icon-48.png +0 -0
  327. package/apps/sessrumnir/resources/icons/icon-512.png +0 -0
  328. package/apps/sessrumnir/resources/icons/icon-64.png +0 -0
  329. package/apps/sessrumnir/resources/icons/icon.icns +0 -0
  330. package/apps/sessrumnir/resources/icons/icon.ico +0 -0
  331. package/apps/sessrumnir/resources/icons/icon.png +0 -0
  332. package/apps/sessrumnir/resources/icons/icon.svg +7 -0
  333. package/apps/sessrumnir/resources/linux/after-install.sh +23 -0
  334. package/apps/sessrumnir/resources/linux/after-remove.sh +18 -0
  335. package/apps/sessrumnir/resources/locales/en/translation.json +1735 -0
  336. package/apps/sessrumnir/resources/permission-prompt-text.test.ts +111 -0
  337. package/apps/sessrumnir/resources/permission-prompt-text.ts +84 -0
  338. package/apps/sessrumnir/resources/permission-rules.test.ts +351 -0
  339. package/apps/sessrumnir/resources/permission-rules.ts +293 -0
  340. package/apps/sessrumnir/resources/pi-desktop-permissions.ts +62 -0
  341. package/apps/sessrumnir/resources/ymir-sessrumnir.desktop.in +12 -0
  342. package/apps/sessrumnir/scripts/check-semantic-colors.mjs +57 -0
  343. package/apps/sessrumnir/src/main/activity-stats.test.ts +240 -0
  344. package/apps/sessrumnir/src/main/activity-stats.ts +596 -0
  345. package/apps/sessrumnir/src/main/agent-detection.test.ts +25 -0
  346. package/apps/sessrumnir/src/main/agent-detection.ts +114 -0
  347. package/apps/sessrumnir/src/main/app-data-paths.test.ts +90 -0
  348. package/apps/sessrumnir/src/main/app-data-paths.ts +95 -0
  349. package/apps/sessrumnir/src/main/app-log.test.ts +114 -0
  350. package/apps/sessrumnir/src/main/app-log.ts +210 -0
  351. package/apps/sessrumnir/src/main/archived-sessions.test.ts +50 -0
  352. package/apps/sessrumnir/src/main/archived-sessions.ts +95 -0
  353. package/apps/sessrumnir/src/main/attachment-reader.test.ts +56 -0
  354. package/apps/sessrumnir/src/main/attachment-reader.ts +52 -0
  355. package/apps/sessrumnir/src/main/auto-tag.test.ts +38 -0
  356. package/apps/sessrumnir/src/main/auto-tag.ts +75 -0
  357. package/apps/sessrumnir/src/main/autostart-linux.test.ts +56 -0
  358. package/apps/sessrumnir/src/main/autostart-linux.ts +58 -0
  359. package/apps/sessrumnir/src/main/cmd-escape.test.ts +424 -0
  360. package/apps/sessrumnir/src/main/cmd-escape.ts +186 -0
  361. package/apps/sessrumnir/src/main/council-manager.test.ts +167 -0
  362. package/apps/sessrumnir/src/main/council-manager.ts +265 -0
  363. package/apps/sessrumnir/src/main/diagnostics-report.test.ts +140 -0
  364. package/apps/sessrumnir/src/main/diagnostics-report.ts +91 -0
  365. package/apps/sessrumnir/src/main/diagnostics.ts +111 -0
  366. package/apps/sessrumnir/src/main/editor-guard.test.ts +59 -0
  367. package/apps/sessrumnir/src/main/editor-guard.ts +50 -0
  368. package/apps/sessrumnir/src/main/extension-ui-ipc.test.ts +299 -0
  369. package/apps/sessrumnir/src/main/extension-ui-ipc.ts +117 -0
  370. package/apps/sessrumnir/src/main/file-service.test.ts +279 -0
  371. package/apps/sessrumnir/src/main/file-service.ts +652 -0
  372. package/apps/sessrumnir/src/main/fs-errors.ts +42 -0
  373. package/apps/sessrumnir/src/main/get-messages-trim.test.ts +77 -0
  374. package/apps/sessrumnir/src/main/get-messages-trim.ts +97 -0
  375. package/apps/sessrumnir/src/main/git-conveyor.test.ts +305 -0
  376. package/apps/sessrumnir/src/main/git-conveyor.ts +338 -0
  377. package/apps/sessrumnir/src/main/git-worktree.test.ts +101 -0
  378. package/apps/sessrumnir/src/main/git-worktree.ts +201 -0
  379. package/apps/sessrumnir/src/main/i18n.ts +31 -0
  380. package/apps/sessrumnir/src/main/index.ts +619 -0
  381. package/apps/sessrumnir/src/main/ipc/active-engine.ts +15 -0
  382. package/apps/sessrumnir/src/main/ipc/context.ts +58 -0
  383. package/apps/sessrumnir/src/main/ipc/council-handlers.ts +126 -0
  384. package/apps/sessrumnir/src/main/ipc/diagnostics-handlers.ts +8 -0
  385. package/apps/sessrumnir/src/main/ipc/file-handlers.ts +96 -0
  386. package/apps/sessrumnir/src/main/ipc/git-conveyor-handlers.ts +55 -0
  387. package/apps/sessrumnir/src/main/ipc/model-handlers.ts +33 -0
  388. package/apps/sessrumnir/src/main/ipc/models-config-handlers.ts +80 -0
  389. package/apps/sessrumnir/src/main/ipc/notes-handlers.ts +65 -0
  390. package/apps/sessrumnir/src/main/ipc/package-handlers.ts +330 -0
  391. package/apps/sessrumnir/src/main/ipc/permission-rules-handlers.ts +223 -0
  392. package/apps/sessrumnir/src/main/ipc/pi-handlers.ts +136 -0
  393. package/apps/sessrumnir/src/main/ipc/pi-start-options.ts +150 -0
  394. package/apps/sessrumnir/src/main/ipc/run-pi-cli.ts +53 -0
  395. package/apps/sessrumnir/src/main/ipc/session-handlers.ts +491 -0
  396. package/apps/sessrumnir/src/main/ipc/settings.ts +120 -0
  397. package/apps/sessrumnir/src/main/ipc/skills-mcp-handlers.ts +192 -0
  398. package/apps/sessrumnir/src/main/ipc/system-handlers.ts +131 -0
  399. package/apps/sessrumnir/src/main/ipc/tag-handlers.ts +69 -0
  400. package/apps/sessrumnir/src/main/ipc/terminal-handlers.ts +41 -0
  401. package/apps/sessrumnir/src/main/ipc/theme-handlers.ts +131 -0
  402. package/apps/sessrumnir/src/main/ipc/update-handlers.ts +61 -0
  403. package/apps/sessrumnir/src/main/ipc/validation.ts +45 -0
  404. package/apps/sessrumnir/src/main/ipc/workflow-handlers.ts +108 -0
  405. package/apps/sessrumnir/src/main/ipc/workspace-activity-wiring.ts +179 -0
  406. package/apps/sessrumnir/src/main/ipc/workspace-handlers.ts +133 -0
  407. package/apps/sessrumnir/src/main/ipc-handlers.ts +98 -0
  408. package/apps/sessrumnir/src/main/map-concurrent.test.ts +80 -0
  409. package/apps/sessrumnir/src/main/map-concurrent.ts +30 -0
  410. package/apps/sessrumnir/src/main/models-file.test.ts +114 -0
  411. package/apps/sessrumnir/src/main/models-file.ts +89 -0
  412. package/apps/sessrumnir/src/main/notes-manager.test.ts +64 -0
  413. package/apps/sessrumnir/src/main/notes-manager.ts +183 -0
  414. package/apps/sessrumnir/src/main/notify-decision.test.ts +39 -0
  415. package/apps/sessrumnir/src/main/notify-decision.ts +25 -0
  416. package/apps/sessrumnir/src/main/omp-fork-points.test.ts +119 -0
  417. package/apps/sessrumnir/src/main/omp-fork-points.ts +105 -0
  418. package/apps/sessrumnir/src/main/omp-plugin-list.test.ts +94 -0
  419. package/apps/sessrumnir/src/main/omp-plugin-list.ts +103 -0
  420. package/apps/sessrumnir/src/main/package-catalog.test.ts +157 -0
  421. package/apps/sessrumnir/src/main/package-catalog.ts +166 -0
  422. package/apps/sessrumnir/src/main/package-updates.test.ts +149 -0
  423. package/apps/sessrumnir/src/main/package-updates.ts +185 -0
  424. package/apps/sessrumnir/src/main/path-authorization.test.ts +64 -0
  425. package/apps/sessrumnir/src/main/path-authorization.ts +31 -0
  426. package/apps/sessrumnir/src/main/pi-binary-resolution.test.ts +582 -0
  427. package/apps/sessrumnir/src/main/pi-binary-resolution.ts +649 -0
  428. package/apps/sessrumnir/src/main/pi-event-router.test.ts +388 -0
  429. package/apps/sessrumnir/src/main/pi-event-router.ts +290 -0
  430. package/apps/sessrumnir/src/main/pi-paths.test.ts +134 -0
  431. package/apps/sessrumnir/src/main/pi-paths.ts +74 -0
  432. package/apps/sessrumnir/src/main/pi-rpc-manager.test.ts +467 -0
  433. package/apps/sessrumnir/src/main/pi-rpc-manager.ts +1319 -0
  434. package/apps/sessrumnir/src/main/process-tree.test.ts +94 -0
  435. package/apps/sessrumnir/src/main/renderer-origin.test.ts +33 -0
  436. package/apps/sessrumnir/src/main/renderer-origin.ts +35 -0
  437. package/apps/sessrumnir/src/main/session-lineage-reader.test.ts +317 -0
  438. package/apps/sessrumnir/src/main/session-lineage-reader.ts +111 -0
  439. package/apps/sessrumnir/src/main/session-metadata.test.ts +508 -0
  440. package/apps/sessrumnir/src/main/session-metadata.ts +526 -0
  441. package/apps/sessrumnir/src/main/session-name.test.ts +71 -0
  442. package/apps/sessrumnir/src/main/session-name.ts +69 -0
  443. package/apps/sessrumnir/src/main/session-paths.test.ts +110 -0
  444. package/apps/sessrumnir/src/main/session-paths.ts +88 -0
  445. package/apps/sessrumnir/src/main/session-tags.test.ts +61 -0
  446. package/apps/sessrumnir/src/main/session-tags.ts +253 -0
  447. package/apps/sessrumnir/src/main/session-trash.test.ts +66 -0
  448. package/apps/sessrumnir/src/main/session-trash.ts +65 -0
  449. package/apps/sessrumnir/src/main/settings-logic.ts +66 -0
  450. package/apps/sessrumnir/src/main/skills-discovery.test.ts +149 -0
  451. package/apps/sessrumnir/src/main/skills-discovery.ts +179 -0
  452. package/apps/sessrumnir/src/main/startup-launch.ts +63 -0
  453. package/apps/sessrumnir/src/main/terminal-service.ts +96 -0
  454. package/apps/sessrumnir/src/main/theme-store.test.ts +440 -0
  455. package/apps/sessrumnir/src/main/theme-store.ts +495 -0
  456. package/apps/sessrumnir/src/main/tray-decision.test.ts +46 -0
  457. package/apps/sessrumnir/src/main/tray-decision.ts +49 -0
  458. package/apps/sessrumnir/src/main/tray-manager.ts +220 -0
  459. package/apps/sessrumnir/src/main/tsconfig.json +20 -0
  460. package/apps/sessrumnir/src/main/window-background.test.ts +29 -0
  461. package/apps/sessrumnir/src/main/window-background.ts +16 -0
  462. package/apps/sessrumnir/src/main/workflow-discovery.test.ts +252 -0
  463. package/apps/sessrumnir/src/main/workflow-monitor.test.ts +371 -0
  464. package/apps/sessrumnir/src/main/workflow-monitor.ts +1020 -0
  465. package/apps/sessrumnir/src/main/workspace-activity.test.ts +227 -0
  466. package/apps/sessrumnir/src/main/workspace-activity.ts +215 -0
  467. package/apps/sessrumnir/src/main/workspace-manager.test.ts +680 -0
  468. package/apps/sessrumnir/src/main/workspace-manager.ts +1199 -0
  469. package/apps/sessrumnir/src/main/workspace-trust.test.ts +78 -0
  470. package/apps/sessrumnir/src/main/workspace-trust.ts +84 -0
  471. package/apps/sessrumnir/src/preload/index.ts +358 -0
  472. package/apps/sessrumnir/src/preload/tsconfig.json +20 -0
  473. package/apps/sessrumnir/src/renderer/index.html +18 -0
  474. package/apps/sessrumnir/src/renderer/public/theme-boot.js +30 -0
  475. package/apps/sessrumnir/src/renderer/src/app.tsx +194 -0
  476. package/apps/sessrumnir/src/renderer/src/assets/openmoji-color.woff2 +0 -0
  477. package/apps/sessrumnir/src/renderer/src/assets/pi-logo.svg +7 -0
  478. package/apps/sessrumnir/src/renderer/src/assets/ymir-mark.svg +55 -0
  479. package/apps/sessrumnir/src/renderer/src/assets.d.ts +7 -0
  480. package/apps/sessrumnir/src/renderer/src/components/chat-code-highlight.ts +239 -0
  481. package/apps/sessrumnir/src/renderer/src/components/chat-file-link.ts +140 -0
  482. package/apps/sessrumnir/src/renderer/src/components/chat-input.tsx +729 -0
  483. package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.test.ts +175 -0
  484. package/apps/sessrumnir/src/renderer/src/components/chat-panel-widths.ts +100 -0
  485. package/apps/sessrumnir/src/renderer/src/components/chat-panel.tsx +523 -0
  486. package/apps/sessrumnir/src/renderer/src/components/chat-project-picker.tsx +193 -0
  487. package/apps/sessrumnir/src/renderer/src/components/chat-search.tsx +251 -0
  488. package/apps/sessrumnir/src/renderer/src/components/code-editor-highlight.ts +85 -0
  489. package/apps/sessrumnir/src/renderer/src/components/code-editor-language.test.ts +19 -0
  490. package/apps/sessrumnir/src/renderer/src/components/code-editor-language.ts +88 -0
  491. package/apps/sessrumnir/src/renderer/src/components/code-editor.tsx +128 -0
  492. package/apps/sessrumnir/src/renderer/src/components/command-palette.tsx +315 -0
  493. package/apps/sessrumnir/src/renderer/src/components/command-results.tsx +80 -0
  494. package/apps/sessrumnir/src/renderer/src/components/composer-permission-menu.tsx +91 -0
  495. package/apps/sessrumnir/src/renderer/src/components/context-menu.tsx +457 -0
  496. package/apps/sessrumnir/src/renderer/src/components/copy-button.tsx +36 -0
  497. package/apps/sessrumnir/src/renderer/src/components/council-panels.tsx +193 -0
  498. package/apps/sessrumnir/src/renderer/src/components/custom-models-editor.tsx +330 -0
  499. package/apps/sessrumnir/src/renderer/src/components/diagnostics-panel.tsx +380 -0
  500. package/apps/sessrumnir/src/renderer/src/components/diff-viewer.tsx +318 -0
  501. package/apps/sessrumnir/src/renderer/src/components/ember-background.tsx +113 -0
  502. package/apps/sessrumnir/src/renderer/src/components/error-boundary.tsx +38 -0
  503. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.test.ts +43 -0
  504. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog-helpers.ts +51 -0
  505. package/apps/sessrumnir/src/renderer/src/components/extension-ui-dialog.tsx +486 -0
  506. package/apps/sessrumnir/src/renderer/src/components/file-tree.tsx +784 -0
  507. package/apps/sessrumnir/src/renderer/src/components/git-conveyor-actions.tsx +246 -0
  508. package/apps/sessrumnir/src/renderer/src/components/home-screen.tsx +396 -0
  509. package/apps/sessrumnir/src/renderer/src/components/image-viewer.tsx +100 -0
  510. package/apps/sessrumnir/src/renderer/src/components/line-numbered-code.tsx +58 -0
  511. package/apps/sessrumnir/src/renderer/src/components/markdown-renderer.tsx +237 -0
  512. package/apps/sessrumnir/src/renderer/src/components/message-bubble.tsx +1006 -0
  513. package/apps/sessrumnir/src/renderer/src/components/mission-control.tsx +341 -0
  514. package/apps/sessrumnir/src/renderer/src/components/model-selector.tsx +219 -0
  515. package/apps/sessrumnir/src/renderer/src/components/note-picker.tsx +155 -0
  516. package/apps/sessrumnir/src/renderer/src/components/notes-panel.tsx +437 -0
  517. package/apps/sessrumnir/src/renderer/src/components/package-browser.tsx +604 -0
  518. package/apps/sessrumnir/src/renderer/src/components/permission-mode.test.ts +22 -0
  519. package/apps/sessrumnir/src/renderer/src/components/permission-mode.ts +49 -0
  520. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.test.ts +44 -0
  521. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor-helpers.ts +35 -0
  522. package/apps/sessrumnir/src/renderer/src/components/permission-rules-editor.tsx +226 -0
  523. package/apps/sessrumnir/src/renderer/src/components/permission-selector.tsx +104 -0
  524. package/apps/sessrumnir/src/renderer/src/components/resize-handle.tsx +51 -0
  525. package/apps/sessrumnir/src/renderer/src/components/review-rail.tsx +194 -0
  526. package/apps/sessrumnir/src/renderer/src/components/session-menu-position.test.ts +22 -0
  527. package/apps/sessrumnir/src/renderer/src/components/session-menu-position.ts +34 -0
  528. package/apps/sessrumnir/src/renderer/src/components/session-panel.tsx +711 -0
  529. package/apps/sessrumnir/src/renderer/src/components/session-runtime-indicator.tsx +43 -0
  530. package/apps/sessrumnir/src/renderer/src/components/settings-panel.tsx +1360 -0
  531. package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.test.ts +47 -0
  532. package/apps/sessrumnir/src/renderer/src/components/sidebar-activity.ts +76 -0
  533. package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.test.ts +73 -0
  534. package/apps/sessrumnir/src/renderer/src/components/sidebar-session-labels.ts +47 -0
  535. package/apps/sessrumnir/src/renderer/src/components/sidebar.tsx +1045 -0
  536. package/apps/sessrumnir/src/renderer/src/components/skills-panel.tsx +159 -0
  537. package/apps/sessrumnir/src/renderer/src/components/stats-panel.tsx +368 -0
  538. package/apps/sessrumnir/src/renderer/src/components/status-bar.tsx +274 -0
  539. package/apps/sessrumnir/src/renderer/src/components/status-popover.tsx +481 -0
  540. package/apps/sessrumnir/src/renderer/src/components/streaming-bubble.tsx +128 -0
  541. package/apps/sessrumnir/src/renderer/src/components/subagent-progress.tsx +254 -0
  542. package/apps/sessrumnir/src/renderer/src/components/task-launcher.tsx +168 -0
  543. package/apps/sessrumnir/src/renderer/src/components/terminal.tsx +203 -0
  544. package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.test.ts +35 -0
  545. package/apps/sessrumnir/src/renderer/src/components/theme-editor-helpers.ts +35 -0
  546. package/apps/sessrumnir/src/renderer/src/components/theme-editor.tsx +440 -0
  547. package/apps/sessrumnir/src/renderer/src/components/theme-gallery.tsx +373 -0
  548. package/apps/sessrumnir/src/renderer/src/components/thinking-level-selector.tsx +85 -0
  549. package/apps/sessrumnir/src/renderer/src/components/timeline.tsx +289 -0
  550. package/apps/sessrumnir/src/renderer/src/components/tool-call-icon.ts +32 -0
  551. package/apps/sessrumnir/src/renderer/src/components/workflow-navigator.tsx +901 -0
  552. package/apps/sessrumnir/src/renderer/src/components/workspace-tabs.tsx +241 -0
  553. package/apps/sessrumnir/src/renderer/src/global.d.ts +8 -0
  554. package/apps/sessrumnir/src/renderer/src/hooks/use-folder-drop.ts +101 -0
  555. package/apps/sessrumnir/src/renderer/src/hooks.test.ts +88 -0
  556. package/apps/sessrumnir/src/renderer/src/hooks.ts +574 -0
  557. package/apps/sessrumnir/src/renderer/src/i18n.test.ts +54 -0
  558. package/apps/sessrumnir/src/renderer/src/i18n.ts +89 -0
  559. package/apps/sessrumnir/src/renderer/src/index.css +425 -0
  560. package/apps/sessrumnir/src/renderer/src/main.tsx +42 -0
  561. package/apps/sessrumnir/src/renderer/src/message-grouping.test.ts +422 -0
  562. package/apps/sessrumnir/src/renderer/src/message-grouping.ts +410 -0
  563. package/apps/sessrumnir/src/renderer/src/message-parsing.test.ts +86 -0
  564. package/apps/sessrumnir/src/renderer/src/message-parsing.ts +166 -0
  565. package/apps/sessrumnir/src/renderer/src/store-external-prompts.test.ts +161 -0
  566. package/apps/sessrumnir/src/renderer/src/store-session-transitions.test.ts +2090 -0
  567. package/apps/sessrumnir/src/renderer/src/store-timeline.test.ts +52 -0
  568. package/apps/sessrumnir/src/renderer/src/store-turn-error.test.ts +136 -0
  569. package/apps/sessrumnir/src/renderer/src/store.ts +3726 -0
  570. package/apps/sessrumnir/src/renderer/src/subagent-detection.test.ts +55 -0
  571. package/apps/sessrumnir/src/renderer/src/theme/engine.test.ts +29 -0
  572. package/apps/sessrumnir/src/renderer/src/theme/engine.ts +20 -0
  573. package/apps/sessrumnir/src/renderer/src/themes/breeze-claudius.json +69 -0
  574. package/apps/sessrumnir/src/renderer/src/themes/breeze-dark.json +69 -0
  575. package/apps/sessrumnir/src/renderer/src/themes/breeze-light.json +69 -0
  576. package/apps/sessrumnir/src/renderer/src/themes/builtin-themes.test.ts +95 -0
  577. package/apps/sessrumnir/src/renderer/src/themes/dark.json +46 -0
  578. package/apps/sessrumnir/src/renderer/src/themes/fensalir.json +82 -0
  579. package/apps/sessrumnir/src/renderer/src/themes/gruvbox.json +69 -0
  580. package/apps/sessrumnir/src/renderer/src/themes/index.ts +60 -0
  581. package/apps/sessrumnir/src/renderer/src/themes/light.json +46 -0
  582. package/apps/sessrumnir/src/renderer/src/themes/nord.json +69 -0
  583. package/apps/sessrumnir/src/renderer/src/themes/sessrumnir.json +82 -0
  584. package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.test.ts +81 -0
  585. package/apps/sessrumnir/src/renderer/src/utils/debounced-buffer.ts +57 -0
  586. package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.test.ts +62 -0
  587. package/apps/sessrumnir/src/renderer/src/utils/format-relative-time.ts +28 -0
  588. package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.test.ts +57 -0
  589. package/apps/sessrumnir/src/renderer/src/utils/heatmap-grid.ts +41 -0
  590. package/apps/sessrumnir/src/renderer/src/utils/ipc-error.test.ts +22 -0
  591. package/apps/sessrumnir/src/renderer/src/utils/ipc-error.ts +12 -0
  592. package/apps/sessrumnir/src/renderer/src/utils/model-search.test.ts +63 -0
  593. package/apps/sessrumnir/src/renderer/src/utils/model-search.ts +20 -0
  594. package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.test.ts +31 -0
  595. package/apps/sessrumnir/src/renderer/src/utils/planning-prompt.ts +27 -0
  596. package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.test.ts +15 -0
  597. package/apps/sessrumnir/src/renderer/src/utils/preview-load-error.ts +22 -0
  598. package/apps/sessrumnir/src/renderer/src/utils/process-status-label.test.ts +13 -0
  599. package/apps/sessrumnir/src/renderer/src/utils/process-status-label.ts +19 -0
  600. package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.test.ts +68 -0
  601. package/apps/sessrumnir/src/renderer/src/utils/quick-switcher.ts +52 -0
  602. package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.test.ts +46 -0
  603. package/apps/sessrumnir/src/renderer/src/utils/rank-file-results.ts +23 -0
  604. package/apps/sessrumnir/src/renderer/src/utils/relative-time.tsx +25 -0
  605. package/apps/sessrumnir/src/renderer/src/utils/session-title.test.ts +98 -0
  606. package/apps/sessrumnir/src/renderer/src/utils/session-title.ts +58 -0
  607. package/apps/sessrumnir/src/renderer/src/utils/stale-guard.test.ts +29 -0
  608. package/apps/sessrumnir/src/renderer/src/utils/stale-guard.ts +20 -0
  609. package/apps/sessrumnir/src/renderer/src/utils/theme.test.ts +174 -0
  610. package/apps/sessrumnir/src/renderer/src/utils/theme.ts +159 -0
  611. package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.test.ts +130 -0
  612. package/apps/sessrumnir/src/renderer/src/utils/workflow-runs.ts +84 -0
  613. package/apps/sessrumnir/src/renderer/tsconfig.json +24 -0
  614. package/apps/sessrumnir/src/shared/agent-engine-label.test.ts +45 -0
  615. package/apps/sessrumnir/src/shared/agent-engine-label.ts +53 -0
  616. package/apps/sessrumnir/src/shared/app-settings.test.ts +25 -0
  617. package/apps/sessrumnir/src/shared/app-settings.ts +23 -0
  618. package/apps/sessrumnir/src/shared/bridge-http.ts +341 -0
  619. package/apps/sessrumnir/src/shared/bridge.ts +337 -0
  620. package/apps/sessrumnir/src/shared/council-config.test.ts +300 -0
  621. package/apps/sessrumnir/src/shared/council-config.ts +356 -0
  622. package/apps/sessrumnir/src/shared/default-settings.ts +39 -0
  623. package/apps/sessrumnir/src/shared/folder-drop.test.ts +118 -0
  624. package/apps/sessrumnir/src/shared/folder-drop.ts +74 -0
  625. package/apps/sessrumnir/src/shared/fork-point.test.ts +30 -0
  626. package/apps/sessrumnir/src/shared/fork-point.ts +24 -0
  627. package/apps/sessrumnir/src/shared/i18n/i18next.d.ts +10 -0
  628. package/apps/sessrumnir/src/shared/i18n/index.test.ts +51 -0
  629. package/apps/sessrumnir/src/shared/i18n/index.ts +50 -0
  630. package/apps/sessrumnir/src/shared/i18n/languages.ts +12 -0
  631. package/apps/sessrumnir/src/shared/i18n/locale-checks.ts +98 -0
  632. package/apps/sessrumnir/src/shared/i18n/locales.test.ts +92 -0
  633. package/apps/sessrumnir/src/shared/i18n/pseudo.test.ts +39 -0
  634. package/apps/sessrumnir/src/shared/i18n/pseudo.ts +44 -0
  635. package/apps/sessrumnir/src/shared/i18n/resolve.test.ts +43 -0
  636. package/apps/sessrumnir/src/shared/i18n/resolve.ts +41 -0
  637. package/apps/sessrumnir/src/shared/i18n/resources.ts +11 -0
  638. package/apps/sessrumnir/src/shared/ipc-contracts.ts +1539 -0
  639. package/apps/sessrumnir/src/shared/models-config.test.ts +124 -0
  640. package/apps/sessrumnir/src/shared/models-config.ts +167 -0
  641. package/apps/sessrumnir/src/shared/package-filter.ts +19 -0
  642. package/apps/sessrumnir/src/shared/package-spec.test.ts +51 -0
  643. package/apps/sessrumnir/src/shared/package-spec.ts +25 -0
  644. package/apps/sessrumnir/src/shared/path-compare.test.ts +38 -0
  645. package/apps/sessrumnir/src/shared/path-compare.ts +76 -0
  646. package/apps/sessrumnir/src/shared/permission-mode.ts +8 -0
  647. package/apps/sessrumnir/src/shared/pi-command.test.ts +165 -0
  648. package/apps/sessrumnir/src/shared/pi-command.ts +139 -0
  649. package/apps/sessrumnir/src/shared/product-name.ts +7 -0
  650. package/apps/sessrumnir/src/shared/session-lineage.test.ts +103 -0
  651. package/apps/sessrumnir/src/shared/session-lineage.ts +50 -0
  652. package/apps/sessrumnir/src/shared/session-preview.test.ts +93 -0
  653. package/apps/sessrumnir/src/shared/session-preview.ts +56 -0
  654. package/apps/sessrumnir/src/shared/sidebar-width.test.ts +58 -0
  655. package/apps/sessrumnir/src/shared/sidebar-width.ts +37 -0
  656. package/apps/sessrumnir/src/shared/theme/builtin-ids.ts +16 -0
  657. package/apps/sessrumnir/src/shared/theme/resolve.test.ts +66 -0
  658. package/apps/sessrumnir/src/shared/theme/resolve.ts +29 -0
  659. package/apps/sessrumnir/src/shared/theme/syntax-defaults.ts +24 -0
  660. package/apps/sessrumnir/src/shared/theme/theme-file.test.ts +88 -0
  661. package/apps/sessrumnir/src/shared/theme/theme-file.ts +132 -0
  662. package/apps/sessrumnir/src/shared/theme/tokens.test.ts +35 -0
  663. package/apps/sessrumnir/src/shared/theme/tokens.ts +86 -0
  664. package/apps/sessrumnir/src/shared/tsconfig.json +17 -0
  665. package/apps/sessrumnir/src/shared/untrusted-data.test.ts +32 -0
  666. package/apps/sessrumnir/src/shared/untrusted-data.ts +22 -0
  667. package/apps/sessrumnir/src/shared/version-compare.test.ts +33 -0
  668. package/apps/sessrumnir/src/shared/version-compare.ts +50 -0
  669. package/apps/sessrumnir/src/shared/workflow-control.ts +33 -0
  670. package/apps/sessrumnir/src/web-server.ts +315 -0
  671. package/apps/sessrumnir/tsconfig.json +26 -0
  672. package/apps/smidja/smidja_build.py +45 -0
  673. package/apps/smidja/smidja_build_review.py +76 -0
  674. package/apps/smidja/smidja_build_test.py +79 -0
  675. package/apps/smidja/smidja_document.py +76 -0
  676. package/apps/smidja/smidja_modules/agent_cc.py +15 -0
  677. package/apps/smidja/smidja_modules/agent_opencode.py +194 -0
  678. package/apps/smidja/smidja_modules/agent_pi.py +392 -0
  679. package/apps/smidja/smidja_modules/agents.py +993 -0
  680. package/apps/smidja/smidja_modules/changes.py +103 -0
  681. package/apps/smidja/smidja_modules/console.py +131 -0
  682. package/apps/smidja/smidja_modules/data_types.py +447 -0
  683. package/apps/smidja/smidja_modules/gates.py +108 -0
  684. package/apps/smidja/smidja_modules/git_helper.py +120 -0
  685. package/apps/smidja/smidja_modules/lm_local.py +144 -0
  686. package/apps/smidja/smidja_modules/permissions.py +194 -0
  687. package/apps/smidja/smidja_modules/prompts.py +21 -0
  688. package/apps/smidja/smidja_modules/quality.py +240 -0
  689. package/apps/smidja/smidja_modules/runner.py +155 -0
  690. package/apps/smidja/smidja_modules/session.py +50 -0
  691. package/apps/smidja/smidja_modules/stall_watch.py +149 -0
  692. package/apps/smidja/smidja_modules/tracer.py +271 -0
  693. package/apps/smidja/smidja_modules/utils.py +77 -0
  694. package/apps/smidja/smidja_orchestrate.py +162 -0
  695. package/apps/smidja/smidja_plan.py +45 -0
  696. package/apps/smidja/smidja_plan_build.py +55 -0
  697. package/apps/smidja/smidja_plan_build_test.py +86 -0
  698. package/apps/smidja/smidja_plan_build_test_quality.py +98 -0
  699. package/apps/smidja/smidja_prompt.py +44 -0
  700. package/apps/smidja/smidja_quality.py +49 -0
  701. package/apps/smidja/smidja_recon_iv.py +202 -0
  702. package/apps/smidja/smidja_scout.py +45 -0
  703. package/apps/smidja/smidja_simple_sdlc.py +183 -0
  704. package/apps/smidja/smidja_smidja_config/smidja.config.yaml +143 -0
  705. package/apps/smidja-factory/AGENTS.md +165 -0
  706. package/apps/smidja-factory/README.md +3 -0
  707. package/apps/smidja-factory/SKILL.md +18 -0
  708. package/apps/smidja-factory/apps/visualizer/.oxlintrc.json +18 -0
  709. package/apps/smidja-factory/apps/visualizer/bun.lock +269 -0
  710. package/apps/smidja-factory/apps/visualizer/desktop/icon.png +0 -0
  711. package/apps/smidja-factory/apps/visualizer/desktop/main.js +140 -0
  712. package/apps/smidja-factory/apps/visualizer/desktop/package-lock.json +871 -0
  713. package/apps/smidja-factory/apps/visualizer/desktop/package.json +18 -0
  714. package/apps/smidja-factory/apps/visualizer/desktop/preload.js +19 -0
  715. package/apps/smidja-factory/apps/visualizer/index.html +28 -0
  716. package/apps/smidja-factory/apps/visualizer/package.json +31 -0
  717. package/apps/smidja-factory/apps/visualizer/public/icon.svg +7 -0
  718. package/apps/smidja-factory/apps/visualizer/public/models/claude.png +0 -0
  719. package/apps/smidja-factory/apps/visualizer/public/models/gemini.png +0 -0
  720. package/apps/smidja-factory/apps/visualizer/public/models/kimi.png +0 -0
  721. package/apps/smidja-factory/apps/visualizer/public/models/openai.png +0 -0
  722. package/apps/smidja-factory/apps/visualizer/public/models/zai.png +0 -0
  723. package/apps/smidja-factory/apps/visualizer/server/chat.ts +425 -0
  724. package/apps/smidja-factory/apps/visualizer/server/db.ts +1050 -0
  725. package/apps/smidja-factory/apps/visualizer/server/index.ts +648 -0
  726. package/apps/smidja-factory/apps/visualizer/server/model-catalog.ts +90 -0
  727. package/apps/smidja-factory/apps/visualizer/server/roster-api.ts +75 -0
  728. package/apps/smidja-factory/apps/visualizer/server/roster_resolve.py +170 -0
  729. package/apps/smidja-factory/apps/visualizer/server/settings.ts +106 -0
  730. package/apps/smidja-factory/apps/visualizer/shared/types.ts +630 -0
  731. package/apps/smidja-factory/apps/visualizer/src/App.vue +379 -0
  732. package/apps/smidja-factory/apps/visualizer/src/components/ChatInput.vue +155 -0
  733. package/apps/smidja-factory/apps/visualizer/src/components/ChatThread.vue +219 -0
  734. package/apps/smidja-factory/apps/visualizer/src/components/ChatView.vue +335 -0
  735. package/apps/smidja-factory/apps/visualizer/src/components/DecisionsView.vue +177 -0
  736. package/apps/smidja-factory/apps/visualizer/src/components/DetailSection.vue +76 -0
  737. package/apps/smidja-factory/apps/visualizer/src/components/MemoryView.vue +397 -0
  738. package/apps/smidja-factory/apps/visualizer/src/components/PhaseDetail.vue +1354 -0
  739. package/apps/smidja-factory/apps/visualizer/src/components/PhaseDots.vue +55 -0
  740. package/apps/smidja-factory/apps/visualizer/src/components/SessionCard.vue +583 -0
  741. package/apps/smidja-factory/apps/visualizer/src/components/SessionMiniCard.vue +58 -0
  742. package/apps/smidja-factory/apps/visualizer/src/components/SessionPanel.vue +272 -0
  743. package/apps/smidja-factory/apps/visualizer/src/components/SessionStartForm.vue +152 -0
  744. package/apps/smidja-factory/apps/visualizer/src/components/SessionTrace.vue +1157 -0
  745. package/apps/smidja-factory/apps/visualizer/src/components/SessionsList.vue +96 -0
  746. package/apps/smidja-factory/apps/visualizer/src/components/SettingsView.vue +152 -0
  747. package/apps/smidja-factory/apps/visualizer/src/components/StatChip.vue +98 -0
  748. package/apps/smidja-factory/apps/visualizer/src/components/StatsView.vue +382 -0
  749. package/apps/smidja-factory/apps/visualizer/src/components/StatusChip.vue +73 -0
  750. package/apps/smidja-factory/apps/visualizer/src/components/ToolCallCard.vue +156 -0
  751. package/apps/smidja-factory/apps/visualizer/src/lib/api.ts +177 -0
  752. package/apps/smidja-factory/apps/visualizer/src/lib/chat-api.ts +95 -0
  753. package/apps/smidja-factory/apps/visualizer/src/lib/chat-store.ts +351 -0
  754. package/apps/smidja-factory/apps/visualizer/src/lib/events.ts +131 -0
  755. package/apps/smidja-factory/apps/visualizer/src/lib/format.ts +92 -0
  756. package/apps/smidja-factory/apps/visualizer/src/lib/highlight.ts +54 -0
  757. package/apps/smidja-factory/apps/visualizer/src/lib/markdown.ts +120 -0
  758. package/apps/smidja-factory/apps/visualizer/src/lib/models.ts +42 -0
  759. package/apps/smidja-factory/apps/visualizer/src/lib/router.ts +67 -0
  760. package/apps/smidja-factory/apps/visualizer/src/lib/types.ts +61 -0
  761. package/apps/smidja-factory/apps/visualizer/src/main.ts +13 -0
  762. package/apps/smidja-factory/apps/visualizer/src/style.css +592 -0
  763. package/apps/smidja-factory/apps/visualizer/tsconfig.json +30 -0
  764. package/apps/smidja-factory/apps/visualizer/vite.config.js +32 -0
  765. package/apps/smidja-factory/apps/visualizer/vite.config.ts +31 -0
  766. package/apps/smidja-factory/cookbooks/create_config.md +61 -0
  767. package/apps/smidja-factory/cookbooks/create_smidja.md +120 -0
  768. package/apps/smidja-factory/cookbooks/how_to_prompt_for_the_eng.md +110 -0
  769. package/apps/smidja-factory/cookbooks/install.md +56 -0
  770. package/apps/smidja-factory/cookbooks/run_smidja.md +122 -0
  771. package/apps/smidja-factory/cookbooks/smidja_overview.md +88 -0
  772. package/apps/smidja-factory/cookbooks/update_config.md +103 -0
  773. package/apps/smidja-factory/cookbooks/update_modules.md +100 -0
  774. package/apps/smidja-factory/cookbooks/update_smidja.md +90 -0
  775. package/apps/smidja-factory/docs/enhancement-plan.md +420 -0
  776. package/apps/smidja-factory/references/config.md +202 -0
  777. package/apps/smidja-factory/references/handoff.md +161 -0
  778. package/apps/smidja-factory/references/observability.md +166 -0
  779. package/apps/smidja-factory/scripts/install.py +113 -0
  780. package/apps/smidja-factory/scripts/make_config.py +35 -0
  781. package/apps/smidja-factory/scripts/make_smidja.py +119 -0
  782. package/apps/smidja-factory/skills/add-or-edit-ai-models/SKILL.md +151 -0
  783. package/apps/smidja-factory/skills/create-new-agent/SKILL.md +125 -0
  784. package/apps/smidja-factory/skills/create-new-teams/SKILL.md +151 -0
  785. package/apps/smidja-factory/skills/git-ops/README.md +25 -0
  786. package/apps/smidja-factory/skills/git-ops/SKILL.md +18 -0
  787. package/apps/smidja-factory/skills/git-ops/create_branch.sh +79 -0
  788. package/apps/smidja-factory/skills/git-ops/safe_commit.sh +82 -0
  789. package/apps/smidja-factory/skills/git-ops/sync_upstream.sh +94 -0
  790. package/apps/smidja-factory/skills/how-to-run-agent-teams/SKILL.md +117 -0
  791. package/apps/smidja-factory/skills/smidja-instructions/SKILL.md +168 -0
  792. package/apps/smidja-factory/skills/smidja-launcher/SKILL.md +155 -0
  793. package/apps/smidja-factory/skills/smidja-start/SKILL.md +266 -0
  794. package/apps/smidja-factory/skills/volundr/SKILL.md +232 -0
  795. package/apps/smidja-factory/skills/volundr/prompt/dispatch-prompt.md +91 -0
  796. package/apps/smidja-factory/skills/volundr/prompt/volundr-system.md +63 -0
  797. package/apps/smidja-factory/skills/volundr/scripts/volundr-observe.py +52 -0
  798. package/apps/smidja-factory/skills/volundr/scripts/volundr-recall.py +54 -0
  799. package/apps/smidja-factory/skills/volundr/scripts/volundr-status.py +51 -0
  800. package/apps/smidja-factory/skills/volundr/scripts/volundr-teach.py +47 -0
  801. package/apps/smidja-factory/templates/env.sample +28 -0
  802. package/apps/smidja-factory/templates/harness_engineering/subagents.ts +706 -0
  803. package/apps/smidja-factory/templates/harness_engineering/themeMap.ts +145 -0
  804. package/apps/smidja-factory/templates/justfile +89 -0
  805. package/apps/smidja-factory/templates/prompt_engineering/builder/system.md +13 -0
  806. package/apps/smidja-factory/templates/prompt_engineering/builder/user.md +34 -0
  807. package/apps/smidja-factory/templates/prompt_engineering/documenter/system.md +17 -0
  808. package/apps/smidja-factory/templates/prompt_engineering/documenter/user.md +48 -0
  809. package/apps/smidja-factory/templates/prompt_engineering/orchestrator/system.md +55 -0
  810. package/apps/smidja-factory/templates/prompt_engineering/orchestrator/user.md +39 -0
  811. package/apps/smidja-factory/templates/prompt_engineering/planner/system.md +21 -0
  812. package/apps/smidja-factory/templates/prompt_engineering/planner/user.md +45 -0
  813. package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/system.md +51 -0
  814. package/apps/smidja-factory/templates/prompt_engineering/recon_orchestrator/user.md +26 -0
  815. package/apps/smidja-factory/templates/prompt_engineering/reviewer/system.md +16 -0
  816. package/apps/smidja-factory/templates/prompt_engineering/reviewer/user.md +44 -0
  817. package/apps/smidja-factory/templates/prompt_engineering/scout/system.md +32 -0
  818. package/apps/smidja-factory/templates/prompt_engineering/scout/user.md +34 -0
  819. package/apps/smidja-factory/templates/smidja/smidja_build.py +45 -0
  820. package/apps/smidja-factory/templates/smidja/smidja_build_review.py +76 -0
  821. package/apps/smidja-factory/templates/smidja/smidja_build_test.py +79 -0
  822. package/apps/smidja-factory/templates/smidja/smidja_document.py +76 -0
  823. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_cc.py +15 -0
  824. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_opencode.py +194 -0
  825. package/apps/smidja-factory/templates/smidja/smidja_modules/agent_pi.py +392 -0
  826. package/apps/smidja-factory/templates/smidja/smidja_modules/agents.py +993 -0
  827. package/apps/smidja-factory/templates/smidja/smidja_modules/changes.py +103 -0
  828. package/apps/smidja-factory/templates/smidja/smidja_modules/console.py +131 -0
  829. package/apps/smidja-factory/templates/smidja/smidja_modules/data_types.py +447 -0
  830. package/apps/smidja-factory/templates/smidja/smidja_modules/gates.py +108 -0
  831. package/apps/smidja-factory/templates/smidja/smidja_modules/git_helper.py +120 -0
  832. package/apps/smidja-factory/templates/smidja/smidja_modules/lm_local.py +144 -0
  833. package/apps/smidja-factory/templates/smidja/smidja_modules/permissions.py +194 -0
  834. package/apps/smidja-factory/templates/smidja/smidja_modules/prompts.py +21 -0
  835. package/apps/smidja-factory/templates/smidja/smidja_modules/quality.py +240 -0
  836. package/apps/smidja-factory/templates/smidja/smidja_modules/runner.py +155 -0
  837. package/apps/smidja-factory/templates/smidja/smidja_modules/session.py +50 -0
  838. package/apps/smidja-factory/templates/smidja/smidja_modules/stall_watch.py +149 -0
  839. package/apps/smidja-factory/templates/smidja/smidja_modules/tracer.py +271 -0
  840. package/apps/smidja-factory/templates/smidja/smidja_modules/utils.py +77 -0
  841. package/apps/smidja-factory/templates/smidja/smidja_orchestrate.py +162 -0
  842. package/apps/smidja-factory/templates/smidja/smidja_plan.py +45 -0
  843. package/apps/smidja-factory/templates/smidja/smidja_plan_build.py +55 -0
  844. package/apps/smidja-factory/templates/smidja/smidja_plan_build_test.py +86 -0
  845. package/apps/smidja-factory/templates/smidja/smidja_plan_build_test_quality.py +98 -0
  846. package/apps/smidja-factory/templates/smidja/smidja_prompt.py +44 -0
  847. package/apps/smidja-factory/templates/smidja/smidja_quality.py +49 -0
  848. package/apps/smidja-factory/templates/smidja/smidja_recon_iv.py +202 -0
  849. package/apps/smidja-factory/templates/smidja/smidja_scout.py +45 -0
  850. package/apps/smidja-factory/templates/smidja/smidja_simple_sdlc.py +183 -0
  851. package/apps/smidja-factory/templates/smidja.config.yaml +143 -0
  852. package/bin/app-build.sh +36 -0
  853. package/bin/electron-lib.sh +25 -0
  854. package/bin/sessrumnir.sh +1 -1
  855. package/bin/ymir-install.sh +7 -0
  856. package/install.sh +88 -0
  857. package/package.json +16 -7
  858. package/scripts/electron.sh +6 -2
  859. package/.agents/bus/messages.json +0 -1
  860. package/.agents/config/agents.machine.example.yaml +0 -16
  861. package/.pi/mcp.json +0 -16
  862. package/assets/reference/state/.branch-eligible-owner +0 -4
  863. package/assets/reference/state/.branch-mirror-cursor +0 -1
  864. package/assets/reference/state/.branch-outcomes-cursor +0 -1
  865. package/assets/reference/state/.branch-session +0 -1
  866. package/assets/reference/state/.count-default_wF_p5 +0 -1
  867. package/assets/reference/state/.count-default_wF_p6 +0 -1
  868. package/assets/reference/state/.count-default_wR_p2 +0 -1
  869. package/assets/reference/state/.count-default_wS_p2 +0 -1
  870. package/assets/reference/state/.count-firstmate_fm-server-knowledge +0 -1
  871. package/assets/reference/state/.hash-default_wF_p5 +0 -1
  872. package/assets/reference/state/.hash-default_wF_p6 +0 -1
  873. package/assets/reference/state/.hash-default_wR_p2 +0 -1
  874. package/assets/reference/state/.hash-default_wS_p2 +0 -1
  875. package/assets/reference/state/.hash-firstmate_fm-server-knowledge +0 -1
  876. package/assets/reference/state/.heartbeat-streak +0 -1
  877. package/assets/reference/state/.inactive-outcome-reconcile +0 -2
  878. package/assets/reference/state/.lock +0 -1
  879. package/assets/reference/state/.pi-branch-extension-loaded +0 -1
  880. package/assets/reference/state/.pi-turnend-extension-loaded +0 -2
  881. package/assets/reference/state/.pi-watch-extension-loaded +0 -2
  882. package/assets/reference/state/.seen-server-knowledge_turn-ended +0 -1
  883. package/assets/reference/state/.session-start-agents-baseline +0 -2
  884. package/assets/reference/state/.session-start-complete +0 -1
  885. package/assets/reference/state/.stale-default_wF_p5 +0 -1
  886. package/assets/reference/state/.stale-default_wR_p2 +0 -1
  887. package/assets/reference/state/.stale-firstmate_fm-server-knowledge +0 -1
  888. package/assets/reference/state/.stale-since-default_wF_p5 +0 -1
  889. package/assets/reference/state/.stale-since-default_wR_p2 +0 -1
  890. package/assets/reference/state/.stale-since-firstmate_fm-server-knowledge +0 -1
  891. package/assets/reference/state/.startup-network.delivered +0 -1
  892. package/assets/reference/state/.startup-network.report +0 -1
  893. package/assets/reference/state/.startup-network.status +0 -10
  894. package/assets/reference/state/.startup-network.timings +0 -6
  895. package/assets/reference/state/.trace-context-effective +0 -1
  896. package/assets/reference/state/.wake-queue +0 -0
  897. package/assets/reference/state/.wake-queue.seq +0 -1
  898. package/assets/reference/state/.wake-rows.consume.BcqXSq +0 -0
  899. package/assets/reference/state/.wake-rows.consume.NDZZei +0 -0
  900. package/assets/reference/state/.wake-rows.consume.VGkhYm +0 -0
  901. package/assets/reference/state/.wake-rows.consume.tR2rTy +0 -0
  902. package/assets/reference/state/.watch-arm-output.3581p6 +0 -0
  903. package/assets/reference/state/.watch-arm-output.3ciDNP +0 -0
  904. package/assets/reference/state/.watch-arm-output.SksR6k +0 -0
  905. package/assets/reference/state/.watch-cycle-exits.log +0 -22
  906. package/assets/reference/state/.watch-deliveries.log +0 -19
  907. package/assets/reference/state/.watch-triage.log +0 -38
  908. package/assets/reference/state/.watch.lock/fm-home +0 -1
  909. package/assets/reference/state/.watch.lock/pid +0 -1
  910. package/assets/reference/state/.watch.lock/pid-identity +0 -1
  911. package/assets/reference/state/.watch.lock/watcher-path +0 -1
  912. package/assets/reference/state/.watch.lock.owner.qqbxmS/fm-home +0 -1
  913. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid +0 -1
  914. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid-identity +0 -1
  915. package/assets/reference/state/.watch.lock.owner.qqbxmS/watcher-path +0 -1
  916. package/assets/reference/state/.watcher-down +0 -1
  917. package/assets/reference/state/branch-outcomes.jsonl +0 -1
  918. package/assets/reference/state/branch-session/2026-09-04T16-49-38-671Z_01a06d53-6b6f-7619-930d-e6dfd9a4a29e.jsonl +0 -211
  919. package/assets/reference/state/home-summary.json +0 -42
  920. package/assets/reference/state/server-knowledge.busy-gen +0 -1
  921. package/assets/reference/state/server-knowledge.busy-state +0 -1
  922. package/assets/reference/state/server-knowledge.herdr-presentation +0 -12
  923. package/assets/reference/state/server-knowledge.meta +0 -18
  924. package/assets/reference/state/server-knowledge.pi-ext.ts +0 -26
  925. package/assets/reference/state/server-knowledge.turn-ended +0 -0
  926. /package/{assets/reference/state/.last-check → apps/hlidskjalf/src/app/.gitkeep} +0 -0
  927. /package/{assets/reference/state/.last-heartbeat → apps/hlidskjalf/src/components/.gitkeep} +0 -0
  928. /package/{assets/reference/state/.last-watcher-beat → apps/hlidskjalf/src/services/.gitkeep} +0 -0
  929. /package/{assets/reference/state/.main-eligible-rows.tmp.p2HMKo → apps/smidja/smidja_modules/__init__.py} +0 -0
  930. /package/{assets/reference/state/.main-eligible-rows.tmp.Exxm82 → apps/smidja-factory/templates/smidja/smidja_modules/__init__.py} +0 -0
@@ -0,0 +1,2045 @@
1
+ /**
2
+ * Hlidskjalf gate API — a read-only view of the Brokk runtime (plan 29).
3
+ *
4
+ * The runtime is file-based: state/, config/, workspace/memory/, .agents/. This
5
+ * server reads those artifacts and the runtime status scripts, and serves the
6
+ * shapes the SPA already declares in src/services/api.ts. It is read-only; no
7
+ * endpoint mutates the runtime.
8
+ *
9
+ * bun run apps/hlidskjalf/server/index.ts # API on :3889
10
+ * PORT=3889 bun run server/index.ts
11
+ */
12
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
13
+ import { Database } from 'bun:sqlite';
14
+ import { dirname, extname, join, resolve } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import {
17
+ findAccount,
18
+ invitesOpen,
19
+ listAccounts,
20
+ listInvites,
21
+ register,
22
+ verify as verifyAccount,
23
+ } from './accounts.ts';
24
+
25
+ const PORT = Number(process.env.PORT ?? 3889);
26
+ const HERE = import.meta.dir;
27
+ const ROOT = resolve(HERE, '../../..'); // the checkout this app is served from
28
+ const AGENTS_DIR = join(ROOT, '.agents/agents');
29
+ const SUBAGENTS_DIR = join(ROOT, '.agents/subagents');
30
+ const AGENTS_ALT = existsSync(AGENTS_DIR) ? AGENTS_DIR : SUBAGENTS_DIR;
31
+ const CONFIG_DIR = join(ROOT, '.agents/config');
32
+ const STATE_DIR = join(ROOT, 'state');
33
+ // The ledger lives in the HOARD, never in the checkout (Rule 04 / migration
34
+ // 0004): $YMIR_HOARD, else $YMIR_HOME/hodd, else ~/Documents/Ymir/hodd.
35
+ /** $YMIR_HOME — the hoard's home: where a realm's real tree lives. */
36
+ const HOME_DIR = process.env.YMIR_HOME ?? join(homedir(), 'Documents', 'Ymir');
37
+ const HOARD = process.env.YMIR_HOARD ?? join(process.env.YMIR_HOME ?? join(homedir(), 'Documents', 'Ymir'), 'hodd');
38
+ const RUNES = join(HOARD, 'memory/runes_audit.md');
39
+ const WELL = join(ROOT, '.agents/memory/well/episodes.jsonl');
40
+ const MASTERPLAN = join(ROOT, 'docs/masterplan.md');
41
+
42
+ const json = (data: unknown, status = 200) =>
43
+ new Response(JSON.stringify(data), {
44
+ status,
45
+ headers: { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' },
46
+ });
47
+
48
+ function read(path: string): string {
49
+ try {
50
+ return readFileSync(path, 'utf8');
51
+ } catch {
52
+ return '';
53
+ }
54
+ }
55
+
56
+ function run(cmd: string[]): string {
57
+ try {
58
+ // Bound every runtime probe so a stuck script can never hang a request.
59
+ const p = Bun.spawnSync(['timeout', '12', ...cmd], {
60
+ cwd: ROOT,
61
+ stdout: 'pipe',
62
+ stderr: 'pipe',
63
+ });
64
+ return new TextDecoder().decode(p.stdout ?? new Uint8Array()).trim();
65
+ } catch {
66
+ return '';
67
+ }
68
+ }
69
+
70
+ /* ---- caches: a request must never re-scan a growing file or re-spawn ------- *
71
+ * The gate reads append-only ledgers and probes live runtime scripts on every
72
+ * request. Both grow without bound, so an uncached read is O(file) per request.
73
+ * These two helpers memoise the reads and move the probes off the event loop. */
74
+ const MEMO = new Map<string, { t: number; v: unknown }>();
75
+ const INFLIGHT = new Map<string, Promise<unknown>>();
76
+
77
+ /** Serve a repeated pure read from memory for `ttlMs`. */
78
+ function memo<T>(key: string, ttlMs: number, fn: () => T): T {
79
+ const hit = MEMO.get(key);
80
+ const now = Date.now();
81
+ if (hit && now - hit.t < ttlMs) return hit.v as T;
82
+ const v = fn();
83
+ MEMO.set(key, { t: now, v });
84
+ return v;
85
+ }
86
+
87
+ /** Async memo: concurrent callers share one in-flight promise, never a throng. */
88
+ function memoAsync<T>(key: string, ttlMs: number, fn: () => Promise<T>): Promise<T> {
89
+ const hit = MEMO.get(key);
90
+ const now = Date.now();
91
+ if (hit && now - hit.t < ttlMs) return Promise.resolve(hit.v as T);
92
+ const running = INFLIGHT.get(key);
93
+ if (running) return running as Promise<T>;
94
+ const p = fn()
95
+ .then((v) => {
96
+ MEMO.set(key, { t: Date.now(), v });
97
+ INFLIGHT.delete(key);
98
+ return v;
99
+ })
100
+ .catch((e) => {
101
+ INFLIGHT.delete(key);
102
+ throw e;
103
+ });
104
+ INFLIGHT.set(key, p);
105
+ return p;
106
+ }
107
+
108
+ /** Read-only subprocess probe, off the event loop. `timeout` still bounds a
109
+ * stuck script, but the loop is never blocked waiting on it, and a brief cache
110
+ * means concurrent polls share one spawn. Mutating actions keep `run()`. */
111
+ async function runAsync(cmd: string[], ttlMs = 3000): Promise<string> {
112
+ return memoAsync('run:' + cmd.join('\u0000'), ttlMs, async () => {
113
+ try {
114
+ const proc = Bun.spawn(['timeout', '12', ...cmd], {
115
+ cwd: ROOT,
116
+ stdout: 'pipe',
117
+ stderr: 'pipe',
118
+ });
119
+ const out = await new Response(proc.stdout).text();
120
+ await proc.exited;
121
+ return out.trim();
122
+ } catch {
123
+ return '';
124
+ }
125
+ });
126
+ }
127
+
128
+ /* ---- Yggdrasil worktrees ------------------------------------------------- */
129
+ async function worktrees(): Promise<Array<{ id: string; branch: string; path: string; head: string; agent: string }>> {
130
+ const out = await runAsync(['git', 'worktree', 'list', '--porcelain']);
131
+ const rows: Array<{ id: string; branch: string; path: string; head: string; agent: string }> = [];
132
+ let cur: { path?: string; head?: string; branch?: string } = {};
133
+ const push = () => {
134
+ if (!cur.path) return;
135
+ const p = cur.path;
136
+ const id = p.includes('/.yggdrasil/') ? p.split('/.yggdrasil/')[1] : '(main)';
137
+ rows.push({ id, branch: cur.branch ?? '-', path: p, head: (cur.head ?? '').slice(0, 8), agent: id === '(main)' ? '-' : id });
138
+ };
139
+ for (const line of out.split('\n')) {
140
+ if (line.startsWith('worktree ')) { push(); cur = { path: line.slice(9).trim() }; }
141
+ else if (line.startsWith('HEAD ')) cur.head = line.slice(5).trim();
142
+ else if (line.startsWith('branch ')) cur.branch = line.slice(7).trim().replace('refs/heads/', '');
143
+ else if (line.trim() === 'detached') cur.branch = 'detached';
144
+ }
145
+ push();
146
+ return rows;
147
+ }
148
+
149
+ /* ---- frontmatter (YAML subset) ------------------------------------------- */
150
+ function frontmatter(text: string): Record<string, unknown> {
151
+ const m = text.match(/^---\n([\s\S]*?)\n---/);
152
+ if (!m) return {};
153
+ const out: Record<string, unknown> = {};
154
+ let listKey: string | null = null;
155
+ for (const line of m[1].split('\n')) {
156
+ if (/^\s*-\s+/.test(line) && listKey) {
157
+ (out[listKey] as unknown[]).push(line.replace(/^\s*-\s+/, '').replace(/^["']|["']$/g, ''));
158
+ continue;
159
+ }
160
+ const kv = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
161
+ if (!kv) continue;
162
+ const [, k, v] = kv;
163
+ if (v === '') {
164
+ out[k] = [];
165
+ listKey = k;
166
+ } else {
167
+ out[k] = v.replace(/^["']|["']$/g, '');
168
+ listKey = null;
169
+ }
170
+ }
171
+ return out;
172
+ }
173
+
174
+ /* ---- /api/agents --------------------------------------------------------- */
175
+ function agents() {
176
+ const model = read(join(CONFIG_DIR, 'eindri-harness')).trim() || 'opencode-go/deepseek-v4-flash';
177
+ const files = existsSync(AGENTS_ALT)
178
+ ? readdirSync(AGENTS_ALT).filter((f) => f.endsWith('.md') && f !== 'galdr.md')
179
+ : [];
180
+ return files.map((f) => {
181
+ const text = read(join(AGENTS_ALT, f));
182
+ const fm = frontmatter(text);
183
+ const id = String(fm.name ?? f.replace(/\.md$/, ''));
184
+ const norse = String(fm.norse_name ?? fm.name ?? id);
185
+ // Real, sourced fields only — no fabricated status/trace/tasks.
186
+ const registered = existsSync(join(ROOT, '.opencode/agents', `${id}.md`));
187
+ return {
188
+ id,
189
+ name: norse.charAt(0).toUpperCase() + norse.slice(1),
190
+ role: String(fm.descriptor ?? fm.role ?? 'agent'),
191
+ realm: 'work',
192
+ domain: String(fm.domain ?? 'ymirlabs'),
193
+ status: registered ? 'nominal' : 'degraded',
194
+ capabilities: Array.isArray(fm.capabilities) ? fm.capabilities : [],
195
+ skills: [],
196
+ interface: { protocol: 'a2a/1.0', endpoint: `local://${id}`, signed: false },
197
+ model: String(fm.model ?? '') || model,
198
+ uptime: 0,
199
+ tasksDone: 0,
200
+ traceability: 0,
201
+ };
202
+ });
203
+ }
204
+
205
+ /* ---- /api/tasks (forge orders) ------------------------------------------- */
206
+ function orders_uncached() {
207
+ const text = read(MASTERPLAN);
208
+ const out: { id: string; title: string; phase: string; status: string }[] = [];
209
+ const lines = text.split('\n');
210
+ let cur: { id: string; title: string; phase: string; status: string } | null = null;
211
+ for (const line of lines) {
212
+ const h = line.match(/^\*\*(W\d{4}) — (.+?)\*\*\s*$/);
213
+ if (h) {
214
+ if (cur) out.push(cur);
215
+ cur = { id: h[1], title: h[2].trim(), phase: 'P?', status: 'ADDED' };
216
+ continue;
217
+ }
218
+ if (!cur) continue;
219
+ const ph = line.match(/^- Phase:\s*([^·]+)/);
220
+ if (ph) cur.phase = ph[1].trim();
221
+ const st = line.match(/^- Status:\s*([A-Z/]+)/);
222
+ if (st && !/^\+/.test(line)) cur.status = st[1];
223
+ const note = line.match(/^\s*-\s+\d{4}-\d{2}-\d{2}\s+([A-Z]+)\b/);
224
+ if (note) cur.status = note[1];
225
+ }
226
+ if (cur) out.push(cur);
227
+ return out;
228
+ }
229
+
230
+ function orders() {
231
+ return memo('orders', 2000, orders_uncached);
232
+ }
233
+
234
+ function tasks() {
235
+ const now = new Date().toISOString();
236
+ return orders().map((o, i) => ({
237
+ id: o.id,
238
+ title: o.title,
239
+ state: o.status === 'COMPLETED' ? 'COMPLETED' : o.status === 'BLOCKED' ? 'INPUT_REQUIRED' : 'WORKING',
240
+ realm: 'wayof',
241
+ agent: 'Brokk',
242
+ agentId: 'brokk',
243
+ order: o.phase,
244
+ progress: o.status === 'COMPLETED' ? 100 : 25,
245
+ startedAt: now,
246
+ updatedAt: now,
247
+ artifacts: [],
248
+ log: [`${o.id} · ${o.status} · ${o.phase}`],
249
+ }));
250
+ }
251
+
252
+ /* ---- /api/runes ---------------------------------------------------------- */
253
+ function runes_uncached() {
254
+ return read(RUNES)
255
+ .split('\n')
256
+ .filter((l) => l.trim().startsWith('{'))
257
+ .map((l, i) => {
258
+ try {
259
+ const e = JSON.parse(l);
260
+ const level = /fail|error/i.test(e.event ?? '') ? 'danger' : /warn|stale/i.test(e.event ?? '') ? 'warn' : /briefing|written/i.test(e.event ?? '') ? 'ok' : 'info';
261
+ return {
262
+ id: e.checksum ? String(e.checksum).slice(0, 12) : `rune-${i}`,
263
+ ts: e.timestamp ?? '',
264
+ agent: e.actor ?? 'brokk',
265
+ module: e.event ?? '',
266
+ event: e.message ?? '',
267
+ checksum: String(e.checksum ?? '').slice(0, 12),
268
+ realm: e.realm || 'wayof',
269
+ order: e.order ?? '',
270
+ level,
271
+ };
272
+ } catch {
273
+ return null;
274
+ }
275
+ })
276
+ .filter(Boolean)
277
+ .reverse();
278
+ }
279
+
280
+ function runes() {
281
+ return memo('runes', 3000, runes_uncached);
282
+ }
283
+
284
+ /* ---- /api/well ----------------------------------------------------------- */
285
+ const MIMIR_URL = process.env.MIMIRSBRUNN_URL ?? 'http://127.0.0.1:4602';
286
+
287
+ function wellTitle(body: string, i = 0): string {
288
+ return (body.split('\n')[0] ?? '').replace(/^#+\s*/, '').slice(0, 80) || `episode ${i}`;
289
+ }
290
+
291
+ /** The local JSONL well — used when the engram bridge is down. */
292
+ function localWell(q = '') {
293
+ const rows = read(WELL)
294
+ .split('\n')
295
+ .filter((l) => l.trim().startsWith('{'))
296
+ .map((l, i) => {
297
+ try {
298
+ const e = JSON.parse(l);
299
+ const body = String(e.content ?? '');
300
+ return {
301
+ id: e.hash ?? `ep-${i}`,
302
+ title: wellTitle(body, i),
303
+ body: body.slice(0, 600),
304
+ score: 0.9,
305
+ mode: 'hybrid',
306
+ agentScope: e.source ?? 'assets/data',
307
+ ts: e.timestamp ?? '',
308
+ tags: Array.isArray(e.tags) ? e.tags : [],
309
+ };
310
+ } catch {
311
+ return null;
312
+ }
313
+ })
314
+ .filter(Boolean) as { title: string; body: string }[];
315
+ const ql = q.toLowerCase();
316
+ const filtered = q ? rows.filter((r) => `${r.title} ${r.body}`.toLowerCase().includes(ql)) : rows;
317
+ return filtered.slice(0, 60).reverse();
318
+ }
319
+
320
+ /** Drink from the well: the engram bridge answers when it is up, else the file. */
321
+ async function well(q = '') {
322
+ try {
323
+ const url = q
324
+ ? `${MIMIR_URL}/recall?q=${encodeURIComponent(q)}&k=60`
325
+ : `${MIMIR_URL}/recent?limit=60`;
326
+ const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
327
+ if (res.ok) {
328
+ const data = (await res.json()) as {
329
+ results?: { score?: number; episode?: { id?: string; content?: string; timestamp?: string; tags?: string[]; actors?: string[]; agent_id?: string } }[];
330
+ episodes?: { id?: string; content?: string; timestamp?: string; tags?: string[]; actors?: string[]; agent_id?: string }[];
331
+ };
332
+ if (data.results) {
333
+ return data.results.map((r) => {
334
+ const e = r.episode ?? {};
335
+ const body = String(e.content ?? '');
336
+ return {
337
+ id: e.id ?? '',
338
+ title: wellTitle(body),
339
+ body: body.slice(0, 600),
340
+ score: r.score ?? 0,
341
+ mode: 'hybrid',
342
+ agentScope: (e.actors ?? [])[0] ?? e.agent_id ?? 'well',
343
+ ts: e.timestamp ?? '',
344
+ tags: e.tags ?? [],
345
+ };
346
+ });
347
+ }
348
+ if (data.episodes) {
349
+ return data.episodes.map((e) => {
350
+ const body = String(e.content ?? '');
351
+ return {
352
+ id: e.id ?? '',
353
+ title: wellTitle(body),
354
+ body: body.slice(0, 600),
355
+ score: 0.9,
356
+ mode: 'recent',
357
+ agentScope: (e.actors ?? [])[0] ?? e.agent_id ?? 'well',
358
+ ts: e.timestamp ?? '',
359
+ tags: e.tags ?? [],
360
+ };
361
+ });
362
+ }
363
+ }
364
+ } catch {
365
+ /* bridge down — fall through to the local file */
366
+ }
367
+ return localWell(q);
368
+ }
369
+
370
+ /* ---- /api/mimir/health --------------------------------------------------- */
371
+ async function mimirHealth() {
372
+ try {
373
+ const res = await fetch(`${MIMIR_URL}/health`, { signal: AbortSignal.timeout(4000) });
374
+ if (res.ok) return await res.json();
375
+ } catch {
376
+ /* down */
377
+ }
378
+ return { status: 'down', store: null, episodes: 0, agents: [] };
379
+ }
380
+
381
+ /** One full episode from the well — the Allfather reads the whole memory. */
382
+ async function wellEpisode(id: string) {
383
+ if (!id) return null;
384
+ try {
385
+ const res = await fetch(`${MIMIR_URL}/episode?id=${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(6000) });
386
+ if (res.ok) return await res.json();
387
+ } catch {
388
+ /* bridge down */
389
+ }
390
+ return null;
391
+ }
392
+
393
+ /* ---- /api/processes ------------------------------------------------------ */
394
+ async function processes() {
395
+ const out: unknown[] = [];
396
+ const cron = await runAsync(['bash', 'bin/nornir-cron-start.sh', '--status']);
397
+ const bridge = await runAsync(['bash', 'bin/bifrost-bridge.sh', '--status']);
398
+ out.push({
399
+ id: 'nornir-cron', name: 'Nornir cron', daemon: 'scheduler', manager: 'pm2',
400
+ status: cron.includes('running') ? 'nominal' : 'down', cpu: 0, mem: 12, restarts: 0,
401
+ uptime: 3600, realm: 'platform',
402
+ });
403
+ out.push({
404
+ id: 'bifrost-bridge', name: 'Bifrost model bridge', daemon: 'opencode-go bridge', manager: 'systemd',
405
+ status: bridge.includes('"up"') ? 'nominal' : 'down', cpu: 0, mem: 18, restarts: 0,
406
+ uptime: 3600, realm: 'platform',
407
+ });
408
+ // The fleet's daemons across PM2/Docker/systemd, read-only from Valhalla.
409
+ for (const r of parseToon(await runAsync(['bash', 'bin/valhalla.sh', 'list']))) {
410
+ const st = /running|active|online/i.test(r.status)
411
+ ? 'nominal'
412
+ : /exit|fail|inactive|dead|stop/i.test(r.status)
413
+ ? 'down'
414
+ : 'degraded';
415
+ out.push({
416
+ id: r.id,
417
+ name: r.name,
418
+ daemon: r.manager,
419
+ manager: r.manager,
420
+ status: st,
421
+ cpu: Number(r.cpu) || 0,
422
+ mem: Number(r.mem) || 0,
423
+ restarts: Number(r.restarts) || 0,
424
+ uptime: Number(r.uptime) || 0,
425
+ realm: 'platform',
426
+ });
427
+ }
428
+ return out;
429
+ }
430
+
431
+ /* ---- /api/smidja (the smithy's own trace, read-only) --------------------- */
432
+ // The smithy's db lives where the RUNTIME keeps it: $YMIR_HOME first (the data
433
+ // belongs outside the repo), else the app tree it moved into. It sat at the repo
434
+ // root's old path for a year; when the tree moved, this reader quietly opened
435
+ // nothing and every statistic showed zero - a gate with nothing to say.
436
+ const SMIDJA_DB = (() => {
437
+ if (process.env.SMIDJA_DB) return process.env.SMIDJA_DB;
438
+ const candidates = [
439
+ join(process.env.YMIR_HOME ?? join(homedir(), 'Documents', 'Ymir'), 'smidja', 'smidja.db'),
440
+ join(ROOT, 'apps', 'smidja', 'smidja_data', 'smidja.db'),
441
+ ];
442
+ return candidates.find((c) => existsSync(c)) ?? candidates[1];
443
+ })();
444
+
445
+ function smidja(): Database | null {
446
+ try {
447
+ if (!existsSync(SMIDJA_DB)) return null;
448
+ return new Database(SMIDJA_DB, { readonly: true });
449
+ } catch {
450
+ return null;
451
+ }
452
+ }
453
+
454
+ function smidjaHealth() {
455
+ const db = smidja();
456
+ if (!db) return { db: 'absent', sessions: 0 };
457
+ try {
458
+ const r = db.query('select count(*) c from sessions').get() as { c?: number };
459
+ return { db: 'present', sessions: r?.c ?? 0 };
460
+ } catch {
461
+ return { db: 'present', sessions: 0 };
462
+ } finally {
463
+ db.close();
464
+ }
465
+ }
466
+
467
+ function smidjaSessions(limit = 100) {
468
+ const db = smidja();
469
+ if (!db) return [];
470
+ try {
471
+ return db
472
+ .query(
473
+ 'select smidja_id, smidja_name, status, engineer, total_tokens, total_cost, started_at, ended_at from sessions order by started_at desc limit ?',
474
+ )
475
+ .all(limit);
476
+ } catch {
477
+ return [];
478
+ } finally {
479
+ db.close();
480
+ }
481
+ }
482
+
483
+ function smidjaSession(id: string) {
484
+ const db = smidja();
485
+ if (!db) return null;
486
+ try {
487
+ const session = db.query('select * from sessions where smidja_id = ?').get(id);
488
+ if (!session) return null;
489
+ return {
490
+ session,
491
+ phases: db.query('select * from phases where smidja_id = ? order by seq, rowid').all(id),
492
+ events: db
493
+ .query('select rowid, event_id, phase_id, parent_id, type, name, payload_json, tokens, started_at, ended_at from events where smidja_id = ? order by rowid limit 2000')
494
+ .all(id),
495
+ envelopes: db.query('select * from envelopes where smidja_id = ? order by created_at, rowid').all(id),
496
+ gates: db.query('select * from gate_results where smidja_id = ? order by id').all(id),
497
+ agents: db.query('select * from agent_sessions where smidja_id = ? order by created_at, agent').all(id),
498
+ };
499
+ } catch {
500
+ return null;
501
+ } finally {
502
+ db.close();
503
+ }
504
+ }
505
+
506
+ function smidjaDecisions() {
507
+ const db = smidja();
508
+ if (!db) return { total_failed: 0, decisions: [] };
509
+ try {
510
+ const decisions = db
511
+ .query(
512
+ "select ph.name phase, ph.error, ag.model, count(*) count from phases ph left join agent_sessions ag on ag.smidja_id=ph.smidja_id and ag.agent=ph.owner where ph.status='fail' group by ph.name, ph.error, ag.model order by count desc",
513
+ )
514
+ .all();
515
+ return { total_failed: decisions.length, decisions };
516
+ } catch {
517
+ return { total_failed: 0, decisions: [] };
518
+ } finally {
519
+ db.close();
520
+ }
521
+ }
522
+
523
+ interface VendorModelDef {
524
+ id: string;
525
+ name: string;
526
+ provider: string;
527
+ tier: 1 | 2 | 3;
528
+ rank: number;
529
+ pin: number;
530
+ po: number;
531
+ cache: number;
532
+ }
533
+
534
+ // Commercial model catalog — rates are standard pay-as-you-go API prices per 1M
535
+ // tokens (input/output), midpoint where the spec quotes a range. `cache` is the
536
+ // share of the input price billed on a cache read (OpenAI/DeepSeek/Meta 50%,
537
+ // Google 25%, Anthropic 10%). Ported 1:1 from the Smiðja visualizer db.ts.
538
+ const VENDOR_MODELS: VendorModelDef[] = [
539
+ { id: 'gpt-5.5-pro', name: 'GPT-5.5 Pro', provider: 'OpenAI', tier: 1, rank: 1, pin: 30.0, po: 180.0, cache: 0.5 },
540
+ { id: 'o3-pro', name: 'o3-pro', provider: 'OpenAI', tier: 1, rank: 2, pin: 20.0, po: 80.0, cache: 0.5 },
541
+ { id: 'claude-opus', name: 'Claude Opus 4.6 / 5', provider: 'Anthropic', tier: 1, rank: 3, pin: 5.0, po: 25.0, cache: 0.1 },
542
+ { id: 'gpt-5.5', name: 'GPT-5.5', provider: 'OpenAI', tier: 1, rank: 4, pin: 5.0, po: 30.0, cache: 0.5 },
543
+ { id: 'llama-3.1-405b', name: 'Llama 3.1 405B', provider: 'Meta (Hosted)', tier: 1, rank: 5, pin: 3.75, po: 3.75, cache: 0.5 },
544
+ { id: 'gpt-5.4', name: 'GPT-5.4', provider: 'OpenAI', tier: 1, rank: 6, pin: 2.5, po: 15.0, cache: 0.5 },
545
+ { id: 'gpt-4o', name: 'GPT-4o', provider: 'OpenAI', tier: 1, rank: 7, pin: 2.5, po: 10.0, cache: 0.5 },
546
+ { id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', provider: 'Google', tier: 1, rank: 8, pin: 2.0, po: 12.0, cache: 0.25 },
547
+ { id: 'o3', name: 'o3', provider: 'OpenAI', tier: 1, rank: 9, pin: 2.0, po: 8.0, cache: 0.5 },
548
+ { id: 'claude-sonnet', name: 'Claude Sonnet 4.6 / 5', provider: 'Anthropic', tier: 1, rank: 10, pin: 2.5, po: 12.5, cache: 0.1 },
549
+ { id: 'mistral-large-3', name: 'Mistral Large 3', provider: 'Mistral AI', tier: 2, rank: 11, pin: 0.5, po: 1.5, cache: 0.5 },
550
+ { id: 'claude-haiku', name: 'Claude Haiku 4.5', provider: 'Anthropic', tier: 2, rank: 12, pin: 1.0, po: 5.0, cache: 0.1 },
551
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro', provider: 'Google', tier: 2, rank: 13, pin: 1.25, po: 10.0, cache: 0.25 },
552
+ { id: 'o4-mini', name: 'o4-mini', provider: 'OpenAI', tier: 2, rank: 14, pin: 0.55, po: 2.2, cache: 0.5 },
553
+ { id: 'deepseek-r1', name: 'DeepSeek R1', provider: 'DeepSeek', tier: 2, rank: 15, pin: 0.55, po: 2.19, cache: 0.5 },
554
+ { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash Preview', provider: 'Google', tier: 2, rank: 16, pin: 0.5, po: 3.0, cache: 0.25 },
555
+ { id: 'command-r-plus', name: 'Command R+', provider: 'Cohere', tier: 2, rank: 17, pin: 2.5, po: 10.0, cache: 0.5 },
556
+ { id: 'gpt-5.4-mini', name: 'GPT-5.4 Mini', provider: 'OpenAI', tier: 2, rank: 18, pin: 0.75, po: 4.5, cache: 0.5 },
557
+ { id: 'deepseek-v4', name: 'DeepSeek V4', provider: 'DeepSeek', tier: 2, rank: 19, pin: 0.3, po: 0.5, cache: 0.5 },
558
+ { id: 'codestral', name: 'Codestral', provider: 'Mistral AI', tier: 2, rank: 20, pin: 0.3, po: 0.9, cache: 0.5 },
559
+ { id: 'deepseek-chat-v3.2', name: 'DeepSeek-Chat (V3.2)', provider: 'DeepSeek', tier: 2, rank: 21, pin: 0.28, po: 0.42, cache: 0.5 },
560
+ { id: 'llama-3.3-70b', name: 'Llama 3.3 70B', provider: 'Meta (Hosted)', tier: 2, rank: 22, pin: 0.41, po: 0.52, cache: 0.5 },
561
+ { id: 'mistral-medium-3.5', name: 'Mistral Medium 3.5', provider: 'Mistral AI', tier: 2, rank: 23, pin: 1.5, po: 7.5, cache: 0.5 },
562
+ { id: 'gpt-4.1', name: 'GPT-4.1', provider: 'OpenAI', tier: 2, rank: 24, pin: 2.0, po: 8.0, cache: 0.5 },
563
+ { id: 'qwen-2.5-72b', name: 'Qwen 2.5 72B', provider: 'Alibaba (Hosted)', tier: 2, rank: 25, pin: 0.35, po: 0.4, cache: 0.5 },
564
+ { id: 'gemini-3.7-flash', name: 'Gemini 3.7 Flash', provider: 'Google', tier: 3, rank: 26, pin: 0.75, po: 3.75, cache: 0.25 },
565
+ { id: 'gemini-3.6-flash', name: 'Gemini 3.6 Flash', provider: 'Google', tier: 3, rank: 27, pin: 0.75, po: 3.75, cache: 0.25 },
566
+ { id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini', provider: 'OpenAI', tier: 3, rank: 28, pin: 0.4, po: 1.6, cache: 0.5 },
567
+ { id: 'llama-4-maverick', name: 'Llama 4 Maverick', provider: 'Meta (Hosted)', tier: 3, rank: 29, pin: 0.25, po: 0.875, cache: 0.5 },
568
+ { id: 'gpt-5-mini', name: 'GPT-5 Mini', provider: 'OpenAI', tier: 3, rank: 30, pin: 0.25, po: 2.0, cache: 0.5 },
569
+ { id: 'gemini-3.1-flash-lite', name: 'Gemini 3.1 Flash-Lite', provider: 'Google', tier: 3, rank: 31, pin: 0.25, po: 1.5, cache: 0.25 },
570
+ { id: 'gpt-5.4-nano', name: 'GPT-5.4 Nano', provider: 'OpenAI', tier: 3, rank: 32, pin: 0.2, po: 1.25, cache: 0.5 },
571
+ { id: 'mistral-small-4', name: 'Mistral Small 4', provider: 'Mistral AI', tier: 3, rank: 33, pin: 0.15, po: 0.6, cache: 0.5 },
572
+ { id: 'gpt-4o-mini', name: 'GPT-4o mini', provider: 'OpenAI', tier: 3, rank: 34, pin: 0.15, po: 0.6, cache: 0.5 },
573
+ { id: 'gpt-4.1-nano', name: 'GPT-4.1 Nano', provider: 'OpenAI', tier: 3, rank: 35, pin: 0.1, po: 0.4, cache: 0.5 },
574
+ { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash-Lite', provider: 'Google', tier: 3, rank: 36, pin: 0.1, po: 0.4, cache: 0.25 },
575
+ { id: 'llama-4-scout', name: 'Llama 4 Scout', provider: 'Meta (Hosted)', tier: 3, rank: 37, pin: 0.165, po: 0.5, cache: 0.5 },
576
+ { id: 'ministral-8b', name: 'Ministral 8B', provider: 'Mistral AI', tier: 3, rank: 38, pin: 0.1, po: 0.1, cache: 0.5 },
577
+ { id: 'llama-3.2-3b', name: 'Llama 3.2 3B', provider: 'Meta (Hosted)', tier: 3, rank: 39, pin: 0.02, po: 0.02, cache: 0.5 },
578
+ { id: 'ministral-3b', name: 'Ministral 3B', provider: 'Mistral AI', tier: 3, rank: 40, pin: 0.04, po: 0.04, cache: 0.5 },
579
+ ];
580
+
581
+ const TIER_LABEL: Record<number, string> = {
582
+ 1: 'Tier 1 — heavyweight flagships & enterprise intelligence',
583
+ 2: 'Tier 2 — mid-range & high-efficiency workhorses',
584
+ 3: 'Tier 3 — flash, mini & edge engines',
585
+ };
586
+
587
+ const LOCAL_MODEL_PREFIXES = ['lmstudio/', 'ollama/', 'localhost/', 'vllm/', 'unsloth/'];
588
+ const isLocalModel = (model: string) =>
589
+ LOCAL_MODEL_PREFIXES.some((p) => model.toLowerCase().startsWith(p));
590
+
591
+ function emptyStats() {
592
+ const provider = () => ({ events: 0, sessions: 0, tokens: 0, cost: 0, input: 0, output: 0, cache_read: 0 });
593
+ const vendorCost = { cached_cost: 0, total_cost: 0, savings: 0, savings_pct: 0 };
594
+ return {
595
+ totals: { runs: 0, success: 0, fail: 0, running: 0, tokens: 0, cost: 0 },
596
+ usage: { input: 0, output: 0, cache_read: 0, cache_write: 0, total: 0 },
597
+ cache_hit_ratio: 0,
598
+ avg_cache_hit_per_run: 0,
599
+ vendors: { gpt4o: { ...vendorCost }, gemini: { ...vendorCost } },
600
+ vendor_catalog: [] as unknown[],
601
+ providers: { local: provider(), online: provider(), per_model: [] as unknown[] },
602
+ by_chain: [] as unknown[],
603
+ by_model: [] as unknown[],
604
+ generated_at: new Date().toISOString(),
605
+ };
606
+ }
607
+
608
+ /** Statistics — runs, tokens, cost, cache-hit ratio, and commercial savings.
609
+ * Ported 1:1 from the Smiðja visualizer `db.ts` `stats()`: usage comes from
610
+ * `agent_end` event payloads, model attribution from `agent_start` events. */
611
+ function smidjaStats_uncached() {
612
+ const db = smidja();
613
+ if (!db) return emptyStats();
614
+ try {
615
+ const hasSessions = db
616
+ .query("select name from sqlite_master where type='table' and name='sessions'")
617
+ .get();
618
+ if (!hasSessions) return emptyStats();
619
+
620
+ const sessions = db
621
+ .query('select smidja_id, smidja_name, status, engineer, total_tokens, total_cost, started_at from sessions order by started_at desc')
622
+ .all() as Array<{
623
+ smidja_id: string;
624
+ smidja_name: string | null;
625
+ status: string | null;
626
+ engineer: string | null;
627
+ total_tokens: number | null;
628
+ total_cost: number | null;
629
+ started_at: string | null;
630
+ }>;
631
+
632
+ const totals = { runs: 0, success: 0, fail: 0, running: 0, tokens: 0, cost: 0 };
633
+ for (const s of sessions) {
634
+ totals.runs += 1;
635
+ if (s.status === 'success') totals.success += 1;
636
+ else if (s.status === 'fail') totals.fail += 1;
637
+ else if (s.status === 'running') totals.running += 1;
638
+ totals.tokens += s.total_tokens ?? 0;
639
+ totals.cost += s.total_cost ?? 0;
640
+ }
641
+
642
+ const usage = { input: 0, output: 0, cache_read: 0, cache_write: 0, total: 0 };
643
+ const cacheHitRuns: number[] = [];
644
+ interface ModelAgg {
645
+ events: number;
646
+ sessions: Set<string>;
647
+ tokens: number;
648
+ cost: number;
649
+ input: number;
650
+ output: number;
651
+ cache_read: number;
652
+ coding_agent: string | null;
653
+ }
654
+ const modelAggs = new Map<string, ModelAgg>();
655
+ const agentModel = new Map<string, string | null>();
656
+ const agentCoding = new Map<string, string | null>();
657
+ const started = new Map<string, { events: number; sessions: Set<string>; coding_agent: string | null }>();
658
+ const emptyAgg = (): ModelAgg => ({
659
+ events: 0,
660
+ sessions: new Set(),
661
+ tokens: 0,
662
+ cost: 0,
663
+ input: 0,
664
+ output: 0,
665
+ cache_read: 0,
666
+ coding_agent: null,
667
+ });
668
+
669
+ for (const s of sessions) {
670
+ const evs = db
671
+ .query('select type, name, payload_json from events where smidja_id = ? order by rowid')
672
+ .all(s.smidja_id) as Array<{ type: string; name: string | null; payload_json: string | null }>;
673
+ let r = 0;
674
+ let t = 0;
675
+ for (const e of evs) {
676
+ let payload: Record<string, unknown> = {};
677
+ try {
678
+ payload = e.payload_json ? JSON.parse(e.payload_json) : {};
679
+ } catch {
680
+ payload = {};
681
+ }
682
+ if (e.type === 'agent_start') {
683
+ const sp = payload as { model?: string; coding_agent?: string };
684
+ if (sp?.model) {
685
+ const key = `${s.smidja_id}|${e.name}`;
686
+ if (!agentModel.has(key)) {
687
+ const st =
688
+ started.get(sp.model) ?? { events: 0, sessions: new Set<string>(), coding_agent: sp.coding_agent ?? null };
689
+ st.events += 1;
690
+ st.sessions.add(s.smidja_id);
691
+ st.coding_agent = sp.coding_agent ?? st.coding_agent;
692
+ started.set(sp.model, st);
693
+ }
694
+ agentModel.set(key, sp.model);
695
+ agentCoding.set(key, sp.coding_agent ?? null);
696
+ }
697
+ continue;
698
+ }
699
+ if (e.type !== 'agent_end') continue;
700
+ const p = payload as {
701
+ usage?: {
702
+ input_tokens?: number;
703
+ output_tokens?: number;
704
+ cache_read_tokens?: number;
705
+ cache_write_tokens?: number;
706
+ total_tokens?: number;
707
+ input_cost?: number;
708
+ output_cost?: number;
709
+ cache_read_cost?: number;
710
+ cache_write_cost?: number;
711
+ };
712
+ cost?: number;
713
+ };
714
+ const u = p?.usage;
715
+ if (!u) continue;
716
+ usage.input += u.input_tokens ?? 0;
717
+ usage.output += u.output_tokens ?? 0;
718
+ usage.cache_read += u.cache_read_tokens ?? 0;
719
+ usage.cache_write += u.cache_write_tokens ?? 0;
720
+ usage.total += u.total_tokens ?? 0;
721
+ r += u.cache_read_tokens ?? 0;
722
+ t += (u.input_tokens ?? 0) + (u.cache_read_tokens ?? 0);
723
+ const model = e.name ? agentModel.get(`${s.smidja_id}|${e.name}`) : undefined;
724
+ if (!model) continue;
725
+ const usageCost =
726
+ (u.input_cost ?? 0) + (u.output_cost ?? 0) + (u.cache_read_cost ?? 0) + (u.cache_write_cost ?? 0);
727
+ const agg = modelAggs.get(model) ?? emptyAgg();
728
+ agg.events += 1;
729
+ agg.sessions.add(s.smidja_id);
730
+ agg.tokens += u.total_tokens ?? 0;
731
+ agg.cost += usageCost || (p.cost ?? 0);
732
+ agg.input += u.input_tokens ?? 0;
733
+ agg.output += u.output_tokens ?? 0;
734
+ agg.cache_read += u.cache_read_tokens ?? 0;
735
+ const coding = e.name ? agentCoding.get(`${s.smidja_id}|${e.name}`) : undefined;
736
+ agg.coding_agent = coding ?? agg.coding_agent;
737
+ modelAggs.set(model, agg);
738
+ }
739
+ if (t > 0) cacheHitRuns.push(r / t);
740
+ }
741
+
742
+ for (const [model, st] of started) {
743
+ if (modelAggs.has(model)) continue;
744
+ const agg = emptyAgg();
745
+ agg.events = st.events;
746
+ agg.sessions = st.sessions;
747
+ agg.coding_agent = st.coding_agent;
748
+ modelAggs.set(model, agg);
749
+ }
750
+ const ingested = usage.input + usage.cache_read;
751
+ const cacheHitRatio = ingested > 0 ? usage.cache_read / ingested : 0;
752
+
753
+ const vendor = (pin: number, pcr: number, po: number) => {
754
+ const std = (usage.input / 1_000_000) * pin;
755
+ const cached = (usage.cache_read / 1_000_000) * pcr;
756
+ const out = (usage.output / 1_000_000) * po;
757
+ const total = std + cached + out;
758
+ const savings = total - totals.cost;
759
+ const savings_pct = total > 0 ? savings / total : 0;
760
+ return { cached_cost: cached, total_cost: total, savings, savings_pct };
761
+ };
762
+ const gpt4o = vendor(2.5, 1.25, 10.0);
763
+ const gemini = vendor(1.25, 0.3125, 5.0);
764
+
765
+ const vendor_catalog = VENDOR_MODELS.map((m) => {
766
+ const v = vendor(m.pin, m.pin * m.cache, m.po);
767
+ return {
768
+ id: m.id,
769
+ name: m.name,
770
+ provider: m.provider,
771
+ tier: m.tier,
772
+ rank: m.rank,
773
+ tier_label: TIER_LABEL[m.tier],
774
+ input_price: m.pin,
775
+ output_price: m.po,
776
+ cache_price: m.pin * m.cache,
777
+ ...v,
778
+ };
779
+ });
780
+
781
+ const providerAgg = () => ({ events: 0, sessions: 0, tokens: 0, cost: 0, input: 0, output: 0, cache_read: 0 });
782
+ const providers = { local: providerAgg(), online: providerAgg() };
783
+ for (const [model, agg] of modelAggs) {
784
+ const p = providers[isLocalModel(model) ? 'local' : 'online'];
785
+ p.events += agg.events;
786
+ p.sessions += agg.sessions.size;
787
+ p.tokens += agg.tokens;
788
+ p.cost += agg.cost;
789
+ p.input += agg.input;
790
+ p.output += agg.output;
791
+ p.cache_read += agg.cache_read;
792
+ }
793
+ const per_model = [...modelAggs.entries()]
794
+ .map(([model, agg]) => ({
795
+ model,
796
+ kind: isLocalModel(model) ? ('local' as const) : ('online' as const),
797
+ coding_agent: agg.coding_agent,
798
+ events: agg.events,
799
+ tokens: agg.tokens,
800
+ cost: agg.cost,
801
+ }))
802
+ .sort((a, b) => b.tokens - a.tokens);
803
+
804
+ const byChain = new Map<string, { runs: number; success: number; tokens: number; cost: number }>();
805
+ const byModel = new Map<string, { runs: number; success: number; tokens: number; cost: number }>();
806
+ for (const s of sessions) {
807
+ const chain = (s.smidja_name ?? 'smidja').split(' + ')[0];
808
+ const chainAgg = byChain.get(chain) ?? { runs: 0, success: 0, tokens: 0, cost: 0 };
809
+ chainAgg.runs += 1;
810
+ if (s.status === 'success') chainAgg.success += 1;
811
+ chainAgg.tokens += s.total_tokens ?? 0;
812
+ chainAgg.cost += s.total_cost ?? 0;
813
+ byChain.set(chain, chainAgg);
814
+
815
+ const modelAgg = byModel.get(s.smidja_name ?? '?') ?? { runs: 0, success: 0, tokens: 0, cost: 0 };
816
+ modelAgg.runs += 1;
817
+ if (s.status === 'success') modelAgg.success += 1;
818
+ modelAgg.tokens += s.total_tokens ?? 0;
819
+ modelAgg.cost += s.total_cost ?? 0;
820
+ byModel.set(s.smidja_name ?? '?', modelAgg);
821
+ }
822
+
823
+ return {
824
+ totals,
825
+ usage,
826
+ cache_hit_ratio: cacheHitRatio,
827
+ avg_cache_hit_per_run: cacheHitRuns.length ? cacheHitRuns.reduce((a, b) => a + b, 0) / cacheHitRuns.length : 0,
828
+ vendors: { gpt4o, gemini },
829
+ vendor_catalog,
830
+ providers: { ...providers, per_model },
831
+ by_chain: [...byChain.entries()].map(([chain, agg]) => Object.assign({ chain }, agg)).sort((a, b) => b.runs - a.runs),
832
+ by_model: [...byModel.entries()].map(([model, agg]) => Object.assign({ model }, agg)).sort((a, b) => b.runs - a.runs),
833
+ generated_at: new Date().toISOString(),
834
+ };
835
+ } catch {
836
+ return emptyStats();
837
+ } finally {
838
+ db.close();
839
+ }
840
+ }
841
+
842
+ function smidjaStats() {
843
+ return memo('smidjaStats', 3000, smidjaStats_uncached);
844
+ }
845
+
846
+ /* ---- /api/reviews (real PRs + compliance as checks) ---------------------- */
847
+ async function reviews() {
848
+ const cards: unknown[] = [];
849
+
850
+ // 1. Real pull requests from the repo's remote — the Glitnir gate. A delivered
851
+ // change must appear here so the Allfather can review and seal it; without
852
+ // this the surface showed only the synthetic lint card and no PR ever landed.
853
+ try {
854
+ const list = JSON.parse(
855
+ (await runAsync([
856
+ 'gh', 'pr', 'list', '--state', 'open', '--limit', '50',
857
+ '--json', 'number,title,author,headRefName,isDraft,updatedAt,additions,deletions,statusCheckRollup',
858
+ ], 15000)) || '[]',
859
+ ) as Array<{
860
+ number: number;
861
+ title: string;
862
+ author?: { login?: string };
863
+ headRefName?: string;
864
+ isDraft?: boolean;
865
+ updatedAt?: string;
866
+ additions?: number;
867
+ deletions?: number;
868
+ statusCheckRollup?: Array<{ name?: string; context?: string; status?: string; conclusion?: string }>;
869
+ }>;
870
+ for (const pr of list) {
871
+ const checks = (pr.statusCheckRollup ?? []).map((c) => ({
872
+ name: c.name ?? c.context ?? 'check',
873
+ state:
874
+ c.conclusion === 'SUCCESS' || c.status === 'IN_PROGRESS' || c.status === 'QUEUED'
875
+ ? 'nominal'
876
+ : c.conclusion
877
+ ? 'down'
878
+ : 'nominal',
879
+ }));
880
+ const failing = checks.some((c) => c.state === 'down');
881
+ cards.push({
882
+ id: `pr-${pr.number}`,
883
+ number: pr.number,
884
+ title: pr.title,
885
+ repo: 'origin',
886
+ author: pr.author?.login ?? 'unknown',
887
+ realm: 'work',
888
+ state: pr.isDraft ? 'open' : failing ? 'changes' : 'open',
889
+ checks,
890
+ checklist: [{ label: `branch ${pr.headRefName ?? '?'}`, done: true }],
891
+ additions: pr.additions ?? 0,
892
+ deletions: pr.deletions ?? 0,
893
+ updatedAt: pr.updatedAt ?? new Date().toISOString(),
894
+ });
895
+ }
896
+ } catch {
897
+ /* gh unavailable or offline — the compliance card below still stands */
898
+ }
899
+
900
+ // 2. The compliance card (lint + governed-path checks) always stands.
901
+ const out = await runAsync(['bash', 'bin/brokk-lint.sh', '--quiet'], 60000);
902
+ const compliance = await runAsync(['bash', '.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh', '--json'], 60000);
903
+ let gates: { id: string; status: string; detail: string }[] = [];
904
+ try {
905
+ gates = JSON.parse(compliance).checks ?? [];
906
+ } catch {
907
+ /* ignore */
908
+ }
909
+ const lintOk = out.trim() === '';
910
+ const failing = !lintOk || gates.some((g) => g.status !== 'PASS');
911
+ cards.push({
912
+ id: 'lint', number: 0, title: 'Brokk lint + compliance gate', repo: 'Ymir', author: 'brokk', realm: 'work',
913
+ // A gate with any failing check is never APPROVED — it awaits the Allfather's seal.
914
+ state: failing ? 'changes' : 'open',
915
+ checks: [
916
+ { name: 'lint', state: lintOk ? 'nominal' : 'down' },
917
+ ...gates.map((g) => ({ name: g.id, state: g.status === 'PASS' ? 'nominal' : 'down' })),
918
+ ],
919
+ checklist: [
920
+ { label: `lint: ${lintOk ? 'clean' : 'issues found'}`, done: lintOk },
921
+ ...gates.map((g) => ({ label: `${g.id}: ${g.detail}`, done: g.status === 'PASS' })),
922
+ ],
923
+ additions: 0, deletions: 0, updatedAt: new Date().toISOString(),
924
+ });
925
+ return cards;
926
+ }
927
+
928
+ /* ---- /api/files (realm tree) --------------------------------------------- */
929
+ /** Resolve a workspace id to its on-disk scope: a company container when the
930
+ * workspace names one, else its repo-root workspace scope. */
931
+ function workspaceRoot(realm: string): string {
932
+ // THE HOARD FIRST (Rule 04). A realm's tree lives at $YMIR_HOME/svartalfaheim/
933
+ // <realm>; the checkout is only a legacy fallback for a home not yet migrated.
934
+ // Skrymir was browsing the repo's own directories and reading files whose links
935
+ // point into the hoard — which is why a listed file answered "not found".
936
+ const ws = workspaces().find((w) => w.id === realm);
937
+ if (ws?.company) {
938
+ const company = join(HOME_DIR, 'svartalfaheim', ws.company);
939
+ if (existsSync(company)) return company;
940
+ }
941
+ const realmTree = join(HOME_DIR, 'svartalfaheim', realm);
942
+ if (existsSync(realmTree)) return realmTree;
943
+ const scope = join(HOME_DIR, 'workspace', realm);
944
+ if (existsSync(scope)) return scope;
945
+ const slug = realm.toLowerCase().replace(/[^a-z0-9]/g, '');
946
+ const legacy = join(ROOT, 'svartalfaheim', slug);
947
+ if (slug && slug !== realm && existsSync(legacy)) return legacy;
948
+ const legacyScope = join(ROOT, 'workspace', realm);
949
+ if (existsSync(legacyScope)) return legacyScope;
950
+ return realmTree; // the hoard, even when empty: a home is never the repo's
951
+ }
952
+
953
+ function files(realm: string) {
954
+ const base = workspaceRoot(realm);
955
+ const walk = (dir: string, rel: string, depth: number): unknown => {
956
+ const name = rel === '' ? (realm || 'svartalfaheim') : rel.split('/').pop()!;
957
+ const node: Record<string, unknown> = { name, type: 'dir', path: rel || '/', children: [] as unknown[] };
958
+ if (depth > 3 || !existsSync(dir)) return node;
959
+ let entries: string[] = [];
960
+ try {
961
+ entries = readdirSync(dir);
962
+ } catch {
963
+ return node;
964
+ }
965
+ for (const e of entries) {
966
+ if (e.startsWith('.')) continue;
967
+ const full = join(dir, e);
968
+ let st;
969
+ try {
970
+ st = statSync(full);
971
+ } catch {
972
+ continue;
973
+ }
974
+ const childRel = rel ? `${rel}/${e}` : e;
975
+ if (st.isDirectory()) (node.children as unknown[]).push(walk(full, childRel, depth + 1));
976
+ else (node.children as unknown[]).push({ name: e, type: 'file', path: childRel, size: st.size, updated: st.mtime.toISOString() });
977
+ }
978
+ return node;
979
+ };
980
+ if (existsSync(base)) return walk(base, '', 0);
981
+ return walk(HOME_DIR, '', 0); // the hoard's home, not the checkout's docs
982
+ }
983
+
984
+ /* ---- /api/file — read one realm file, read-only, scoped ------------------ */
985
+ function fileContent(realm: string, rel: string) {
986
+ const base = resolve(workspaceRoot(realm));
987
+ const full = resolve(base, rel);
988
+ if (full !== base && !full.startsWith(base + '/')) return { error: 'outside realm' };
989
+ if (!existsSync(full)) return { error: 'not found' };
990
+ let st;
991
+ try {
992
+ st = statSync(full);
993
+ } catch {
994
+ return { error: 'unreadable' };
995
+ }
996
+ if (!st.isFile()) return { error: 'not a file' };
997
+ if (st.size > 512 * 1024) return { path: rel, size: st.size, body: '', note: 'file too large to preview' };
998
+ return { path: rel, size: st.size, updated: st.mtime.toISOString(), body: read(full) };
999
+ }
1000
+
1001
+ /* ---- /api/skills — the live skill index (read-only) ---------------------- */
1002
+ function skills() {
1003
+ const dir = join(ROOT, '.agents/skills');
1004
+ if (!existsSync(dir)) return [];
1005
+ return readdirSync(dir)
1006
+ .filter((d) => existsSync(join(dir, d, 'SKILL.md')) || existsSync(join(dir, d, 'AGENTS.md')))
1007
+ .map((d) => {
1008
+ const p = existsSync(join(dir, d, 'SKILL.md')) ? join(dir, d, 'SKILL.md') : join(dir, d, 'AGENTS.md');
1009
+ const fm = frontmatter(read(p));
1010
+ const name = String(fm.name ?? d);
1011
+ const description = String(fm.description ?? '').slice(0, 240);
1012
+ const aett = name.includes('-') ? name.split('-')[0] : 'galdr';
1013
+ return { id: `skl-${d}`, name, aett, description, capabilities: [], validated: true, domain: 'ymirlabs', createdAt: '' };
1014
+ });
1015
+ }
1016
+
1017
+ /* ---- /api/runtime -------------------------------------------------------- */
1018
+ async function runtime() {
1019
+ const digest = await runAsync(['bash', 'bin/saga-session-start.sh'], 10000);
1020
+ const markers = {
1021
+ lock: read(join(STATE_DIR, '.lock')).trim(),
1022
+ started: existsSync(join(STATE_DIR, '.session-start-complete')),
1023
+ armed: existsSync(join(STATE_DIR, '.supervision-armed')),
1024
+ };
1025
+ return { digest, markers, doc: 'docs/session-start.md' };
1026
+ }
1027
+
1028
+ /* ---- /api/cron ----------------------------------------------------------- */
1029
+ async function cron() {
1030
+ const cfg = read(join(CONFIG_DIR, 'cron.yaml'));
1031
+ const jobs = cfg
1032
+ .split('\n')
1033
+ .filter((l) => /^\d{2}:\d{2}\s+/.test(l))
1034
+ .map((l) => ({ at: l.slice(0, 5), command: l.slice(6).trim() }));
1035
+ const status = await runAsync(['bash', 'bin/nornir-cron-start.sh', '--status']);
1036
+ const running = status.includes('running');
1037
+ const pid = (status.match(/pid=(\d+)/) ?? [])[1] ?? '';
1038
+ return { running, pid, jobs: jobs.map((j) => ({ ...j, status: running ? 'scheduled' : 'stopped' })) };
1039
+ }
1040
+
1041
+ /* ---- /api/loaders, /api/checks, /api/settings ---------------------------- */
1042
+ function parseToon(text: string): Record<string, string>[] {
1043
+ const rows: Record<string, string>[] = [];
1044
+ const lines = text.split('\n');
1045
+ const header = lines.find((l) => /\{[^}]+\}:/.test(l));
1046
+ if (!header) return rows;
1047
+ const keys = (header.match(/\{([^}]+)\}/) ?? ['', ''])[1].split(',');
1048
+ const start = lines.indexOf(header);
1049
+ for (let i = start + 1; i < lines.length; i++) {
1050
+ const l = lines[i];
1051
+ if (!/^\s{2,}"/.test(l)) break;
1052
+ const vals = l.trim().split(/","/).map((v) => v.replace(/^"|"$/g, ''));
1053
+ const row: Record<string, string> = {};
1054
+ keys.forEach((k, idx) => (row[k.trim()] = vals[idx] ?? ''));
1055
+ rows.push(row);
1056
+ }
1057
+ return rows;
1058
+ }
1059
+
1060
+ async function loaders() {
1061
+ return parseToon(await runAsync(['bash', 'bin/valknut-load.sh', '--status']));
1062
+ }
1063
+ async function checks() {
1064
+ try {
1065
+ return (JSON.parse(await runAsync(['bash', '.agents/skills/galdr-ymirsystem/scripts/compliance-check.sh', '--json'], 60000)).checks ?? []) as unknown[];
1066
+ } catch {
1067
+ return [];
1068
+ }
1069
+ }
1070
+ function settings() {
1071
+ const dir = CONFIG_DIR;
1072
+ if (!existsSync(dir)) return [];
1073
+ return readdirSync(dir).map((f) => {
1074
+ const v = read(join(dir, f)).trim().slice(0, 120);
1075
+ return { key: f, set: v.length > 0, value: v };
1076
+ });
1077
+ }
1078
+
1079
+ /* ---- /api/stream (SSE): new runes lines ---------------------------------- */
1080
+ function stream() {
1081
+ let cursor = 0;
1082
+ try {
1083
+ cursor = statSync(RUNES).size;
1084
+ } catch {
1085
+ /* no ledger yet */
1086
+ }
1087
+ return new Response(
1088
+ new ReadableStream({
1089
+ start(controller) {
1090
+ const enc = new TextEncoder();
1091
+ const tick = setInterval(() => {
1092
+ try {
1093
+ const size = statSync(RUNES).size;
1094
+ if (size > cursor) {
1095
+ const tail = readFileSync(RUNES, 'utf8').slice(cursor);
1096
+ cursor = size;
1097
+ for (const line of tail.split('\n')) {
1098
+ if (!line.trim().startsWith('{')) continue;
1099
+ const e = JSON.parse(line);
1100
+ const ev = {
1101
+ id: String(e.checksum ?? Date.now()).slice(0, 12),
1102
+ ts: e.timestamp ?? new Date().toISOString(),
1103
+ kind: 'rune',
1104
+ from: e.actor ?? 'brokk',
1105
+ module: e.event ?? '',
1106
+ message: e.message ?? '',
1107
+ checksum: String(e.checksum ?? '').slice(0, 8),
1108
+ };
1109
+ controller.enqueue(enc.encode(`data: ${JSON.stringify(ev)}\n\n`));
1110
+ }
1111
+ }
1112
+ } catch {
1113
+ /* keep streaming */
1114
+ }
1115
+ }, 2000);
1116
+ (controller as unknown as { _tick?: ReturnType<typeof setInterval> })._tick = tick;
1117
+ },
1118
+ cancel() {
1119
+ /* interval GC scope */
1120
+ },
1121
+ }),
1122
+ { headers: { 'content-type': 'text/event-stream', 'cache-control': 'no-store', connection: 'keep-alive' } },
1123
+ );
1124
+ }
1125
+
1126
+ /* ---- /api/chat — Kaia, the oracle by the well ---------------------------- */
1127
+ const CHAT_DIR = join(STATE_DIR, 'chat');
1128
+ const CHAT_WINDOW = 40; // rolling context — the last 40 messages
1129
+ const SAFE_SESSION = /^[A-Za-z0-9._-]+$/;
1130
+ // Kaia speaks through the models the operator actually connected. The root Pi
1131
+ // catalog (`~/.pi/agent/models.json`) is the source of truth for the llama.cpp
1132
+ // router (`:8080`) and LM Studio; the Bifrost bridge (`:4603`) is the online
1133
+ // fallback. We never guess a llama.cpp model id — we read the one `.pi` uses.
1134
+ const PI_MODELS_PATH = join(homedir(), '.pi/agent/models.json');
1135
+ const PI_SETTINGS_PATH = join(homedir(), '.pi/agent/settings.json');
1136
+ const CHAT_BASES = [
1137
+ process.env.CHAT_BASE_URL,
1138
+ 'http://127.0.0.1:8080/v1',
1139
+ 'http://127.0.0.1:4603/v1',
1140
+ ].filter(Boolean) as string[];
1141
+ const CHAT_MODEL_ENV = process.env.CHAT_MODEL;
1142
+ const modelFor: Record<string, string> = {};
1143
+
1144
+ interface ChatTarget {
1145
+ base: string;
1146
+ model: string;
1147
+ key?: string;
1148
+ label: string;
1149
+ kind: 'local' | 'online';
1150
+ }
1151
+ interface ChatCatalogEntry extends ChatTarget {
1152
+ id: string;
1153
+ name: string;
1154
+ provider: string;
1155
+ }
1156
+
1157
+ async function listModels(base: string): Promise<string[]> {
1158
+ const res = await fetch(`${base}/models`);
1159
+ const data = (await res.json()) as { data?: { id?: string }[] };
1160
+ return (data.data ?? []).map((m) => m.id ?? '').filter(Boolean);
1161
+ }
1162
+
1163
+ /** Providers from the root Pi catalog: the exact base/model/key Pi uses. */
1164
+ function piCatalog(): ChatCatalogEntry[] {
1165
+ const out: ChatCatalogEntry[] = [];
1166
+ try {
1167
+ const raw = JSON.parse(read(PI_MODELS_PATH)) as {
1168
+ providers?: Record<string, { baseUrl?: string; apiKey?: string; models?: { id?: string }[] }>;
1169
+ };
1170
+ for (const [pkey, pv] of Object.entries(raw.providers ?? {})) {
1171
+ const base = pv.baseUrl ?? '';
1172
+ if (!base) continue;
1173
+ const kind: 'local' | 'online' = /(127\.0\.0\.1|localhost)/.test(base) ? 'local' : 'online';
1174
+ const models = (pv.models ?? []).map((m) => m.id ?? '').filter(Boolean);
1175
+ const single = models.length === 1;
1176
+ for (const mid of models) {
1177
+ out.push({
1178
+ id: single ? pkey : `${pkey}/${mid}`,
1179
+ name: mid,
1180
+ provider: pkey,
1181
+ kind,
1182
+ base,
1183
+ model: mid,
1184
+ key: pv.apiKey,
1185
+ label: mid,
1186
+ });
1187
+ }
1188
+ }
1189
+ } catch {
1190
+ /* no Pi catalog — fall back to live probing */
1191
+ }
1192
+ return out;
1193
+ }
1194
+
1195
+ async function resolveModel(base: string): Promise<string> {
1196
+ if (CHAT_MODEL_ENV) return CHAT_MODEL_ENV;
1197
+ if (modelFor[base]) return modelFor[base];
1198
+ const ids = await listModels(base);
1199
+ const pick = ids.find((i) => /gemma|qwen|deepseek|hermes/i.test(i)) ?? ids[0];
1200
+ if (!pick) throw new Error('no models advertised');
1201
+ modelFor[base] = pick;
1202
+ return pick;
1203
+ }
1204
+
1205
+ /** Order the backends to try for a chosen model (or the operator's default). */
1206
+ function resolveChatTarget(model?: string): ChatTarget[] {
1207
+ const cat = piCatalog();
1208
+ if (model) {
1209
+ // One model name can be advertised by several engines (LM Studio :1234,
1210
+ // the llama.cpp router :8080, Bifrost :4603). Return EVERY match — a dead
1211
+ // engine then falls through to a live one instead of failing the chat.
1212
+ // A provider-prefixed id (e.g. lmstudio/<m>) maps to the bare model name.
1213
+ const bare = model.includes('/') ? model.slice(model.indexOf('/') + 1) : model;
1214
+ const matches = cat.filter((e) => e.id === model || e.name === model || e.model === bare);
1215
+ if (matches.length) {
1216
+ const seen = new Set<string>();
1217
+ return matches.filter((e) => {
1218
+ const key = `${e.base}|${e.model}`;
1219
+ if (seen.has(key)) return false;
1220
+ seen.add(key);
1221
+ return true;
1222
+ });
1223
+ }
1224
+ if (/^(opencode-go|opencode)\//.test(model)) {
1225
+ return [{ base: 'http://127.0.0.1:4603/v1', model, label: model, kind: 'online' }];
1226
+ }
1227
+ const raw: ChatTarget[] = [
1228
+ { base: 'http://127.0.0.1:8080/v1', model, label: model, kind: 'local' },
1229
+ { base: 'http://127.0.0.1:4603/v1', model, label: model, kind: 'online' },
1230
+ ];
1231
+ return raw;
1232
+ }
1233
+ // No choice: the operator's Pi default, then any local catalog model, then bases.
1234
+ const targets: ChatTarget[] = [];
1235
+ try {
1236
+ const s = JSON.parse(read(PI_SETTINGS_PATH)) as { defaultProvider?: string; defaultModel?: string };
1237
+ const d = cat.find((e) => e.provider === s.defaultProvider && e.model === s.defaultModel) ?? cat.find((e) => e.provider === s.defaultProvider);
1238
+ if (d) targets.push(d);
1239
+ } catch {
1240
+ /* no settings */
1241
+ }
1242
+ const local = cat.find((e) => e.kind === 'local');
1243
+ if (local && !targets.some((t) => t.base === local.base && t.model === local.model)) targets.push(local);
1244
+ for (const base of CHAT_BASES) {
1245
+ if (!targets.some((t) => t.base === base)) targets.push({ base, model: '', label: '', kind: base.includes('4603') ? 'online' : 'local' });
1246
+ }
1247
+ return targets;
1248
+ }
1249
+
1250
+ /** Try the ordered targets; return the first non-empty reply. */
1251
+ async function chatCompletion(
1252
+ messages: { role: string; content: string }[],
1253
+ targets: ChatTarget[],
1254
+ ): Promise<{ body: string; backend: string; model: string }> {
1255
+ let lastErr = '';
1256
+ for (const t of targets) {
1257
+ try {
1258
+ const model = t.model || (await resolveModel(t.base));
1259
+ const headers: Record<string, string> = { 'content-type': 'application/json' };
1260
+ if (t.key) headers.authorization = `Bearer ${t.key}`;
1261
+ const res = await fetch(`${t.base}/chat/completions`, {
1262
+ method: 'POST',
1263
+ headers,
1264
+ body: JSON.stringify({ model, messages, stream: false, temperature: 0.6, max_tokens: Number(process.env.YMIR_CHAT_MAX_TOKENS ?? 4096) }), // deepseek-v4.1 spends budget on reasoning FIRST: 700 gave an empty answer
1265
+ });
1266
+ if (!res.ok) {
1267
+ lastErr = `${t.base} → ${res.status}`;
1268
+ continue;
1269
+ }
1270
+ const data = (await res.json()) as { choices?: { message?: { content?: string } }[] };
1271
+ const body = data.choices?.[0]?.message?.content?.trim();
1272
+ if (body) return { body, backend: t.base, model };
1273
+ lastErr = `${t.base} → empty`;
1274
+ } catch (err) {
1275
+ lastErr = `${t.base} → ${(err as Error).message}`;
1276
+ }
1277
+ }
1278
+ throw new Error(lastErr || 'no chat backend reachable');
1279
+ }
1280
+
1281
+ /** The models the operator has connected: the root Pi catalog (exact ids,
1282
+ * bases, keys), plus any live online backend. Free-typing is still allowed. */
1283
+ async function chatModels(): Promise<{ id: string; name: string; provider: string; kind: 'local' | 'online' }[]> {
1284
+ const out = new Map<string, { id: string; name: string; provider: string; kind: 'local' | 'online' }>();
1285
+ for (const e of piCatalog()) out.set(e.id, { id: e.id, name: e.name, provider: e.provider, kind: e.kind });
1286
+ // Online providers Pi knows (opencode-go / opencode) — list without probing.
1287
+ try {
1288
+ const res = await fetch('http://127.0.0.1:4603/v1/models', { signal: AbortSignal.timeout(2500) });
1289
+ if (res.ok) {
1290
+ const data = (await res.json()) as { data?: { id?: string }[] };
1291
+ for (const m of data.data ?? []) {
1292
+ const id = m.id ?? '';
1293
+ if (id && !out.has(id)) out.set(id, { id, name: id, provider: 'opencode-go', kind: 'online' });
1294
+ }
1295
+ }
1296
+ } catch {
1297
+ /* bridge down */
1298
+ }
1299
+ return [...out.values()].sort((a, b) => a.id.localeCompare(b.id));
1300
+ }
1301
+
1302
+ const KAIA_SYSTEM = [
1303
+ 'You are Kaia, the oracle by the well (Mimirsbrunn), the orchestrator of the Ymir fleet.',
1304
+ 'Address the Allfather directly in every reply; never respond with zero direct address.',
1305
+ 'Apply Norse methodology: name subsystems for the role-matched figure; never use imported terms.',
1306
+ 'Drink from the well before you answer; when a recall excerpt is provided, ground the reply in it.',
1307
+ 'Be concise, direct, and action-oriented — produce outcomes, not preamble.',
1308
+ 'You may dispatch Eindri in the real system; here, state plainly what you recalled and what you would dispatch.',
1309
+ ].join(' ');
1310
+
1311
+ interface ChatRow {
1312
+ id: string;
1313
+ from: 'user' | 'kaia';
1314
+ body: string;
1315
+ ts: string;
1316
+ recalling?: boolean;
1317
+ thinking?: boolean;
1318
+ error?: boolean;
1319
+ model?: string;
1320
+ agents?: string[];
1321
+ }
1322
+
1323
+ function chatFile(session: string): string {
1324
+ const safe = SAFE_SESSION.test(session) ? session : 'default';
1325
+ return join(CHAT_DIR, `${safe}.jsonl`);
1326
+ }
1327
+
1328
+ function chatHistory(session = 'default'): ChatRow[] {
1329
+ return read(chatFile(session))
1330
+ .split('\n')
1331
+ .filter((l) => l.trim().startsWith('{'))
1332
+ .map((l) => {
1333
+ try {
1334
+ return JSON.parse(l) as ChatRow;
1335
+ } catch {
1336
+ return null;
1337
+ }
1338
+ })
1339
+ .filter(Boolean) as ChatRow[];
1340
+ }
1341
+
1342
+ function appendChat(session: string, row: ChatRow): void {
1343
+ mkdirSync(CHAT_DIR, { recursive: true });
1344
+ appendFileSync(chatFile(session), `${JSON.stringify(row)}\n`);
1345
+ }
1346
+
1347
+ /** Every stored conversation, newest first; `default` always exists. */
1348
+ function chatSessions(): { id: string; messages: number; updated_at: string | null }[] {
1349
+ mkdirSync(CHAT_DIR, { recursive: true });
1350
+ if (!existsSync(chatFile('default'))) appendFileSync(chatFile('default'), '');
1351
+ const ids = readdirSync(CHAT_DIR)
1352
+ .filter((f) => f.endsWith('.jsonl'))
1353
+ .map((f) => f.replace(/\.jsonl$/, ''))
1354
+ .filter((id) => SAFE_SESSION.test(id));
1355
+ return ids
1356
+ .map((id) => {
1357
+ const fp = chatFile(id);
1358
+ let messages = 0;
1359
+ let updated_at: string | null = null;
1360
+ try {
1361
+ messages = read(fp).split('\n').filter((l) => l.trim().startsWith('{')).length;
1362
+ updated_at = statSync(fp).mtime.toISOString();
1363
+ } catch {
1364
+ /* empty */
1365
+ }
1366
+ return { id, messages, updated_at };
1367
+ })
1368
+ .sort((a, b) => (b.updated_at ?? '').localeCompare(a.updated_at ?? ''));
1369
+ }
1370
+
1371
+ function deleteChatSession(session: string): { ok: boolean } {
1372
+ try {
1373
+ unlinkSync(chatFile(session));
1374
+ } catch {
1375
+ /* already gone */
1376
+ }
1377
+ return { ok: true };
1378
+ }
1379
+
1380
+ /** A bounded recall from the well — Kaia drinks before every dispatch.
1381
+ * Relevance to the query first, recency as the tie-break. */
1382
+ function chatRecall_uncached(query: string, limit = 6): { text: string; count: number } {
1383
+ const lines = read(WELL).split('\n').filter((l) => l.trim().startsWith('{'));
1384
+ const terms = query
1385
+ .toLowerCase()
1386
+ .split(/[^a-z0-9]+/)
1387
+ .filter((w) => w.length > 3);
1388
+ const scored: { score: number; source: string; head: string; i: number }[] = [];
1389
+ for (const line of lines) {
1390
+ try {
1391
+ const e = JSON.parse(line) as { content?: string; source?: string };
1392
+ const content = String(e.content ?? '');
1393
+ const head = content.split('\n')[0].replace(/^#+\s*/, '').trim();
1394
+ if (!head) continue;
1395
+ const low = content.toLowerCase();
1396
+ let score = 0;
1397
+ for (const t of terms) if (low.includes(t)) score++;
1398
+ scored.push({ score, source: e.source ?? 'well', head, i: scored.length });
1399
+ } catch {
1400
+ /* skip malformed */
1401
+ }
1402
+ }
1403
+ scored.sort((a, b) => b.score - a.score || b.i - a.i);
1404
+ const picks = scored.slice(0, limit);
1405
+ return {
1406
+ text: picks.map((p) => `- (${p.source}) ${p.head.slice(0, 140)}`).join('\n'),
1407
+ count: picks.length,
1408
+ };
1409
+ }
1410
+
1411
+ function chatRecall(query: string, limit = 6): { text: string; count: number } {
1412
+ return memo(`chatRecall:${query}:${limit}`, 3000, () => chatRecall_uncached(query, limit));
1413
+ }
1414
+
1415
+ async function postChat(
1416
+ session: string,
1417
+ content: string,
1418
+ model?: string,
1419
+ agents?: string[],
1420
+ ): Promise<{ reply: ChatRow; live: boolean }> {
1421
+ mkdirSync(CHAT_DIR, { recursive: true });
1422
+ const sid = SAFE_SESSION.test(session) ? session : 'default';
1423
+ const user: ChatRow = { id: `u-${Date.now()}`, from: 'user', body: content, ts: new Date().toISOString() };
1424
+ appendChat(sid, user);
1425
+
1426
+ // Rolling window of the last 40 messages — the context Kaia keeps in view.
1427
+ const history = chatHistory(sid).slice(-CHAT_WINDOW);
1428
+ const recall = chatRecall(content);
1429
+ const lanes = agents?.length ? `\n\nFocused Eindri lanes: ${agents.join(', ')}.` : '';
1430
+ const messages = [
1431
+ {
1432
+ role: 'system',
1433
+ content: `${KAIA_SYSTEM}${lanes}\n\nWell recall:\n${recall.text || '(dry — answer from first principles, and say the well was dry)'}`,
1434
+ },
1435
+ ...history.map((m) => ({ role: m.from === 'user' ? 'user' : 'assistant', content: m.body })),
1436
+ ];
1437
+
1438
+ let body: string;
1439
+ let live = false;
1440
+ let usedModel: string | undefined;
1441
+ try {
1442
+ const out = await chatCompletion(messages, resolveChatTarget(model));
1443
+ body = out.body;
1444
+ usedModel = out.model;
1445
+ live = true;
1446
+ } catch (err) {
1447
+ body = `The well is local; no model backend answered (${(err as Error).message}). Recall ran; for a reply, raise a local model (llama-server on :8080) or the Bifrost bridge (\`bin/bifrost-bridge.sh\`).`;
1448
+ }
1449
+
1450
+ const kaia: ChatRow = {
1451
+ id: `k-${Date.now()}`,
1452
+ from: 'kaia',
1453
+ body,
1454
+ ts: new Date().toISOString(),
1455
+ recalling: recall.count > 0,
1456
+ ...(usedModel ? { model: usedModel } : {}),
1457
+ ...(agents?.length ? { agents } : {}),
1458
+ ...(live ? {} : { error: true }),
1459
+ };
1460
+ appendChat(sid, kaia);
1461
+ return { reply: kaia, live };
1462
+ }
1463
+
1464
+ /* ---- /api/prompts — the smithy's agent prompts, editable in the Forge ---- */
1465
+ const PROMPT_ROOT = join(ROOT, 'smidja/smidja_data/prompt_engineering');
1466
+
1467
+ interface PromptFile {
1468
+ agent: string;
1469
+ kind: 'system' | 'user';
1470
+ path: string;
1471
+ body: string;
1472
+ }
1473
+
1474
+ function prompts(): PromptFile[] {
1475
+ if (!existsSync(PROMPT_ROOT)) return [];
1476
+ const out: PromptFile[] = [];
1477
+ for (const agent of readdirSync(PROMPT_ROOT).sort()) {
1478
+ const dir = join(PROMPT_ROOT, agent);
1479
+ try {
1480
+ if (!statSync(dir).isDirectory()) continue;
1481
+ } catch {
1482
+ continue;
1483
+ }
1484
+ for (const kind of ['system', 'user'] as const) {
1485
+ const fp = join(dir, `${kind}.md`);
1486
+ if (existsSync(fp)) {
1487
+ out.push({ agent, kind, path: `smidja/smidja_data/prompt_engineering/${agent}/${kind}.md`, body: read(fp) });
1488
+ }
1489
+ }
1490
+ }
1491
+ return out;
1492
+ }
1493
+
1494
+ function savePrompt(agent: string, kind: string, body: string): { ok: boolean; path: string } {
1495
+ if (!/^[A-Za-z0-9._-]+$/.test(agent) || (kind !== 'system' && kind !== 'user')) {
1496
+ throw new Error('bad prompt id');
1497
+ }
1498
+ const fp = join(PROMPT_ROOT, agent, `${kind}.md`);
1499
+ mkdirSync(dirname(fp), { recursive: true });
1500
+ writeFileSync(fp, body);
1501
+ return { ok: true, path: `smidja/smidja_data/prompt_engineering/${agent}/${kind}.md` };
1502
+ }
1503
+
1504
+ /* ---- /api/workspaces + /api/setup — single-tenant workspaces ------------ */
1505
+ function workspaces(): { id: string; name: string; kind: string; company?: string; domains: string[] }[] {
1506
+ let regPath = join(ROOT, 'workspace/workspaces.yaml');
1507
+ const hoardReg = join(HOARD, 'identity/workspaces.yaml');
1508
+ try { read(hoardReg); regPath = hoardReg; } catch { /* fall back to the tracked scaffold */ }
1509
+ const txt = read(regPath);
1510
+ const out: { id: string; name: string; kind: string; company?: string; domains: string[] }[] = [];
1511
+ let cur: { id: string; name: string; kind: string; company?: string; domains: string[] } | null = null;
1512
+ for (const line of txt.split('\n')) {
1513
+ const idm = line.match(/^\s*-\s+id:\s*(\S+)/);
1514
+ if (idm) {
1515
+ if (cur) out.push(cur);
1516
+ cur = { id: idm[1], name: idm[1], kind: 'personal', domains: [] };
1517
+ continue;
1518
+ }
1519
+ if (!cur) continue;
1520
+ const nm = line.match(/^\s*name:\s*(.+)$/);
1521
+ if (nm) cur.name = nm[1].trim();
1522
+ const km = line.match(/^\s*kind:\s*(\S+)/);
1523
+ if (km) cur.kind = km[1];
1524
+ const cm = line.match(/^\s*company:\s*(\S+)/);
1525
+ if (cm) cur.company = cm[1];
1526
+ const dm = line.match(/^\s*domains:\s*\[(.*)\]/);
1527
+ if (dm) cur.domains = dm[1].split(',').map((s) => s.trim()).filter(Boolean);
1528
+ }
1529
+ if (cur) out.push(cur);
1530
+ return out.length ? out : [
1531
+ { id: 'work', name: 'Work', kind: 'work', domains: ['company', 'marketing', 'development', 'life'] },
1532
+ { id: 'personal', name: 'Personal', kind: 'personal', domains: ['me', 'life', 'development'] },
1533
+ ];
1534
+ }
1535
+
1536
+ async function setupStatus() {
1537
+ return parseToon(await runAsync(['bash', 'bin/ymir-install.sh', '--check'], 30000));
1538
+ }
1539
+ function setupRun() {
1540
+ return parseToon(run(['bash', 'bin/ymir-install.sh', '--skip-services']));
1541
+ }
1542
+ function workspaceProvision(name: string, kind: string, domains: string) {
1543
+ return parseToon(run(['bash', 'bin/workspace-provision.sh', name, '--kind', kind, '--domains', domains]));
1544
+ }
1545
+
1546
+ /* ---- tunnel gate (hardcoded for now) ------------------------------------- */
1547
+ // Temporary HTTP Basic Auth for the public tunnel. Override with
1548
+ // HLIDSKJALF_AUTH="user:pass"; replace with Heimdall (oauth2-proxy) later.
1549
+ const GATE_AUTH = process.env.HLIDSKJALF_AUTH ?? '';
1550
+ const SMIDJA_URL = process.env.SMIDJA_VIZ_URL ?? 'http://127.0.0.1:8437';
1551
+ // No default host: which hostname serves the smithy is the machine's fact.
1552
+ const SMIDJA_HOST = (process.env.SMIDJA_HOST ?? '').toLowerCase();
1553
+ const ODRERIR_HOST = (process.env.ODRERIR_HOST ?? '').toLowerCase();
1554
+ /** The one door: the primary host that carries Hlidskjalf's own login. Every app
1555
+ * host redirects its unauthenticated visitors here, with where they were going. */
1556
+ const PRIMARY_HOST = (process.env.YMIR_PRIMARY_HOST ?? '').toLowerCase();
1557
+ const ODRERIR_URL = process.env.ODRERIR_URL ?? 'http://127.0.0.1:4322';
1558
+ /** Every app host the gate fronts: unauthenticated visitors get the login, never
1559
+ * the app. Add a row when an app earns a public hostname. */
1560
+ const APP_HOSTS: Array<{ host: string; base: string; name: string }> = [
1561
+ { host: SMIDJA_HOST, base: SMIDJA_URL, name: 'Smíðja' },
1562
+ { host: ODRERIR_HOST, base: ODRERIR_URL, name: 'Óðrerir' },
1563
+ ].filter((a) => a.host !== '');
1564
+ const DIST = join(ROOT, 'apps/hlidskjalf/dist');
1565
+
1566
+ /** token → login. A session must know *who* it is, not merely that it exists. */
1567
+ const SESSIONS = new Map<string, string>();
1568
+
1569
+ /**
1570
+ * The token a request carries is the httpOnly `ymir_session` cookie — and nothing
1571
+ * else. There is deliberately NO header credential (no `X-Ymir-Session`/`Bearer`)
1572
+ * and NO token on disk: a persisted bearer is a credential at rest that any local
1573
+ * process could replay, and every app must be verified live by this gate rather
1574
+ * than read a file that claims a session. The gate is the one authority.
1575
+ */
1576
+ function cookieToken(req: Request): string {
1577
+ const c = req.headers.get('cookie') ?? '';
1578
+ const m = c.match(/(?:^|;\s*)ymir_session=([^;]+)/);
1579
+ return m ? m[1] : '';
1580
+ }
1581
+ function isAuthed(req: Request): boolean {
1582
+ if (isDesktopSeat(req)) return true;
1583
+ const t = cookieToken(req);
1584
+ return !!t && SESSIONS.has(t);
1585
+ }
1586
+
1587
+ /** The peer address per request: Bun's Request has no socket, so it is recorded
1588
+ * at the top of the handler. */
1589
+ const REQUEST_IP = new WeakMap<Request, string>();
1590
+
1591
+ /**
1592
+ * The desktop seat (Electron) is local and trusted, so it is never asked to log
1593
+ * in: the shell loads this app from 127.0.0.1 and its preload marks every
1594
+ * request with `x-ymir-surface: desktop`. The marker alone proves nothing — a
1595
+ * web caller could send the header too — so it is honoured ONLY when the request
1596
+ * genuinely arrives over loopback, which a remote client never does. The web
1597
+ * door keeps its lock.
1598
+ */
1599
+ function isDesktopSeat(req: Request): boolean {
1600
+ // A header where the client can set one; a query marker where it cannot —
1601
+ // EventSource (the live Runes stream) sends no custom headers.
1602
+ const marked =
1603
+ (req.headers.get('x-ymir-surface') ?? '') === 'desktop' ||
1604
+ new URL(req.url).searchParams.get('surface') === 'desktop';
1605
+ if (!marked) return false;
1606
+ const ip =
1607
+ REQUEST_IP.get(req) ??
1608
+ (req as Request & { socket?: { remoteAddress?: string } }).socket?.remoteAddress ??
1609
+ '';
1610
+ const bare = ip.startsWith('::ffff:') ? ip.slice('::ffff:'.length) : ip;
1611
+ return bare === '127.0.0.1' || bare === '::1';
1612
+ }
1613
+
1614
+ /** Who the gate says you are: the desktop seat wears the operator's own name. */
1615
+ function seatOf(req: Request): string {
1616
+ if (isDesktopSeat(req)) {
1617
+ const user = (GATE_AUTH || '').split(':')[0];
1618
+ return user || 'operator';
1619
+ }
1620
+ return loginOf(req);
1621
+ }
1622
+
1623
+ /** The login behind a request's session, if any. */
1624
+ function loginOf(req: Request): string | null {
1625
+ return SESSIONS.get(cookieToken(req)) ?? null;
1626
+ }
1627
+
1628
+ /** Begin a session and hand back the cookie. No copy is kept on disk. */
1629
+ function sessionResponse(login: string): Response {
1630
+ const token = crypto.randomUUID();
1631
+ SESSIONS.set(token, login);
1632
+ return new Response(JSON.stringify({ ok: true, login }), {
1633
+ headers: {
1634
+ 'content-type': 'application/json',
1635
+ 'set-cookie': `ymir_session=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=2592000`,
1636
+ },
1637
+ });
1638
+ }
1639
+
1640
+ /** The login screen for an app host — same gate, same credentials. */
1641
+ function appLoginPage(name = 'Smíðja'): Response {
1642
+ const html = `<!doctype html>
1643
+ <html lang="en">
1644
+ <head>
1645
+ <meta charset="utf-8" />
1646
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1647
+ <title>${name} — Sign in</title>
1648
+ <style>
1649
+ :root { color-scheme: dark; }
1650
+ * { box-sizing: border-box; }
1651
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; padding: 24px;
1652
+ background: radial-gradient(900px 500px at 50% -10%, rgba(226,232,240,.07), transparent 60%), #080c14;
1653
+ color: #e2e8f0; font: 15px/1.4 system-ui, sans-serif; }
1654
+ form { width: 100%; max-width: 380px; display: flex; flex-direction: column; gap: 14px;
1655
+ background: #0d131f; border: 1px solid #23304a; border-radius: 14px; padding: 26px;
1656
+ box-shadow: 0 24px 60px rgba(0,0,0,.5); }
1657
+ .brand { display: flex; align-items: center; gap: 12px; }
1658
+ .brand b { font-size: 20px; letter-spacing: .18em; }
1659
+ .brand small { display: block; font: 10px/1 ui-monospace, monospace; letter-spacing: .28em;
1660
+ text-transform: uppercase; color: #e2e8f0; margin-top: 4px; }
1661
+ h1 { font-size: 22px; margin: 0; }
1662
+ p { margin: 0; color: #94a3b8; font-size: 13px; }
1663
+ input { padding: 11px 12px; border-radius: 9px; border: 1px solid #23304a; background: #111a2b;
1664
+ color: #e2e8f0; font-size: 15px; }
1665
+ input:focus { outline: none; border-color: #e2e8f0; }
1666
+ button { padding: 11px 12px; border: none; border-radius: 9px; background: #e2e8f0; color: #06121f;
1667
+ font-weight: 700; font-size: 15px; cursor: pointer; }
1668
+ .err { color: #f87171; font-size: 12px; min-height: 14px; }
1669
+ </style>
1670
+ </head>
1671
+ <body>
1672
+ <form id="gate">
1673
+ <div class="brand">
1674
+ <svg width="34" height="34" viewBox="0 0 32 32" aria-hidden="true">
1675
+ <rect x="1" y="1" width="30" height="30" rx="7" fill="#0f172a" stroke="#1e293b"/>
1676
+ <g fill="#e2e8f0">
1677
+ <polygon points="7,6 10,6 16,11.5 22,6 25,6 17.5,13 17.5,20 14.5,20 14.5,13"/>
1678
+ <polygon points="6,21 26,21 25.4,24 6.6,24"/>
1679
+ <polygon points="8,25 24,25 23.3,27.5 8.7,27.5"/>
1680
+ </g>
1681
+ </svg>
1682
+ <span><b>${name.toUpperCase()}</b><small>Ymir</small></span>
1683
+ </div>
1684
+ <h1>Sign in</h1>
1685
+ <p>The gate is closed. Sign in, or enter an invite code to make your own account.</p>
1686
+ <input name="username" placeholder="username" autocomplete="username" autofocus />
1687
+ <input name="password" type="password" placeholder="••••••••" autocomplete="current-password" />
1688
+ <input name="invite" placeholder="invite code (only to create an account)" autocomplete="off" />
1689
+ <span class="err" id="err"></span>
1690
+ <button type="submit">Enter</button>
1691
+ </form>
1692
+ <script>
1693
+ const f = document.getElementById('gate');
1694
+ f.addEventListener('submit', async (e) => {
1695
+ e.preventDefault();
1696
+ const b = new FormData(f);
1697
+ const invite = String(b.get('invite') || '').trim();
1698
+ // An invite in the field means "make me an account"; without one this is an
1699
+ // ordinary sign-in. A bad or spent code is refused by the gate either way.
1700
+ const r = await fetch(invite ? '/api/register' : '/api/login', {
1701
+ method: 'POST',
1702
+ headers: { 'content-type': 'application/json' },
1703
+ body: JSON.stringify({ username: b.get('username'), password: b.get('password'), invite }),
1704
+ });
1705
+ if (r.ok) { location.reload(); return; }
1706
+ const said = await r.json().catch(() => ({}));
1707
+ document.getElementById('err').textContent = said.error || 'Wrong username or password';
1708
+ });
1709
+ </script>
1710
+ </body>
1711
+ </html>`;
1712
+ return new Response(html, {
1713
+ headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
1714
+ });
1715
+ }
1716
+
1717
+ /** Forward an authed Smíðja-host request to the visualizer on :8437. */
1718
+ async function proxyApp(req: Request, url: URL, base = SMIDJA_URL, label = 'smidja'): Promise<Response> {
1719
+ const headers = new Headers(req.headers);
1720
+ headers.delete('host');
1721
+ headers.delete('cookie');
1722
+ headers.delete('connection');
1723
+ const init: RequestInit = { method: req.method, headers, redirect: 'manual' };
1724
+ if (req.method !== 'GET' && req.method !== 'HEAD') init.body = await req.arrayBuffer();
1725
+ try {
1726
+ const up = await fetch(`${base}${url.pathname}${url.search}`, init);
1727
+ return new Response(up.body, { status: up.status, headers: up.headers });
1728
+ } catch (err) {
1729
+ return json({ error: `${label} upstream unreachable: ${(err as Error).message}` }, 502);
1730
+ }
1731
+ }
1732
+
1733
+ const STATIC_TYPES: Record<string, string> = {
1734
+ '.html': 'text/html; charset=utf-8',
1735
+ '.js': 'text/javascript',
1736
+ '.mjs': 'text/javascript',
1737
+ '.css': 'text/css',
1738
+ '.map': 'application/json',
1739
+ '.json': 'application/json',
1740
+ '.webmanifest': 'application/manifest+json',
1741
+ '.xml': 'application/xml',
1742
+ '.txt': 'text/plain; charset=utf-8',
1743
+ '.svg': 'image/svg+xml',
1744
+ '.png': 'image/png',
1745
+ '.jpg': 'image/jpeg',
1746
+ '.jpeg': 'image/jpeg',
1747
+ '.webp': 'image/webp',
1748
+ '.gif': 'image/gif',
1749
+ '.ico': 'image/x-icon',
1750
+ '.woff': 'font/woff',
1751
+ '.woff2': 'font/woff2',
1752
+ '.ttf': 'font/ttf',
1753
+ '.wasm': 'application/wasm',
1754
+ '.mp4': 'video/mp4',
1755
+ '.webm': 'video/webm',
1756
+ '.apk': 'application/vnd.android.package-archive',
1757
+ };
1758
+
1759
+ function serveStatic(pathname: string): Response {
1760
+ const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
1761
+ let file = join(DIST, rel);
1762
+ if (!file.startsWith(DIST) || !existsSync(file) || statSync(file).isDirectory()) file = join(DIST, 'index.html');
1763
+ const ext = extname(file).toLowerCase();
1764
+ const headers: Record<string, string> = { 'content-type': STATIC_TYPES[ext] ?? 'application/octet-stream' };
1765
+ if (ext === '.html') headers['cache-control'] = 'no-cache, no-store, must-revalidate';
1766
+ if (ext === '.apk') headers['content-disposition'] = 'attachment; filename="ymir.apk"';
1767
+ if (ext === '.apk' || ext === '.woff' || ext === '.woff2' || ext === '.ttf') headers['cache-control'] = 'public, max-age=86400';
1768
+ try {
1769
+ return new Response(readFileSync(file), { headers });
1770
+ } catch {
1771
+ return new Response('Not found', { status: 404 });
1772
+ }
1773
+ }
1774
+
1775
+ /* ---- server -------------------------------------------------------------- */
1776
+ // Sessions are in-memory only: a gate restart ends them, and nothing on disk can
1777
+ // revive one. That is the point — no bearer is kept at rest.
1778
+ const server = Bun.serve({
1779
+ port: PORT,
1780
+ async fetch(req, srv) {
1781
+ const url = new URL(req.url);
1782
+ const p = url.pathname;
1783
+ // Bun's Request carries no socket; the peer address comes from the server.
1784
+ // Recorded per request (a WeakMap, so concurrent requests cannot mix).
1785
+ REQUEST_IP.set(req, srv.requestIP(req)?.address ?? '');
1786
+ try {
1787
+ if (p === '/api/login' && req.method === 'POST') {
1788
+ const b = (await req.json().catch(() => ({}))) as { username?: string; password?: string };
1789
+ const user = b.username ?? '';
1790
+ const pass = b.password ?? '';
1791
+ // Two ways in: the operator's own credentials (HLIDSKJALF_AUTH), or an
1792
+ // account minted with an invite code.
1793
+ let login: string | null = null;
1794
+ if (GATE_AUTH && `${user}:${pass}` === GATE_AUTH) login = user;
1795
+ else if (user && pass) login = await verifyAccount(user, pass);
1796
+ if (login) return sessionResponse(login);
1797
+ return json({ error: 'invalid credentials' }, 401);
1798
+ }
1799
+ if (p === '/api/register' && req.method === 'POST') {
1800
+ const b = (await req.json().catch(() => ({}))) as {
1801
+ username?: string;
1802
+ password?: string;
1803
+ invite?: string;
1804
+ };
1805
+ const made = await register(b.username ?? '', b.password ?? '', b.invite ?? '');
1806
+ if (!made.ok) return json({ error: made.reason }, 403);
1807
+ return sessionResponse(made.login);
1808
+ }
1809
+ /* ---- GitHub: the same gate, a second door ---------------------------- *
1810
+ * Sign-in with GitHub never creates access on its own. It names a login,
1811
+ * and that login must already hold an account — accounts come from an
1812
+ * invite, so the door stays closed to strangers however they arrive. */
1813
+ if (p === '/api/auth/github' && req.method === 'GET') {
1814
+ const clientId = process.env.GITHUB_CLIENT_ID ?? '';
1815
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET ?? '';
1816
+ if (!clientId || !clientSecret) {
1817
+ return json(
1818
+ {
1819
+ error:
1820
+ 'GitHub sign-in is not configured. Set GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET in .env.local (a GitHub OAuth app) and restart the gate.',
1821
+ },
1822
+ 503,
1823
+ );
1824
+ }
1825
+ const state = crypto.randomUUID();
1826
+ const redirect = `${url.origin}/api/auth/github/callback`;
1827
+ const authorize =
1828
+ 'https://github.com/login/oauth/authorize' +
1829
+ `?client_id=${encodeURIComponent(clientId)}` +
1830
+ `&redirect_uri=${encodeURIComponent(redirect)}` +
1831
+ '&scope=read:user&state=' +
1832
+ encodeURIComponent(state);
1833
+ return new Response(JSON.stringify({ url: authorize }), {
1834
+ headers: {
1835
+ 'content-type': 'application/json',
1836
+ 'set-cookie': `ymir_oauth_state=${state}; Path=/; HttpOnly; SameSite=Lax; Max-Age=600`,
1837
+ },
1838
+ });
1839
+ }
1840
+ if (p === '/api/auth/github/callback' && req.method === 'GET') {
1841
+ const clientId = process.env.GITHUB_CLIENT_ID ?? '';
1842
+ const clientSecret = process.env.GITHUB_CLIENT_SECRET ?? '';
1843
+ const code = url.searchParams.get('code') ?? '';
1844
+ const state = url.searchParams.get('state') ?? '';
1845
+ const expected = (req.headers.get('cookie') ?? '').match(/ymir_oauth_state=([^;]+)/)?.[1] ?? '';
1846
+ if (!code) return json({ error: 'GitHub returned no code' }, 400);
1847
+ if (!state || state !== expected) return json({ error: 'OAuth state did not match' }, 400);
1848
+ const tokenRes = await fetch('https://github.com/login/oauth/access_token', {
1849
+ method: 'POST',
1850
+ headers: { 'content-type': 'application/json', accept: 'application/json' },
1851
+ body: JSON.stringify({ client_id: clientId, client_secret: clientSecret, code }),
1852
+ });
1853
+ const tok = (await tokenRes.json().catch(() => ({}))) as { access_token?: string };
1854
+ if (!tok.access_token) return json({ error: 'GitHub refused the code' }, 502);
1855
+ const userRes = await fetch('https://api.github.com/user', {
1856
+ headers: {
1857
+ authorization: `Bearer ${tok.access_token}`,
1858
+ accept: 'application/vnd.github+json',
1859
+ 'user-agent': 'ymir-gate',
1860
+ },
1861
+ });
1862
+ const gh = (await userRes.json().catch(() => ({}))) as { login?: string };
1863
+ const login = (gh.login ?? '').trim().toLowerCase();
1864
+ if (!login) return json({ error: 'GitHub returned no login' }, 502);
1865
+ if (!findAccount(login)) {
1866
+ return json(
1867
+ { error: `No account is held for the GitHub login '${login}'. Ask for an invite code, then register.` },
1868
+ 403,
1869
+ );
1870
+ }
1871
+ return sessionResponse(login);
1872
+ }
1873
+ if (p === '/api/invites' && req.method === 'GET' && isAuthed(req)) {
1874
+ // The operator's own view of who may still be let in.
1875
+ return json({
1876
+ invites: listInvites().map((i) => ({ ...i, code: i.code })),
1877
+ accounts: listAccounts(),
1878
+ });
1879
+ }
1880
+ if (p === '/api/session') {
1881
+ return json({
1882
+ authed: GATE_AUTH ? isAuthed(req) : true,
1883
+ login: seatOf(req),
1884
+ // The login surface shows "create an account" only while a live invite
1885
+ // exists — a newcomer with a code can enter, and nobody else can.
1886
+ registration: invitesOpen(),
1887
+ // GitHub is a second door only when an OAuth app is configured; the
1888
+ // login surface hides the button otherwise (it would 503).
1889
+ github: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET),
1890
+ });
1891
+ }
1892
+ if (p === '/api/logout' && req.method === 'POST') {
1893
+ SESSIONS.delete(cookieToken(req));
1894
+ return new Response(JSON.stringify({ ok: true }), {
1895
+ headers: { 'content-type': 'application/json', 'set-cookie': 'ymir_session=; Path=/; Max-Age=0' },
1896
+ });
1897
+ }
1898
+ // The Smíðja host rides the same gate: sign in here, then every request
1899
+ // is reverse-proxied to the visualizer on :8437.
1900
+ const host = (req.headers.get('host') ?? '').toLowerCase().split(':')[0];
1901
+ const app = APP_HOSTS.find((a) => a.host === host);
1902
+ if (app) {
1903
+ if (!isAuthed(req)) {
1904
+ // No dead login page: send them to the one login, remembering the app.
1905
+ if (p.startsWith('/api/')) return json({ error: 'unauthorized' }, 401);
1906
+ const scheme = (req.headers.get('x-forwarded-proto') ?? 'https').split(',')[0];
1907
+ const door = PRIMARY_HOST || host;
1908
+ return new Response(null, {
1909
+ status: 302,
1910
+ headers: { location: `${scheme}://${door}/?next=${encodeURIComponent(url.toString())}` },
1911
+ });
1912
+ }
1913
+ return proxyApp(req, url, app.base, app.name);
1914
+ }
1915
+ if (GATE_AUTH && p.startsWith('/api/') && !isAuthed(req)) return json({ error: 'unauthorized' }, 401);
1916
+ if (p === '/api/health') return json({ ok: true, root: ROOT, sessions: orders().length });
1917
+ if (p === '/api/usage') {
1918
+ // What the HARNESSES spent — opencode and pi — not only the smithy's runs.
1919
+ // Memoised for a minute: the aggregate is one bounded SQL statement over a
1920
+ // 37 GB store, and a panel must not pay for it on every poll.
1921
+ return json(await memoAsync('usage', 60_000, async () => {
1922
+ try {
1923
+ const { execFileSync } = await import('node:child_process');
1924
+ const script = new URL('../../../bin/hlidskjalf-usage.sh', import.meta.url).pathname;
1925
+ const out = execFileSync(script, ['--days', process.env.YMIR_USAGE_DAYS ?? '30'], { timeout: 60_000 }).toString().trim();
1926
+ return JSON.parse(out);
1927
+ } catch (e) {
1928
+ return { error: String(e).slice(0, 120) };
1929
+ }
1930
+ }));
1931
+ }
1932
+ if (p === '/api/worktrees') return json(await worktrees());
1933
+ if (p === '/api/me') return json({ login: loginOf(req) ?? 'operator', realm: 'work' });
1934
+ if (p === '/api/workspace') {
1935
+ const realm = url.searchParams.get('realm') ?? 'work';
1936
+ return json({ realm, path: workspaceRoot(realm) });
1937
+ }
1938
+ if (p === '/api/agents') {
1939
+ // WHO IS STANDING, not who could be. The connector reads herdr's live
1940
+ // pane list, so every opencode and pi session appears on the board with
1941
+ // its kind, state and task. The roster is the fallback, never the answer.
1942
+ try {
1943
+ const { execFileSync } = await import('node:child_process');
1944
+ const script = new URL('../../../bin/hlidskjalf-agents.sh', import.meta.url).pathname;
1945
+ const out = execFileSync(script, [], { timeout: 4000 }).toString().trim();
1946
+ if (out.startsWith('[')) {
1947
+ return new Response(out, { headers: { 'content-type': 'application/json' } });
1948
+ }
1949
+ } catch {
1950
+ // herdr absent or the connector failed: fall through to the roster
1951
+ }
1952
+ return json(agents());
1953
+ }
1954
+ if (p === '/api/tasks') return json(tasks());
1955
+ if (p === '/api/orders') return json({ open: orders().filter((o) => o.status !== 'COMPLETED').length, orders: orders() });
1956
+ if (p === '/api/runes') return json(runes());
1957
+ if (p === '/api/well') return json(await well(url.searchParams.get('q') ?? ''));
1958
+ if (p === '/api/well/episode') return json((await wellEpisode(url.searchParams.get('id') ?? '')) ?? { error: 'not found' });
1959
+ if (p === '/api/mimir/health') return json(await mimirHealth());
1960
+ if (p === '/api/processes') return json(await processes());
1961
+ // The reviews gate shells out twice (lint + compliance), each with a 60s cap,
1962
+ // so an unmemoised poll leaves Glitnir hanging for a minute and reads as a
1963
+ // dead surface. One answer per minute is what a human review board needs.
1964
+ if (p === '/api/reviews') return json(await memoAsync('reviews', 60_000, reviews));
1965
+ if (p === '/api/files') return json(files(url.searchParams.get('realm') ?? 'work'));
1966
+ if (p === '/api/file') return json(fileContent(url.searchParams.get('realm') ?? 'work', url.searchParams.get('path') ?? ''));
1967
+ if (p === '/api/skills') return json(skills());
1968
+ if (p === '/api/runtime') return json(await runtime());
1969
+ // A speed-start from the UI. It calls the same launcher the Omarchy key
1970
+ // bindings use, so the app is raised when it is already up and started
1971
+ // when it is not — one way to raise a Ymir window, not two.
1972
+ if (p === '/api/desktop' && req.method === 'POST') {
1973
+ const body = (await req.json().catch(() => ({}))) as { view?: string };
1974
+ const view = body?.view;
1975
+ if (view !== 'hlidskjalf' && view !== 'smidja' && view !== 'sessrumnir') {
1976
+ return json({ error: 'view must be hlidskjalf, smidja, or sessrumnir' }, 400);
1977
+ }
1978
+ // Hlidskjalf and Smíðja are views of the one Electron shell; Sessrúmnir
1979
+ // is its own app with its own launcher.
1980
+ const argv = view === 'sessrumnir'
1981
+ ? ['bash', join(ROOT, 'bin', 'sessrumnir.sh'), 'start']
1982
+ : ['bash', join(ROOT, 'scripts', 'electron.sh'), 'start', '--view', view];
1983
+ const proc = Bun.spawn(argv, { stdout: 'pipe', stderr: 'pipe', env: process.env });
1984
+ const out = await new Response(proc.stdout).text();
1985
+ await proc.exited;
1986
+ return json({ view, ok: proc.exitCode === 0, output: out.trim().slice(-400) });
1987
+ }
1988
+ if (p === '/api/cron') return json(await cron());
1989
+ if (p === '/api/loaders') return json(await loaders());
1990
+ if (p === '/api/checks') return json(await checks());
1991
+ if (p === '/api/smidja/health') return json(smidjaHealth());
1992
+ if (p === '/api/smidja/sessions') return json(smidjaSessions());
1993
+ if (p === '/api/smidja/decisions') return json(smidjaDecisions());
1994
+ if (p === '/api/smidja/stats') return json(smidjaStats());
1995
+ if (p.startsWith('/api/smidja/sessions/')) {
1996
+ const id = decodeURIComponent(p.slice('/api/smidja/sessions/'.length));
1997
+ const detail = smidjaSession(id);
1998
+ return detail ? json(detail) : json({ error: `no session ${id}` }, 404);
1999
+ }
2000
+ if (p === '/api/settings') return json(settings());
2001
+ if (p === '/api/workspaces' && req.method === 'GET') return json(workspaces());
2002
+ if (p === '/api/workspaces' && req.method === 'POST') {
2003
+ const b = (await req.json().catch(() => ({}))) as { name?: string; kind?: string; domains?: string[] | string };
2004
+ const name = (b.name ?? '').trim().toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/(^-|-$)/g, '');
2005
+ if (!name) return json({ error: 'workspace needs a name' }, 400);
2006
+ const kind = b.kind === 'work' ? 'work' : 'personal';
2007
+ const domains = Array.isArray(b.domains) ? b.domains.join(',') : b.domains ?? '';
2008
+ return json(workspaceProvision(name, kind, domains));
2009
+ }
2010
+ if (p === '/api/setup/status') return json(await setupStatus());
2011
+ if (p === '/api/setup/run' && req.method === 'POST') return json(setupRun());
2012
+ if (p === '/api/prompts' && req.method === 'GET') return json(prompts());
2013
+ if (p === '/api/prompts' && req.method === 'POST') {
2014
+ const body = (await req.json().catch(() => ({}))) as { agent?: string; kind?: string; body?: string };
2015
+ if (!body.agent || !body.kind) return json({ error: 'prompt needs agent + kind' }, 400);
2016
+ return json(savePrompt(body.agent, body.kind, body.body ?? ''));
2017
+ }
2018
+ if (p === '/api/stream') return stream();
2019
+ if (p === '/api/chat/models') return json(await chatModels());
2020
+ if (p === '/api/chat/sessions') return json(chatSessions());
2021
+ if (p === '/api/chat/history') return json(chatHistory(url.searchParams.get('session') ?? 'default'));
2022
+ if (p === '/api/chat/session' && req.method === 'DELETE') {
2023
+ return json(deleteChatSession(url.searchParams.get('session') ?? 'default'));
2024
+ }
2025
+ if (p === '/api/chat' && req.method === 'POST') {
2026
+ const body = (await req.json().catch(() => ({}))) as {
2027
+ session?: string;
2028
+ content?: string;
2029
+ model?: string;
2030
+ agents?: string[];
2031
+ };
2032
+ const content = (body.content ?? '').trim();
2033
+ if (!content) return json({ error: 'chat needs content' }, 400);
2034
+ return json(await postChat(body.session ?? 'default', content, body.model, body.agents));
2035
+ }
2036
+ if (p.startsWith('/api/')) return json({ error: `no route ${p}` }, 404);
2037
+ return serveStatic(p);
2038
+ } catch (err) {
2039
+ return json({ error: (err as Error).message }, 500);
2040
+ }
2041
+ },
2042
+ });
2043
+
2044
+ console.log(`[hlidskjalf] gate api http://127.0.0.1:${server.port}`);
2045
+ console.log(`[hlidskjalf] runtime ${ROOT}`);