@zerwiz/ymir 0.1.34 → 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 (929) 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/package.json +15 -7
  857. package/scripts/electron.sh +6 -2
  858. package/.agents/bus/messages.json +0 -1
  859. package/.agents/config/agents.machine.example.yaml +0 -16
  860. package/.pi/mcp.json +0 -16
  861. package/assets/reference/state/.branch-eligible-owner +0 -4
  862. package/assets/reference/state/.branch-mirror-cursor +0 -1
  863. package/assets/reference/state/.branch-outcomes-cursor +0 -1
  864. package/assets/reference/state/.branch-session +0 -1
  865. package/assets/reference/state/.count-default_wF_p5 +0 -1
  866. package/assets/reference/state/.count-default_wF_p6 +0 -1
  867. package/assets/reference/state/.count-default_wR_p2 +0 -1
  868. package/assets/reference/state/.count-default_wS_p2 +0 -1
  869. package/assets/reference/state/.count-firstmate_fm-server-knowledge +0 -1
  870. package/assets/reference/state/.hash-default_wF_p5 +0 -1
  871. package/assets/reference/state/.hash-default_wF_p6 +0 -1
  872. package/assets/reference/state/.hash-default_wR_p2 +0 -1
  873. package/assets/reference/state/.hash-default_wS_p2 +0 -1
  874. package/assets/reference/state/.hash-firstmate_fm-server-knowledge +0 -1
  875. package/assets/reference/state/.heartbeat-streak +0 -1
  876. package/assets/reference/state/.inactive-outcome-reconcile +0 -2
  877. package/assets/reference/state/.lock +0 -1
  878. package/assets/reference/state/.pi-branch-extension-loaded +0 -1
  879. package/assets/reference/state/.pi-turnend-extension-loaded +0 -2
  880. package/assets/reference/state/.pi-watch-extension-loaded +0 -2
  881. package/assets/reference/state/.seen-server-knowledge_turn-ended +0 -1
  882. package/assets/reference/state/.session-start-agents-baseline +0 -2
  883. package/assets/reference/state/.session-start-complete +0 -1
  884. package/assets/reference/state/.stale-default_wF_p5 +0 -1
  885. package/assets/reference/state/.stale-default_wR_p2 +0 -1
  886. package/assets/reference/state/.stale-firstmate_fm-server-knowledge +0 -1
  887. package/assets/reference/state/.stale-since-default_wF_p5 +0 -1
  888. package/assets/reference/state/.stale-since-default_wR_p2 +0 -1
  889. package/assets/reference/state/.stale-since-firstmate_fm-server-knowledge +0 -1
  890. package/assets/reference/state/.startup-network.delivered +0 -1
  891. package/assets/reference/state/.startup-network.report +0 -1
  892. package/assets/reference/state/.startup-network.status +0 -10
  893. package/assets/reference/state/.startup-network.timings +0 -6
  894. package/assets/reference/state/.trace-context-effective +0 -1
  895. package/assets/reference/state/.wake-queue +0 -0
  896. package/assets/reference/state/.wake-queue.seq +0 -1
  897. package/assets/reference/state/.wake-rows.consume.BcqXSq +0 -0
  898. package/assets/reference/state/.wake-rows.consume.NDZZei +0 -0
  899. package/assets/reference/state/.wake-rows.consume.VGkhYm +0 -0
  900. package/assets/reference/state/.wake-rows.consume.tR2rTy +0 -0
  901. package/assets/reference/state/.watch-arm-output.3581p6 +0 -0
  902. package/assets/reference/state/.watch-arm-output.3ciDNP +0 -0
  903. package/assets/reference/state/.watch-arm-output.SksR6k +0 -0
  904. package/assets/reference/state/.watch-cycle-exits.log +0 -22
  905. package/assets/reference/state/.watch-deliveries.log +0 -19
  906. package/assets/reference/state/.watch-triage.log +0 -38
  907. package/assets/reference/state/.watch.lock/fm-home +0 -1
  908. package/assets/reference/state/.watch.lock/pid +0 -1
  909. package/assets/reference/state/.watch.lock/pid-identity +0 -1
  910. package/assets/reference/state/.watch.lock/watcher-path +0 -1
  911. package/assets/reference/state/.watch.lock.owner.qqbxmS/fm-home +0 -1
  912. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid +0 -1
  913. package/assets/reference/state/.watch.lock.owner.qqbxmS/pid-identity +0 -1
  914. package/assets/reference/state/.watch.lock.owner.qqbxmS/watcher-path +0 -1
  915. package/assets/reference/state/.watcher-down +0 -1
  916. package/assets/reference/state/branch-outcomes.jsonl +0 -1
  917. package/assets/reference/state/branch-session/2026-09-04T16-49-38-671Z_01a06d53-6b6f-7619-930d-e6dfd9a4a29e.jsonl +0 -211
  918. package/assets/reference/state/home-summary.json +0 -42
  919. package/assets/reference/state/server-knowledge.busy-gen +0 -1
  920. package/assets/reference/state/server-knowledge.busy-state +0 -1
  921. package/assets/reference/state/server-knowledge.herdr-presentation +0 -12
  922. package/assets/reference/state/server-knowledge.meta +0 -18
  923. package/assets/reference/state/server-knowledge.pi-ext.ts +0 -26
  924. package/assets/reference/state/server-knowledge.turn-ended +0 -0
  925. /package/{assets/reference/state/.last-check → apps/hlidskjalf/src/app/.gitkeep} +0 -0
  926. /package/{assets/reference/state/.last-heartbeat → apps/hlidskjalf/src/components/.gitkeep} +0 -0
  927. /package/{assets/reference/state/.last-watcher-beat → apps/hlidskjalf/src/services/.gitkeep} +0 -0
  928. /package/{assets/reference/state/.main-eligible-rows.tmp.p2HMKo → apps/smidja/smidja_modules/__init__.py} +0 -0
  929. /package/{assets/reference/state/.main-eligible-rows.tmp.Exxm82 → apps/smidja-factory/templates/smidja/smidja_modules/__init__.py} +0 -0
@@ -0,0 +1,1319 @@
1
+ import { ChildProcess, SpawnOptions, spawn, spawnSync } from 'child_process'
2
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'fs'
3
+ import { join } from 'path'
4
+ import { EventEmitter } from 'events'
5
+ import { StringDecoder } from 'string_decoder'
6
+ import type {
7
+ AgentEngineKind,
8
+ PiRpcEvent,
9
+ PiStartOptions,
10
+ PiProcessStatus,
11
+ PiStartupPhase,
12
+ PiStatus,
13
+ PiResponseEvent,
14
+ AgentInstallation,
15
+ } from '../shared/ipc-contracts'
16
+ import type { CaptureOptions, PiEngine, PiResolution, PiStartFailure, ResolutionDeps } from './pi-binary-resolution'
17
+ import {
18
+ describePiStartFailure,
19
+ isOmpExecutable,
20
+ normalizeOverride,
21
+ resolvePiBinary,
22
+ whichInPath,
23
+ } from './pi-binary-resolution'
24
+ import { escapeCmdSpawn } from './cmd-escape'
25
+ import { appLog } from './app-log'
26
+ import { getGuiDataPath } from './app-data-paths'
27
+ import { t, tEnglish } from '../shared/i18n'
28
+
29
+ /**
30
+ * Manages a Pi RPC child process.
31
+ *
32
+ * Responsibilities:
33
+ * - Spawn/kill Pi in --mode rpc
34
+ * - Parse JSONL from stdout (LF-delimited, no Unicode line separators)
35
+ * - Route events to subscribers
36
+ * - Correlate request/response via id field
37
+ * - Handle extension UI request/response sub-protocol
38
+ */
39
+
40
+ const JSONL_NEWLINE = '\n'
41
+ const RPC_MODE = 'rpc'
42
+ const NO_SESSION_FLAG = '--no-session'
43
+ const MODE_FLAG = '--mode'
44
+ const PROVIDER_FLAG = '--provider'
45
+ const MODEL_FLAG = '--model'
46
+ const SESSION_FLAG = '--session'
47
+ const FORK_FLAG = '--fork'
48
+ const CONTINUE_FLAG = '--continue'
49
+ const IS_WINDOWS = process.platform === 'win32'
50
+ const MS_PER_SECOND = 1_000
51
+ // The startup deadline is two-staged (issue #58). Stage 1: while Pi has
52
+ // printed nothing at all, give up quickly — a dead spawn or a broken stdio
53
+ // pipe stays silent forever. Stage 2: once stdout has shown life the process
54
+ // is provably alive and piped, so the deadline stretches: the engine runs
55
+ // every extension session_start hook BEFORE it reads stdin, and a hook that
56
+ // calls a local model server can sit through tens of seconds of prompt
57
+ // prefill while our readiness probe waits unread.
58
+ const STARTUP_SILENCE_TIMEOUT_MS = 20_000
59
+ const STARTUP_ENGINE_BUSY_TIMEOUT_MS = 120_000
60
+
61
+ /** Startup deadline caps, injectable so tests do not wait out real minutes. */
62
+ export interface PiStartupTimeouts {
63
+ /** Max wait for the first byte of Pi stdout. */
64
+ silenceMs: number
65
+ /** Max wait for readiness once stdout has shown life. */
66
+ engineBusyMs: number
67
+ }
68
+ // Spawn attempts per start(): the initial try plus one retry, used ONLY when Pi
69
+ // crashes before becoming ready (spawn error / early exit) — a transient hiccup
70
+ // (AV lock, momentary ENOENT) often clears on a second spawn. A no-response
71
+ // timeout is not retried: respawning would just burn another full timeout.
72
+ const STARTUP_MAX_ATTEMPTS = 2
73
+ // Pi's RPC mode emits nothing on connect — it only replies to requests. So
74
+ // instead of a blind settle wait, we send a cheap read-only probe after spawn
75
+ // and treat its CORRELATED response (matched by STARTUP_PROBE_ID in handleLine,
76
+ // success OR error) as "ready". Keying off the correlated response — not merely
77
+ // the first stdout byte — confirms the request→response loop works and stays
78
+ // robust even if the probe command is renamed (Pi echoes our id on an "unknown
79
+ // command" error too). The probe is resent on this interval in case the first
80
+ // write raced Pi's stdin reader; the two-stage startup deadline (see
81
+ // PiStartupTimeouts above) bounds the wait.
82
+ const STARTUP_PROBE_ID = '__startup_probe__'
83
+ // get_state is the cheapest liveness command: a handful of in-memory session
84
+ // field reads — no I/O, no model/provider calls, O(1). (get_session_stats is
85
+ // O(messages) and get_available_models filters the model list.)
86
+ const STARTUP_PROBE_COMMAND = 'get_state'
87
+ const STARTUP_PROBE_INTERVAL_MS = 750
88
+ const FORCE_KILL_TIMEOUT_MS = 3_000
89
+ /** Bound on the one `ps` call used to snapshot a child's descendants at kill time. */
90
+ const PROCESS_TREE_TIMEOUT_MS = 2_000
91
+ /**
92
+ * Fallbacks only. The engine advertises its real limits in the `ready` frame
93
+ * (`maxFrameBytes`, `maxReassembledFrameBytes`); configureLimits() adopts those
94
+ * so the receiver never enforces a stricter rule than the sender obeys. These
95
+ * defaults match what OMP 18 advertises and are used until `ready` arrives, and
96
+ * for the original Pi RPC implementation, which sends no ready frame.
97
+ */
98
+ const RPC_DEFAULT_MAX_REASSEMBLED_BYTES = 64 * 1024 * 1024
99
+ const RPC_DEFAULT_MAX_CHUNK_PAYLOAD_BYTES = 1024 * 1024
100
+ /** Chunking only exists for frames a single line cannot carry. */
101
+ const RPC_MIN_CHUNK_COUNT = 2
102
+ const RPC_MAX_CHUNK_ID_LENGTH = 128
103
+ /**
104
+ * Standard base64 with the padding the sender's encoder emits. The alphabet
105
+ * and the padding shape are enforced here; the round-trip re-encode in push()
106
+ * additionally rejects non-canonical trailing bits (`QR==` and `QQ==` both
107
+ * decode to one byte, only the latter is what an encoder produces), so one
108
+ * payload has exactly one legal wire spelling. Base64url and unpadded input
109
+ * are rejected by this pattern, not by the round-trip — the two checks state
110
+ * the same policy and are loosened or kept together.
111
+ */
112
+ const RPC_CANONICAL_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
113
+
114
+ interface PendingRpcChunks {
115
+ chunkId: string
116
+ count: number
117
+ byteLength: number
118
+ nextIndex: number
119
+ chunks: Buffer[]
120
+ receivedBytes: number
121
+ }
122
+
123
+ function isRpcChunkFrame(value: unknown): value is Record<string, unknown> {
124
+ return typeof value === 'object' && value !== null && (value as { type?: unknown }).type === 'rpc_chunk'
125
+ }
126
+
127
+ /** Minimal lossless decoder for OMP RPC protocol v2 physical frames. */
128
+ export class RpcFrameDecoder {
129
+ private pending: PendingRpcChunks | null = null
130
+ private maxReassembledBytes = RPC_DEFAULT_MAX_REASSEMBLED_BYTES
131
+ private maxChunkPayloadBytes = RPC_DEFAULT_MAX_CHUNK_PAYLOAD_BYTES
132
+
133
+ hasPending(): boolean {
134
+ return this.pending !== null
135
+ }
136
+
137
+ reset(): void {
138
+ this.pending = null
139
+ }
140
+
141
+ /**
142
+ * Adopt the limits the engine advertised in its `ready` frame. A sender only
143
+ * chunks what exceeds its own single-frame cap, so that cap is also the
144
+ * largest payload any one chunk can carry. Ignores absent or nonsensical
145
+ * values so a malformed ready frame cannot widen or break the decoder.
146
+ */
147
+ configureLimits(limits: { maxFrameBytes?: unknown; maxReassembledFrameBytes?: unknown }): void {
148
+ const frame = limits.maxFrameBytes
149
+ const reassembled = limits.maxReassembledFrameBytes
150
+ if (typeof frame === 'number' && Number.isSafeInteger(frame) && frame > 0) {
151
+ this.maxChunkPayloadBytes = frame
152
+ }
153
+ if (typeof reassembled === 'number' && Number.isSafeInteger(reassembled) && reassembled > 0) {
154
+ this.maxReassembledBytes = reassembled
155
+ }
156
+ }
157
+
158
+ push(value: Record<string, unknown>): object | undefined {
159
+ const chunkId = value.chunkId
160
+ const index = typeof value.index === 'number' ? value.index : Number.NaN
161
+ const count = typeof value.count === 'number' ? value.count : Number.NaN
162
+ const byteLength = typeof value.byteLength === 'number' ? value.byteLength : Number.NaN
163
+ const data = value.data
164
+ // `byteLength` is bounded above only. How the sender splits a frame is its
165
+ // own business, so there is no floor: a floor of one whole frame would make
166
+ // every short chunk sequence undecodable if the sender ever lowered its
167
+ // frame cap. A wrong declared length is still caught while reassembling,
168
+ // against the bytes actually received.
169
+ const maxChunkCount = Math.ceil(this.maxReassembledBytes / this.maxChunkPayloadBytes)
170
+ if (
171
+ typeof chunkId !== 'string' || chunkId.length === 0 || chunkId.length > RPC_MAX_CHUNK_ID_LENGTH ||
172
+ !Number.isSafeInteger(index) || !Number.isSafeInteger(count) || !Number.isSafeInteger(byteLength) ||
173
+ index < 0 || count < RPC_MIN_CHUNK_COUNT || count > maxChunkCount || index >= count ||
174
+ byteLength <= 0 || byteLength > this.maxReassembledBytes ||
175
+ typeof data !== 'string' || data.length === 0 || !RPC_CANONICAL_BASE64.test(data)
176
+ ) {
177
+ throw new Error('invalid rpc chunk metadata')
178
+ }
179
+ const chunkIndex = index as number
180
+ const chunkCount = count as number
181
+ const declaredByteLength = byteLength as number
182
+ const bytes = Buffer.from(data, 'base64')
183
+ if (bytes.toString('base64') !== data || bytes.byteLength > this.maxChunkPayloadBytes) {
184
+ throw new Error('invalid rpc chunk data')
185
+ }
186
+
187
+ if (!this.pending) {
188
+ if (chunkIndex !== 0) throw new Error('rpc chunk sequence must start at index 0')
189
+ this.pending = { chunkId, count: chunkCount, byteLength: declaredByteLength, nextIndex: 0, chunks: [], receivedBytes: 0 }
190
+ }
191
+ const pending = this.pending!
192
+ if (
193
+ pending.chunkId !== chunkId || pending.count !== chunkCount || pending.byteLength !== declaredByteLength ||
194
+ pending.nextIndex !== chunkIndex
195
+ ) {
196
+ throw new Error('rpc chunk sequence mismatch')
197
+ }
198
+ pending.chunks.push(bytes)
199
+ pending.receivedBytes += bytes.byteLength
200
+ pending.nextIndex++
201
+ if (pending.receivedBytes > pending.byteLength) throw new Error('rpc chunk sequence exceeds declared length')
202
+ if (pending.nextIndex < pending.count) return undefined
203
+ if (pending.receivedBytes !== pending.byteLength) throw new Error('rpc chunk sequence length mismatch')
204
+
205
+ this.pending = null
206
+ const decoded = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(pending.chunks))
207
+ const frame = JSON.parse(decoded) as unknown
208
+ if (typeof frame !== 'object' || frame === null || Array.isArray(frame)) throw new Error('rpc frame must be an object')
209
+ return frame as object
210
+ }
211
+ }
212
+
213
+ // Real filesystem/process access for the resolver in pi-binary-resolution.ts.
214
+ // Kept in one object so the search order stays testable against a fake.
215
+ const RESOLUTION_DEPS: ResolutionDeps = {
216
+ isWindows: IS_WINDOWS,
217
+ env: process.env,
218
+ exists: (path) => existsSync(path),
219
+ isDirectory: (path) => {
220
+ try {
221
+ return statSync(path).isDirectory()
222
+ } catch (err) {
223
+ // ENOENT/EACCES/ELOOP all mean "not a directory we can use".
224
+ if (isFsAccessError(err)) return false
225
+ throw err
226
+ }
227
+ },
228
+ listDir: (path) => {
229
+ try {
230
+ return readdirSync(path)
231
+ } catch (err) {
232
+ if (isFsAccessError(err)) return []
233
+ throw err
234
+ }
235
+ },
236
+ capture: (command, args, options) => runCapture(command, args, options),
237
+ }
238
+
239
+ const FS_ACCESS_ERROR_CODES = new Set(['ENOENT', 'ENOTDIR', 'EACCES', 'EPERM', 'ELOOP'])
240
+
241
+ function isFsAccessError(err: unknown): boolean {
242
+ const code = (err as NodeJS.ErrnoException | null)?.code
243
+ return typeof code === 'string' && FS_ACCESS_ERROR_CODES.has(code)
244
+ }
245
+
246
+ /**
247
+ * Run a probe command and return its stdout, or null if it could not run.
248
+ * stdin is closed immediately so an interactive login shell never blocks.
249
+ */
250
+ function runCapture(command: string, args: string[], options: CaptureOptions): string | null {
251
+ try {
252
+ const result = spawnSync(command, args, {
253
+ encoding: 'utf-8',
254
+ shell: options.shell,
255
+ timeout: options.timeoutMs,
256
+ input: '',
257
+ env: { ...process.env, PATH: options.pathEnv },
258
+ })
259
+ if (result.status !== 0 || !result.stdout) return null
260
+ return result.stdout
261
+ } catch (err) {
262
+ // Spawn-level failure (missing binary, EACCES) — treat as "no answer".
263
+ if (isFsAccessError(err)) return null
264
+ throw err
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Find a Node binary to run the Pi .js script with. Searches NODE env,
270
+ * npm_node_execpath (set when running under npm), Electron's own process,
271
+ * common install paths, and PATH.
272
+ */
273
+ function findNodeBinary(): string {
274
+ if (process.env.NODE && existsSync(process.env.NODE)) return process.env.NODE
275
+ if (process.env.npm_node_execpath && existsSync(process.env.npm_node_execpath)) {
276
+ return process.env.npm_node_execpath
277
+ }
278
+
279
+ if (IS_WINDOWS) {
280
+ const programFiles = process.env.ProgramFiles ?? 'C:\\Program Files'
281
+ const programFilesX86 = process.env['ProgramFiles(x86)'] ?? 'C:\\Program Files (x86)'
282
+ const localAppData = process.env.LOCALAPPDATA ?? ''
283
+ const candidates = [
284
+ // Pi's install.ps1 puts an auto-installed Node under
285
+ // %LOCALAPPDATA%\pi-node\current\node.exe. Check the symlinked
286
+ // 'current' path first; fall back to the bare pi-node dir for
287
+ // older layouts.
288
+ localAppData ? join(localAppData, 'pi-node', 'current', 'node.exe') : '',
289
+ localAppData ? join(localAppData, 'pi-node', 'node.exe') : '',
290
+ join(programFiles, 'nodejs', 'node.exe'),
291
+ join(programFilesX86, 'nodejs', 'node.exe'),
292
+ localAppData ? join(localAppData, 'fnm_multishells', 'node.exe') : '',
293
+ ].filter(Boolean)
294
+ for (const c of candidates) if (existsSync(c)) return c
295
+ const fromPath = whichInPath(RESOLUTION_DEPS, 'node', process.env.PATH ?? '')
296
+ if (fromPath) return fromPath
297
+ return 'node.exe'
298
+ }
299
+
300
+ for (const c of ['/usr/bin/node', '/usr/local/bin/node', '/opt/homebrew/bin/node']) {
301
+ if (existsSync(c)) return c
302
+ }
303
+ const fromPath = whichInPath(RESOLUTION_DEPS, 'node', process.env.PATH ?? '')
304
+ if (fromPath) return fromPath
305
+ return 'node'
306
+ }
307
+
308
+ /**
309
+ * Resolved Pi invocation. `found` is false when nothing was located and
310
+ * `script` is only a hopeful fallback; `failureReason` then explains why
311
+ * (render it with describePiStartFailure).
312
+ */
313
+ export interface PiCli {
314
+ /** The selected engine. OMP deliberately keeps the Pi-compatible RPC path. */
315
+ kind?: 'pi' | 'omp'
316
+ script: string
317
+ node: string
318
+ useNode: boolean
319
+ needsShell: boolean
320
+ found: boolean
321
+ nodeFound: boolean
322
+ failureReason: PiStartFailure | null
323
+ }
324
+
325
+ // Resolution is lazy and cached rather than computed at import time, because
326
+ // it depends on the executable path and explicit engine setting, which are only
327
+ // readable once app settings have loaded.
328
+ let configuredOverride: string | null = null
329
+ let configuredEngine: PiEngine = 'auto'
330
+ let cachedResolution: PiResolution | null = null
331
+ /**
332
+ * Auto-detected resolution per engine, for starts that must run a specific
333
+ * engine rather than the configured one. Cached and invalidated exactly like
334
+ * `cachedResolution` — both read the same filesystem state.
335
+ */
336
+ const engineResolutions = new Map<AgentEngineKind, PiResolution>()
337
+ let cachedNodeBinary: string | null = null
338
+ let detectedInstallationsCache: { at: number; value: AgentInstallation[] } | null = null
339
+ /** How long a detection result is served without re-walking the filesystem. */
340
+ const INSTALLATION_CACHE_TTL_MS = 30_000
341
+ /**
342
+ * Apply the selected executable and engine. The engine is persisted separately
343
+ * so a custom path named something other than `omp` cannot silently change the
344
+ * protocol/tool surface.
345
+ */
346
+ export function setPiExecutableOverride(raw: string | undefined | null, engine: PiEngine = 'auto'): void {
347
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? ''
348
+ const next = normalizeOverride(raw, home)
349
+ if (next === configuredOverride && engine === configuredEngine && cachedResolution) return
350
+ configuredOverride = next
351
+ configuredEngine = engine
352
+ cachedResolution = null
353
+ engineResolutions.clear()
354
+ cachedNodeBinary = null
355
+ }
356
+
357
+
358
+ function getResolution(): PiResolution {
359
+ if (cachedResolution) return cachedResolution
360
+ const resolution = resolvePiBinary(RESOLUTION_DEPS, configuredOverride, configuredEngine)
361
+ // Adopt the login shell's PATH process-wide so Pi itself — and every helper
362
+ // we spawn — can find node, npm and the tools the user's shell exposes.
363
+ if (resolution.pathEnv && resolution.pathEnv !== process.env.PATH) {
364
+ process.env.PATH = resolution.pathEnv
365
+ }
366
+ cachedResolution = resolution
367
+ logResolution(resolution)
368
+ return resolution
369
+ }
370
+
371
+ /**
372
+ * Full resolution details (source, PATH, rejected override) for the
373
+ * diagnostics report. First call may run the login-shell and npm probes.
374
+ */
375
+ export function getPiResolution(): PiResolution {
376
+ return getResolution()
377
+ }
378
+
379
+ /**
380
+ * Detect the first usable installation for each supported engine.
381
+ *
382
+ * `force` skips the cache. A rescan is the user telling us the filesystem
383
+ * changed since the last look — serving a cached answer there turns the
384
+ * Rescan button into a spinner that can never report a new install.
385
+ */
386
+ export function detectPiInstallations(force = false): AgentInstallation[] {
387
+ const cached = detectedInstallationsCache
388
+ if (!force && cached && Date.now() - cached.at < INSTALLATION_CACHE_TTL_MS) {
389
+ return cached.value.map((item) => ({ ...item }))
390
+ }
391
+ const candidates: Array<{ kind: AgentInstallation['kind']; resolution: PiResolution }> = [
392
+ { kind: 'pi', resolution: resolvePiBinary(RESOLUTION_DEPS, null, 'pi') },
393
+ { kind: 'omp', resolution: resolvePiBinary(RESOLUTION_DEPS, null, 'omp') },
394
+ ]
395
+ const seen = new Set<string>()
396
+ const value = candidates.flatMap(({ kind, resolution }) => {
397
+ if (!resolution.found) return []
398
+ const key = resolution.script.toLowerCase()
399
+ if (seen.has(key)) return []
400
+ seen.add(key)
401
+ return [{ kind, path: resolution.script, source: resolution.source }]
402
+ })
403
+ detectedInstallationsCache = { at: Date.now(), value }
404
+ return value.map((item) => ({ ...item }))
405
+ }
406
+
407
+ function logResolution(resolution: PiResolution): void {
408
+ const node = getNodeBinary()
409
+ console.log('─── Pi binary resolution ────────────────────────────')
410
+ console.log('[Pi] Script :', resolution.script, resolution.found ? '(exists)' : '(MISSING)')
411
+ console.log('[Pi] Source :', resolution.source)
412
+ if (resolution.rejectedOverride) {
413
+ console.warn('[Pi] Configured path ignored (does not exist):', resolution.rejectedOverride)
414
+ }
415
+ console.log('[Pi] Uses node :', resolution.useNode)
416
+ console.log(
417
+ '[Pi] Node binary :',
418
+ node,
419
+ resolution.useNode ? (existsSync(node) ? '(exists)' : '(MISSING)') : '(unused)'
420
+ )
421
+ console.log('[Pi] Needs shell :', resolution.needsShell)
422
+ console.log('─────────────────────────────────────────────────────')
423
+ }
424
+
425
+ function getNodeBinary(): string {
426
+ if (cachedNodeBinary === null) cachedNodeBinary = findNodeBinary()
427
+ return cachedNodeBinary
428
+ }
429
+
430
+ /** Pair a resolution with the Node binary and the failure spawn needs. */
431
+ function toPiCli(resolution: PiResolution, kind: AgentEngineKind): PiCli {
432
+ const node = getNodeBinary()
433
+ const nodeFound = !resolution.useNode || existsSync(node)
434
+ let failureReason: PiStartFailure | null = null
435
+ if (!resolution.found) {
436
+ failureReason = { kind: 'pi-not-found', resolution }
437
+ } else if (!nodeFound) {
438
+ failureReason = { kind: 'node-not-found', node }
439
+ }
440
+ return {
441
+ kind,
442
+ script: resolution.script,
443
+ node,
444
+ useNode: resolution.useNode,
445
+ needsShell: resolution.needsShell,
446
+ found: resolution.found,
447
+ nodeFound,
448
+ failureReason,
449
+ }
450
+ }
451
+
452
+ /**
453
+ * The resolved Pi invocation. Also exported for ipc-handlers, which runs
454
+ * `pi install/remove/update` with the same binary — Electron's own PATH won't
455
+ * have `pi` on it.
456
+ */
457
+ export function getPiCli(): PiCli {
458
+ const resolution = getResolution()
459
+ return toPiCli(
460
+ resolution,
461
+ configuredEngine === 'omp'
462
+ ? 'omp'
463
+ : configuredEngine === 'pi'
464
+ ? 'pi'
465
+ : isOmpExecutable(resolution.script) ? 'omp' : 'pi'
466
+ )
467
+ }
468
+
469
+ /**
470
+ * The engine a start would use right now. Needed wherever the status of "no
471
+ * process yet" has to be reported: the window opens before anything is
472
+ * spawned, and a status with no engine reads as Pi, so the status bar named
473
+ * the wrong CLI until the first agent started.
474
+ */
475
+ export function getConfiguredEngineKind(): AgentEngineKind {
476
+ return getPiCli().kind ?? 'pi'
477
+ }
478
+
479
+ /**
480
+ * The invocation for one named engine, whatever the configured default is.
481
+ *
482
+ * The engine is a PARAMETER here instead of a temporary write to
483
+ * `configuredEngine`: session runtimes start concurrently, so a global the
484
+ * caller flips before each spawn would let one session's engine leak into
485
+ * another session's start — the exact race a per-session engine has to avoid.
486
+ * Each engine's resolution is cached under its own key and cleared with the
487
+ * configured one.
488
+ */
489
+ export function getPiCliForEngine(engine: AgentEngineKind): PiCli {
490
+ const configured = getPiCli()
491
+ // The configured resolution already targets this engine, including any
492
+ // executable path the user set for it.
493
+ if (configured.kind === engine) return configured
494
+ // A configured override names the OTHER engine's binary, so this engine is
495
+ // auto-detected rather than inheriting a path that is not its own.
496
+ const cached = engineResolutions.get(engine)
497
+ if (cached) return toPiCli(cached, engine)
498
+ const resolution = resolvePiBinary(RESOLUTION_DEPS, null, engine)
499
+ // Only a hit is remembered. Caching "not installed" would keep answering
500
+ // that after the user installs the engine, for the whole run.
501
+ if (resolution.found) engineResolutions.set(engine, resolution)
502
+ return toPiCli(resolution, engine)
503
+ }
504
+
505
+ /**
506
+ * The CLI one start() spawns. `options.engine` names the engine that owns the
507
+ * session being opened and outranks the configured default, so a Pi session
508
+ * always resumes under Pi and an OMP session under OMP.
509
+ */
510
+ export function resolveStartCli(options: PiStartOptions): PiCli {
511
+ if (!options.engine) return getPiCli()
512
+ const owner = getPiCliForEngine(options.engine)
513
+ if (owner.found) return owner
514
+ // The owning engine is not installed. Both engines write the same JSONL, so
515
+ // opening the conversation with the configured engine beats refusing to open
516
+ // it at all; getEngineKind then reports what actually runs.
517
+ appLog.warn(
518
+ 'pi',
519
+ `Session engine ${options.engine} is not installed; starting the configured engine instead`
520
+ )
521
+ return getPiCli()
522
+ }
523
+
524
+ /**
525
+ * The argv for one RPC start, engine-independent.
526
+ *
527
+ * No `--session-dir` is injected: each engine keeps its sessions in its own
528
+ * default store and the index reads both. Forcing OMP at Pi's root only ever
529
+ * moved resumed sessions, because OMP ignores the flag for new ones, which
530
+ * split one conversation history across two trees. A caller that really wants
531
+ * a shared store passes `--session-dir` in `options.args`, and it survives —
532
+ * caller args are appended verbatim.
533
+ */
534
+ export function buildPiArgs(options: PiStartOptions): string[] {
535
+ const args: string[] = [MODE_FLAG, RPC_MODE]
536
+
537
+ if (options.noSession) {
538
+ args.push(NO_SESSION_FLAG)
539
+ }
540
+
541
+ if (options.provider) {
542
+ args.push(PROVIDER_FLAG, options.provider)
543
+ }
544
+
545
+ if (options.model) {
546
+ args.push(MODEL_FLAG, options.model)
547
+ }
548
+
549
+ if (options.forkSessionPath) {
550
+ args.push(FORK_FLAG, options.forkSessionPath)
551
+ } else if (options.sessionPath) {
552
+ // Both engines honour an explicit session path, which is what lets a
553
+ // session be resumed out of the store its own engine owns.
554
+ args.push(SESSION_FLAG, options.sessionPath)
555
+ } else if (options.continueSession && !options.noSession) {
556
+ // Resume the most recent session for the cwd. Pi falls back to a fresh
557
+ // session when none exists, so this is safe on first run.
558
+ args.push(CONTINUE_FLAG)
559
+ }
560
+
561
+ if (options.args) {
562
+ args.push(...options.args)
563
+ }
564
+
565
+ return args
566
+ }
567
+
568
+ /**
569
+ * The exact program and argv for one Pi invocation, ready for spawn().
570
+ *
571
+ * `useNode` runs Pi's .js entry point through Node, which never needs a shell,
572
+ * so both paths stay byte-identical there and on POSIX. A direct spawn of a
573
+ * Windows `.cmd`/`.bat` shim does need shell:true (Node refuses to launch one
574
+ * otherwise since CVE-2024-27980) and Node performs no quoting on that path,
575
+ * so the script path — which reaches us through the user profile directory and
576
+ * can hold spaces or cmd metacharacters — and every argument are escaped for
577
+ * the cmd.exe traversal. Throws on characters cmd.exe cannot carry at all.
578
+ */
579
+ export function buildPiInvocation(
580
+ cli: PiCli,
581
+ args: readonly string[],
582
+ ): { file: string; args: string[] } {
583
+ return cli.useNode
584
+ ? escapeCmdSpawn(cli.needsShell, cli.node, [cli.script, ...args])
585
+ : escapeCmdSpawn(cli.needsShell, cli.script, args)
586
+ }
587
+
588
+ const MAX_PENDING_RESPONSES = 64
589
+ const RESPONSE_TIMEOUT_MS = 30_000
590
+
591
+ /**
592
+ * Writable temp directory for the Pi child process on Windows only. Prefer the
593
+ * GUI data dir so extensions (pi-subagents, etc.) don't depend on a locked
594
+ * %TEMP% tree. Not used on POSIX — those platforms keep the system temp so
595
+ * $TMPDIR still receives OS cleanup.
596
+ */
597
+ function resolvePiChildTempDir(): string {
598
+ try {
599
+ const dir = getGuiDataPath('tmp')
600
+ mkdirSync(dir, { recursive: true })
601
+ return dir
602
+ } catch {
603
+ // Fall back to home — still more reliable than a broken Local\Temp ACL.
604
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? process.cwd()
605
+ const dir = join(home, '.pi', 'tmp')
606
+ try {
607
+ mkdirSync(dir, { recursive: true })
608
+ } catch {
609
+ // Last resort: leave system TEMP as-is via process.env
610
+ }
611
+ return dir
612
+ }
613
+ }
614
+
615
+ /** Windows-only TEMP/TMP/TMPDIR override for the Pi child. Empty on other OSes. */
616
+ function buildPiChildEnv(): NodeJS.ProcessEnv {
617
+ if (!IS_WINDOWS) return {}
618
+ const tmp = resolvePiChildTempDir()
619
+ return {
620
+ TEMP: tmp,
621
+ TMP: tmp,
622
+ TMPDIR: tmp,
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Best-effort wipe of the GUI-owned Pi temp dir (Windows). Called on app quit
628
+ * so pi-subagents / extension scratch does not grow without bound.
629
+ */
630
+ export function cleanupPiChildTempDir(): void {
631
+ if (!IS_WINDOWS) return
632
+ try {
633
+ const dir = getGuiDataPath('tmp')
634
+ if (!existsSync(dir)) return
635
+ for (const name of readdirSync(dir)) {
636
+ try {
637
+ rmSync(join(dir, name), { recursive: true, force: true })
638
+ } catch {
639
+ // In use or locked — leave for next quit.
640
+ }
641
+ }
642
+ } catch {
643
+ // Ignore — quit path must not throw.
644
+ }
645
+ }
646
+
647
+ interface PendingResponse {
648
+ resolve: (event: PiResponseEvent) => void
649
+ reject: (error: Error) => void
650
+ timer: ReturnType<typeof setTimeout>
651
+ }
652
+
653
+ /** A Pi RPC command got no response in time. Callers check the class, not the text. */
654
+ export class RpcTimeoutError extends Error {
655
+ constructor(readonly commandType: string, timeoutMs: number) {
656
+ super(t('errors.pi.commandTimedOut', { commandType, timeoutMs }))
657
+ this.name = 'RpcTimeoutError'
658
+ }
659
+ }
660
+
661
+ export class PiRpcManager extends EventEmitter {
662
+ private process: ChildProcess | null = null
663
+ private status: PiProcessStatus = 'stopped'
664
+ private stdoutBuffer = ''
665
+ private stderrBuffer = ''
666
+ private pendingResponses = new Map<string, PendingResponse>()
667
+ private nextRequestId = 1
668
+ private decoder = new StringDecoder('utf8')
669
+ private rpcFrameDecoder = new RpcFrameDecoder()
670
+ private startInFlight: Promise<PiStatus> | null = null
671
+ private runningEngine: 'pi' | 'omp' | null = null
672
+ private readonly exitWaiters = new Map<ChildProcess, Set<() => void>>()
673
+ // Set while a spawn attempt awaits readiness; handleLine invokes it when the
674
+ // startup probe's correlated response arrives. Cleared once the attempt settles.
675
+ private markReady: (() => void) | null = null
676
+ // Settles a pending startup attempt when the process is torn down under it.
677
+ // Without this, stop()/restart() during 'starting' would sever every settle
678
+ // path of spawnAndAwaitReady (kill() removes the child's listeners and the
679
+ // deadline guard is status-gated), leaving start() hung forever.
680
+ private abortStartup: (() => void) | null = null
681
+ // Set on the first stdout byte of the current spawn attempt; decides which
682
+ // startup deadline applies and which timeout error is reported.
683
+ private startupSawOutput = false
684
+ // Non-null only while a spawn attempt is in flight (see PiStartupPhase).
685
+ private startupPhase: PiStartupPhase | null = null
686
+ private readonly startupTimeouts: PiStartupTimeouts
687
+
688
+ constructor(startupTimeouts?: Partial<PiStartupTimeouts>) {
689
+ super()
690
+ this.startupTimeouts = {
691
+ silenceMs: startupTimeouts?.silenceMs ?? STARTUP_SILENCE_TIMEOUT_MS,
692
+ engineBusyMs: startupTimeouts?.engineBusyMs ?? STARTUP_ENGINE_BUSY_TIMEOUT_MS,
693
+ }
694
+ }
695
+
696
+ getStatus(): PiStatus {
697
+ return {
698
+ status: this.status,
699
+ pid: this.process?.pid ?? null,
700
+ // Only report captured stderr as an error when we're actually in the
701
+ // 'error' state. Pi and its extensions (e.g. pi-ollama) log benign,
702
+ // informational lines to stderr while running — surfacing those as an
703
+ // error misleads the UI into showing healthy startup logs as ERROR.
704
+ error: this.status === 'error' ? (this.stderrBuffer || null) : null,
705
+ engine: this.getEngineKind(),
706
+ // Written even when unset (as undefined) so spread-merges of successive
707
+ // snapshots (e.g. SessionRuntimeInfo) cannot carry a stale phase along.
708
+ startupPhase: this.startupPhase ?? undefined,
709
+ }
710
+ }
711
+ /** Engine identity of the live child, not the currently configured future one. */
712
+ getEngineKind(): AgentEngineKind {
713
+ return this.runningEngine ?? getConfiguredEngineKind()
714
+ }
715
+
716
+ async start(options: PiStartOptions = {}): Promise<PiStatus> {
717
+ if (this.status === 'running') {
718
+ return this.getStatus()
719
+ }
720
+ // Coalesce concurrent starts during the 'starting' window so we never
721
+ // spawn duplicate child processes when two callers race.
722
+ if (this.startInFlight) {
723
+ return this.startInFlight
724
+ }
725
+
726
+ this.startInFlight = this.doStart(options).finally(() => {
727
+ this.startInFlight = null
728
+ })
729
+ return this.startInFlight
730
+ }
731
+
732
+ private async doStart(options: PiStartOptions): Promise<PiStatus> {
733
+ this.kill()
734
+ this.setStatus('starting')
735
+ this.stderrBuffer = ''
736
+
737
+ // Pre-flight: if the binary we resolved doesn't exist, fail fast with a
738
+ // clear message instead of letting spawn die with a cryptic ENOENT.
739
+ const { failureReason } = resolveStartCli(options)
740
+ if (failureReason) {
741
+ return this.failPreflight(
742
+ describePiStartFailure(failureReason, t),
743
+ describePiStartFailure(failureReason, tEnglish)
744
+ )
745
+ }
746
+ if (options.cwd && (!existsSync(options.cwd) || !statSync(options.cwd).isDirectory())) {
747
+ return this.failPreflight(
748
+ t('errors.pi.cwdMissing', { cwd: options.cwd }),
749
+ tEnglish('errors.pi.cwdMissing', { cwd: options.cwd })
750
+ )
751
+ }
752
+
753
+ // Spawn, with one retry reserved for a crash before readiness. See
754
+ // STARTUP_MAX_ATTEMPTS: a crash is often transient; a timeout is not.
755
+ for (let attempt = 1; attempt <= STARTUP_MAX_ATTEMPTS; attempt++) {
756
+ const outcome = await this.spawnAndAwaitReady(options)
757
+ if (outcome === 'ready') return this.getStatus()
758
+
759
+ // A deliberate stop/restart tore this attempt down; the initiator has
760
+ // already set the status — report it without retrying or erroring.
761
+ if (outcome === 'aborted') return this.getStatus()
762
+
763
+ if (outcome === 'crashed' && attempt < STARTUP_MAX_ATTEMPTS) {
764
+ console.log(
765
+ `[Pi] Startup crashed before ready (attempt ${attempt}/${STARTUP_MAX_ATTEMPTS}); retrying once…`
766
+ )
767
+ this.kill()
768
+ this.setStatus('starting')
769
+ continue
770
+ }
771
+
772
+ // Terminal failure — surface a useful error. kill() reaps any hung
773
+ // process; it clears stdout but leaves stderrBuffer intact, so the
774
+ // captured reason survives.
775
+ const captured = this.stderrBuffer.trim()
776
+ this.kill()
777
+ this.setStatus('error')
778
+ this.stderrBuffer =
779
+ outcome === 'timeout'
780
+ ? this.describeStartupTimeout(captured)
781
+ : captured || t('errors.pi.crashedBeforeReady')
782
+ return this.getStatus()
783
+ }
784
+
785
+ // Unreachable — the loop always returns — but satisfies the type checker.
786
+ return this.getStatus()
787
+ }
788
+
789
+ /**
790
+ * End a start before spawning. `reason` is the interface-language text the
791
+ * UI shows; `englishReason` is the same message for the log, which stays
792
+ * English.
793
+ */
794
+ private failPreflight(reason: string, englishReason: string): PiStatus {
795
+ this.stderrBuffer = reason
796
+ this.setStatus('error')
797
+ console.error('[Pi] Pre-flight failed:', englishReason)
798
+ appLog.error('pi', 'Pre-flight failed', englishReason)
799
+ return this.getStatus()
800
+ }
801
+
802
+ /**
803
+ * The user-facing reason for a startup timeout. Names only what the streams
804
+ * proved — never guesses at causes the evidence rules out (the old fixed
805
+ * message misdiagnosed issue #58 for days).
806
+ */
807
+ private describeStartupTimeout(captured: string): string {
808
+ const seconds = this.startupSawOutput
809
+ ? this.startupTimeouts.engineBusyMs / MS_PER_SECOND
810
+ : this.startupTimeouts.silenceMs / MS_PER_SECOND
811
+ if (this.startupSawOutput) {
812
+ const busy = t('errors.pi.startupTimeoutBusy', { seconds })
813
+ return captured ? t('errors.pi.startupTimeoutBusyWithStderr', { message: busy, detail: captured }) : busy
814
+ }
815
+ return captured
816
+ ? t('errors.pi.startupTimeoutSilentWithStderr', { seconds, detail: captured })
817
+ : t('errors.pi.startupTimeoutSilentNoOutput', { seconds })
818
+ }
819
+
820
+ /**
821
+ * Spawn one Pi process and wait for it to become RPC-ready. Resolves:
822
+ * - 'ready' — the readiness probe's correlated response arrived; status is
823
+ * now 'running'.
824
+ * - 'crashed' — spawn error, or the process exited before becoming ready.
825
+ * - 'timeout' — no response within the applicable startup deadline:
826
+ * silenceMs while stdout stayed silent, engineBusyMs once
827
+ * output proved the process alive (see PiStartupTimeouts).
828
+ * It does NOT set the terminal 'error' status — doStart owns that, so it can
829
+ * retry a crash without flipping the UI to 'error' between attempts.
830
+ */
831
+ private spawnAndAwaitReady(
832
+ options: PiStartOptions
833
+ ): Promise<'ready' | 'crashed' | 'timeout' | 'aborted'> {
834
+ const args = buildPiArgs(options)
835
+ const cli = resolveStartCli(options)
836
+
837
+ const spawnOptions: SpawnOptions = {
838
+ stdio: ['pipe', 'pipe', 'pipe'],
839
+ cwd: options.cwd,
840
+ // Windows only: redirect TEMP so pi-subagents can mkdir without EPERM on
841
+ // locked %LocalAppData%\Temp trees. POSIX keeps the system temp (OS cleanup).
842
+ env: { ...process.env, ...buildPiChildEnv(), ...options.env },
843
+ // .cmd/.bat/.ps1 shims on Windows can't be invoked directly from
844
+ // spawn — they need the cmd.exe interpreter via shell:true.
845
+ shell: cli.needsShell,
846
+ // On POSIX, make the child its own process-group leader so kill()'s
847
+ // negative-PID group kill reaps Pi and all its descendants. Skipped on
848
+ // Windows, where it would spawn a detached console window with shell:true.
849
+ detached: !IS_WINDOWS,
850
+ }
851
+
852
+ let proc: ChildProcess
853
+ try {
854
+ // Escaping happens inside the try so a path or argument cmd.exe cannot
855
+ // carry fails this attempt like any other spawn failure ('crashed'),
856
+ // instead of throwing out of start().
857
+ const invocation = buildPiInvocation(cli, args)
858
+ console.log('[Pi] Spawning with cwd:', options.cwd)
859
+ console.log('[Pi] Spawn argv :', [invocation.file, ...invocation.args])
860
+ proc = spawn(invocation.file, invocation.args, spawnOptions)
861
+ } catch (err) {
862
+ this.stderrBuffer += (err instanceof Error ? err.message : String(err)) + '\n'
863
+ return Promise.resolve('crashed')
864
+ }
865
+ this.process = proc
866
+ this.runningEngine = cli.kind ?? 'pi'
867
+ this.startupSawOutput = false
868
+ this.startupPhase = 'spawning'
869
+ this.setupStreams()
870
+
871
+ return new Promise<'ready' | 'crashed' | 'timeout' | 'aborted'>((resolve) => {
872
+ let settled = false
873
+ let probeTimer: NodeJS.Timeout | null = null
874
+ let silenceTimer: NodeJS.Timeout | null = null
875
+ let engineBusyTimer: NodeJS.Timeout | null = null
876
+ const startedAt = Date.now()
877
+ const finish = (outcome: 'ready' | 'crashed' | 'timeout' | 'aborted'): void => {
878
+ if (settled) return
879
+ settled = true
880
+ this.markReady = null
881
+ this.abortStartup = null
882
+ this.startupPhase = null
883
+ if (probeTimer) {
884
+ clearInterval(probeTimer)
885
+ probeTimer = null
886
+ }
887
+ if (silenceTimer) {
888
+ clearTimeout(silenceTimer)
889
+ silenceTimer = null
890
+ }
891
+ if (engineBusyTimer) {
892
+ clearTimeout(engineBusyTimer)
893
+ engineBusyTimer = null
894
+ }
895
+ resolve(outcome)
896
+ }
897
+ // Deliberate teardown (stop/restart) while starting: settle without
898
+ // retrying and without stamping an error — the caller chose to stop.
899
+ this.abortStartup = (): void => finish('aborted')
900
+
901
+ // Readiness signal: handleLine invokes this when the probe's correlated
902
+ // response (success OR "unknown command" error) arrives — proof the
903
+ // request→response loop is live.
904
+ this.markReady = (): void => {
905
+ if (this.status === 'starting') {
906
+ console.log(`[Pi] Ready after ${Date.now() - startedAt}ms`)
907
+ this.setStatus('running')
908
+ }
909
+ finish('ready')
910
+ }
911
+
912
+ proc.on('error', (err) => {
913
+ console.error('[Pi] Spawn error:', err.message)
914
+ appLog.error('pi', 'Spawn error', err)
915
+ const spawnError = t('errors.pi.spawnError', { detail: err.message })
916
+ this.stderrBuffer += `${spawnError}\n`
917
+ finish('crashed')
918
+ })
919
+
920
+ proc.on('exit', (code, signal) => {
921
+ console.log('[Pi] Process exited with code:', code, 'signal:', signal, 'pid:', proc.pid)
922
+ if (this.status === 'running') {
923
+ // Exited after becoming ready → normal lifecycle stop.
924
+ this.setStatus('stopped')
925
+ this.emit('exit', { code, signal })
926
+ this.rejectAllPending(t('errors.pi.processExited'))
927
+ return
928
+ }
929
+ // Exited before ready → a startup crash (doStart may retry once).
930
+ if (code !== 0 && code !== null) {
931
+ const priorStderr = this.stderrBuffer || ''
932
+ const exitMessage = t('errors.pi.exitedBeforeReady', { code })
933
+ this.stderrBuffer = `${priorStderr}${exitMessage}`
934
+ }
935
+ finish('crashed')
936
+ })
937
+
938
+ // Send the readiness probe, resent on an interval in case the first write
939
+ // raced Pi's stdin reader (Pi doesn't read stdin until its session is
940
+ // bound, so early writes just buffer harmlessly until then).
941
+ const sendProbe = (): void => {
942
+ if (this.status !== 'starting') return
943
+ try {
944
+ this.process?.stdin?.write(
945
+ JSON.stringify({ type: STARTUP_PROBE_COMMAND, id: STARTUP_PROBE_ID }) + JSONL_NEWLINE
946
+ )
947
+ } catch {
948
+ // stdin not writable yet / EPIPE — a resend or the exit handler covers it.
949
+ }
950
+ }
951
+ sendProbe()
952
+ probeTimer = setInterval(sendProbe, STARTUP_PROBE_INTERVAL_MS)
953
+
954
+ // Hard deadline, in two stages. Silent stdout at the silence cap means a
955
+ // dead spawn or a broken pipe — give up. Output without readiness means
956
+ // the engine is alive but still binding (its extension session_start
957
+ // hooks run before it reads stdin, so our probe sits unread while a hook
958
+ // may wait on a local model server — issue #58): announce the wait and
959
+ // grant the rest of the engine-busy cap.
960
+ silenceTimer = setTimeout(() => {
961
+ if (settled || this.status !== 'starting') return
962
+ if (!this.startupSawOutput) {
963
+ finish('timeout')
964
+ return
965
+ }
966
+ this.startupPhase = 'waiting-on-engine'
967
+ console.log(
968
+ `[Pi] Alive but not ready after ${this.startupTimeouts.silenceMs}ms; ` +
969
+ `waiting up to ${this.startupTimeouts.engineBusyMs}ms for the engine`
970
+ )
971
+ this.emit('startup-phase', this.startupPhase)
972
+ engineBusyTimer = setTimeout(() => {
973
+ if (!settled && this.status === 'starting') {
974
+ finish('timeout')
975
+ }
976
+ }, Math.max(0, this.startupTimeouts.engineBusyMs - this.startupTimeouts.silenceMs))
977
+ }, this.startupTimeouts.silenceMs)
978
+ })
979
+ }
980
+
981
+ stop(): void {
982
+ this.kill()
983
+ this.setStatus('stopped')
984
+ }
985
+
986
+ /**
987
+ * Stop the child and wait until Windows releases its cwd/handles.
988
+ * Ordinary stop() remains fire-and-forget for UI navigation.
989
+ */
990
+ async stopAndWait(timeoutMs = FORCE_KILL_TIMEOUT_MS + 500): Promise<void> {
991
+ const proc = this.process
992
+ if (!proc) {
993
+ this.stop()
994
+ return
995
+ }
996
+ await new Promise<void>((resolvePromise) => {
997
+ let settled = false
998
+ let timer: ReturnType<typeof setTimeout> | undefined
999
+ const finish = (): void => {
1000
+ if (settled) return
1001
+ settled = true
1002
+ clearTimeout(timer)
1003
+ const waiters = this.exitWaiters.get(proc)
1004
+ waiters?.delete(finish)
1005
+ if (waiters && waiters.size === 0) this.exitWaiters.delete(proc)
1006
+ resolvePromise()
1007
+ }
1008
+ const waiters = this.exitWaiters.get(proc) ?? new Set<() => void>()
1009
+ waiters.add(finish)
1010
+ this.exitWaiters.set(proc, waiters)
1011
+ this.stop()
1012
+ if (proc.exitCode !== null || proc.signalCode !== null) finish()
1013
+ else timer = setTimeout(finish, timeoutMs)
1014
+ })
1015
+ }
1016
+
1017
+ restart(options: PiStartOptions = {}): Promise<PiStatus> {
1018
+ this.kill()
1019
+ return this.start(options)
1020
+ }
1021
+
1022
+ /**
1023
+ * Send a command to the Pi RPC process.
1024
+ * Returns a correlated response if an id is provided.
1025
+ */
1026
+ async sendCommand(command: Record<string, unknown>): Promise<PiResponseEvent | null> {
1027
+ if (!this.process?.stdin || this.status !== 'running') {
1028
+ throw new Error(t('errors.pi.processNotRunning'))
1029
+ }
1030
+
1031
+ const id = `req-${this.nextRequestId++}`
1032
+ const cmdWithId = { ...command, id }
1033
+ const line = JSON.stringify(cmdWithId) + JSONL_NEWLINE
1034
+
1035
+ return new Promise<PiResponseEvent | null>((resolve, reject) => {
1036
+ // Check capacity BEFORE allocating a slot so the limit is exact.
1037
+ if (this.pendingResponses.size >= MAX_PENDING_RESPONSES) {
1038
+ reject(new Error('Too many pending responses'))
1039
+ return
1040
+ }
1041
+
1042
+ const timer = setTimeout(() => {
1043
+ this.pendingResponses.delete(id)
1044
+ reject(new RpcTimeoutError(String(command.type), RESPONSE_TIMEOUT_MS))
1045
+ }, RESPONSE_TIMEOUT_MS)
1046
+
1047
+ this.pendingResponses.set(id, { resolve, reject, timer })
1048
+
1049
+ this.process!.stdin!.write(line, (err) => {
1050
+ if (err) {
1051
+ clearTimeout(timer)
1052
+ this.pendingResponses.delete(id)
1053
+ reject(err)
1054
+ }
1055
+ })
1056
+ })
1057
+ }
1058
+
1059
+ /**
1060
+ * Send a command without waiting for a correlated response.
1061
+ */
1062
+ sendCommandFireAndForget(command: Record<string, unknown>): void {
1063
+ if (!this.process?.stdin || this.status !== 'running') {
1064
+ return // Silently ignore if Pi isn't running
1065
+ }
1066
+
1067
+ const line = JSON.stringify(command) + JSONL_NEWLINE
1068
+ try {
1069
+ this.process.stdin.write(line)
1070
+ } catch (err) {
1071
+ if ((err as NodeJS.ErrnoException)?.code !== 'EPIPE') {
1072
+ throw err
1073
+ }
1074
+ // EPIPE means Pi process exited
1075
+ this.setStatus('stopped')
1076
+ }
1077
+ }
1078
+
1079
+ /**
1080
+ * Respond to an extension UI request.
1081
+ */
1082
+ sendExtensionUiResponse(id: string, response: Record<string, unknown>): void {
1083
+ this.sendCommandFireAndForget({
1084
+ type: 'extension_ui_response',
1085
+ id,
1086
+ ...response,
1087
+ })
1088
+ }
1089
+
1090
+ private setupStreams(): void {
1091
+ if (!this.process) return
1092
+
1093
+ // stdout: JSONL events
1094
+ this.process.stdout?.on('data', (chunk: Buffer) => {
1095
+ this.startupSawOutput = true
1096
+ this.stdoutBuffer += this.decoder.write(chunk)
1097
+
1098
+ while (true) {
1099
+ const newlineIndex = this.stdoutBuffer.indexOf('\n')
1100
+ if (newlineIndex === -1) break
1101
+
1102
+ let line = this.stdoutBuffer.slice(0, newlineIndex)
1103
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1)
1104
+
1105
+ // Strip optional \r
1106
+ if (line.endsWith('\r')) {
1107
+ line = line.slice(0, -1)
1108
+ }
1109
+
1110
+ if (line.length > 0) {
1111
+ this.handleLine(line)
1112
+ }
1113
+ }
1114
+ })
1115
+
1116
+ this.process.stdout?.on('end', () => {
1117
+ this.stdoutBuffer += this.decoder.end()
1118
+ if (this.stdoutBuffer.length > 0) {
1119
+ let line = this.stdoutBuffer
1120
+ if (line.endsWith('\r')) line = line.slice(0, -1)
1121
+ if (line.length > 0) this.handleLine(line)
1122
+ }
1123
+ this.stdoutBuffer = ''
1124
+ })
1125
+
1126
+ // stderr: capture for diagnostics
1127
+ this.process.stderr?.on('data', (chunk: Buffer) => {
1128
+ const text = chunk.toString('utf8')
1129
+ this.stderrBuffer += text
1130
+ this.emit('stderr', text)
1131
+ console.log('[Pi STDERR]:', text.slice(0, 200))
1132
+ })
1133
+ }
1134
+
1135
+ private handleLine(line: string): void {
1136
+ let parsed: unknown
1137
+ try {
1138
+ parsed = JSON.parse(line)
1139
+ if (isRpcChunkFrame(parsed)) {
1140
+ parsed = this.rpcFrameDecoder.push(parsed)
1141
+ if (parsed === undefined) return
1142
+ } else if (this.rpcFrameDecoder.hasPending()) {
1143
+ throw new Error('rpc chunk sequence interrupted')
1144
+ }
1145
+ } catch {
1146
+ this.rpcFrameDecoder.reset()
1147
+ this.emit('parse-error', line)
1148
+ return
1149
+ }
1150
+ const event = parsed as PiRpcEvent
1151
+
1152
+ // OMP advertises readiness explicitly. Keep the probe fallback for the
1153
+ // original Pi RPC implementation, which has no ready frame.
1154
+ if ((event as { type?: unknown }).type === 'ready') {
1155
+ this.markReady?.()
1156
+ const ready = event as {
1157
+ supportedProtocolVersions?: unknown
1158
+ maxFrameBytes?: unknown
1159
+ maxReassembledFrameBytes?: unknown
1160
+ }
1161
+ // Adopt the engine's own framing limits before v2 traffic can arrive, so
1162
+ // the decoder never rejects a frame the sender considers legal.
1163
+ this.rpcFrameDecoder.configureLimits(ready)
1164
+ if (Array.isArray(ready.supportedProtocolVersions) && ready.supportedProtocolVersions.includes(2)) {
1165
+ void this.sendCommand({ type: 'negotiate_protocol', protocolVersion: 2 }).catch(() => {})
1166
+ }
1167
+ return
1168
+ }
1169
+
1170
+ // Correlate responses with pending requests
1171
+ if (event.type === 'response') {
1172
+ const responseEvent = event as PiResponseEvent
1173
+ // Startup readiness probe (see spawnAndAwaitReady): its correlated
1174
+ // response — success OR an "unknown command" error, both echoing our id —
1175
+ // means Pi is answering RPC. Flip to ready, then consume it silently so
1176
+ // it never surfaces as a stray event.
1177
+ if (responseEvent.id === STARTUP_PROBE_ID) {
1178
+ this.markReady?.()
1179
+ return
1180
+ }
1181
+ if (responseEvent.id) {
1182
+ const pending = this.pendingResponses.get(responseEvent.id)
1183
+ if (pending) {
1184
+ clearTimeout(pending.timer)
1185
+ this.pendingResponses.delete(responseEvent.id)
1186
+ pending.resolve(responseEvent)
1187
+ return
1188
+ }
1189
+ }
1190
+ }
1191
+
1192
+ // Emit all events for subscribers
1193
+ this.emit('event', event)
1194
+ this.emit(event.type, event)
1195
+ }
1196
+
1197
+ private setStatus(status: PiProcessStatus): void {
1198
+ if (this.status !== status) {
1199
+ this.status = status
1200
+ this.emit('status-change', status)
1201
+ }
1202
+ }
1203
+
1204
+ /**
1205
+ * PIDs descended from `root`, children before grandchildren.
1206
+ *
1207
+ * A negative-PID signal only reaches the killed process's own group, and OMP
1208
+ * puts each subagent it spawns in a new group, so `hub` fan-out survives the
1209
+ * parent dying: the app exits, the subagents are re-parented to init, and
1210
+ * they keep running and keep billing. They have to be enumerated before the
1211
+ * parent is signalled, because re-parenting erases the link immediately.
1212
+ *
1213
+ * POSIX only. Windows has the same gap but `taskkill /T` is the fix there and
1214
+ * it cannot be verified from here, so that path is left as it was.
1215
+ */
1216
+ private descendantPids(root: number): number[] {
1217
+ if (IS_WINDOWS) return []
1218
+ const result = spawnSync('ps', ['-eo', 'pid=,ppid='], { encoding: 'utf-8', timeout: PROCESS_TREE_TIMEOUT_MS })
1219
+ if (result.status !== 0 || typeof result.stdout !== 'string') return []
1220
+
1221
+ const childrenByParent = new Map<number, number[]>()
1222
+ for (const line of result.stdout.split('\n')) {
1223
+ const [pid, ppid] = line.trim().split(/\s+/).map(Number)
1224
+ if (!Number.isInteger(pid) || !Number.isInteger(ppid)) continue
1225
+ const siblings = childrenByParent.get(ppid)
1226
+ if (siblings) siblings.push(pid)
1227
+ else childrenByParent.set(ppid, [pid])
1228
+ }
1229
+
1230
+ const found: number[] = []
1231
+ const queue = [root]
1232
+ // Breadth-first with a seen set: a malformed table must not loop forever.
1233
+ const seen = new Set<number>([root])
1234
+ while (queue.length > 0) {
1235
+ for (const child of childrenByParent.get(queue.shift()!) ?? []) {
1236
+ if (seen.has(child)) continue
1237
+ seen.add(child)
1238
+ found.push(child)
1239
+ queue.push(child)
1240
+ }
1241
+ }
1242
+ return found
1243
+ }
1244
+
1245
+ private kill(): void {
1246
+ // Settle any pending startup attempt first — after the listener teardown
1247
+ // below it could never settle on its own. No-op when already settled.
1248
+ this.abortStartup?.()
1249
+ for (const [, pending] of this.pendingResponses) {
1250
+ clearTimeout(pending.timer)
1251
+ pending.reject(new Error(t('errors.pi.processKilled')))
1252
+ }
1253
+ this.pendingResponses.clear()
1254
+
1255
+ if (this.process) {
1256
+ const proc = this.process
1257
+ proc.removeAllListeners()
1258
+ proc.stdout?.removeAllListeners()
1259
+ proc.stderr?.removeAllListeners()
1260
+ const waiters = this.exitWaiters.get(proc)
1261
+ if (waiters?.size) {
1262
+ const settle = (): void => {
1263
+ this.exitWaiters.delete(proc)
1264
+ for (const waiter of waiters) waiter()
1265
+ }
1266
+ proc.once('exit', settle)
1267
+ proc.once('close', settle)
1268
+ }
1269
+ proc.stdin?.end()
1270
+
1271
+ // Snapshot the tree before signalling: once the parent dies its children
1272
+ // are re-parented to init and can no longer be found from this pid.
1273
+ const strays = proc.pid ? this.descendantPids(proc.pid) : []
1274
+
1275
+ // Kill entire process group (negative PID)
1276
+ try {
1277
+ if (proc.pid) {
1278
+ process.kill(-proc.pid, 'SIGTERM')
1279
+ }
1280
+ } catch {
1281
+ proc.kill('SIGTERM')
1282
+ }
1283
+
1284
+ // Subagents sit in their own groups, so the group signal above misses
1285
+ // them. Signal each directly; ESRCH just means it already exited.
1286
+ for (const stray of strays) {
1287
+ try { process.kill(stray, 'SIGTERM') } catch { /* already gone */ }
1288
+ }
1289
+
1290
+ // Force kill after timeout
1291
+ setTimeout(() => {
1292
+ try {
1293
+ if (proc.pid && !proc.killed) {
1294
+ process.kill(-proc.pid, 'SIGKILL')
1295
+ }
1296
+ } catch {
1297
+ try { proc.kill('SIGKILL') } catch { /* already dead */ }
1298
+ }
1299
+ for (const stray of strays) {
1300
+ try { process.kill(stray, 'SIGKILL') } catch { /* already gone */ }
1301
+ }
1302
+ }, FORCE_KILL_TIMEOUT_MS)
1303
+
1304
+ this.process = null
1305
+ }
1306
+
1307
+ this.stdoutBuffer = ''
1308
+ this.decoder = new StringDecoder('utf8')
1309
+ this.rpcFrameDecoder = new RpcFrameDecoder()
1310
+ }
1311
+
1312
+ private rejectAllPending(reason: string): void {
1313
+ for (const [, pending] of this.pendingResponses) {
1314
+ clearTimeout(pending.timer)
1315
+ pending.reject(new Error(reason))
1316
+ }
1317
+ this.pendingResponses.clear()
1318
+ }
1319
+ }