specrails-desktop 2.36.0 → 2.42.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (510) hide show
  1. package/README.md +94 -247
  2. package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
  3. package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
  4. package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
  5. package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
  6. package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
  7. package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
  8. package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
  9. package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
  10. package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
  11. package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
  12. package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
  13. package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
  14. package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
  15. package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
  16. package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
  17. package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
  18. package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
  19. package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
  20. package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
  21. package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
  22. package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
  23. package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
  24. package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
  25. package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
  26. package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
  27. package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
  28. package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
  29. package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
  30. package/client/dist/assets/agent-BdT0SB1D.js +1 -0
  31. package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
  32. package/client/dist/assets/agent-C4ibdFk9.js +1 -0
  33. package/client/dist/assets/agent-CSFY22Zo.js +1 -0
  34. package/client/dist/assets/agent-DAnqxUn8.js +1 -0
  35. package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
  36. package/client/dist/assets/agent-T54WuR_Y.js +1 -0
  37. package/client/dist/assets/agent-i75WDZOA.js +1 -0
  38. package/client/dist/assets/browser-05vRDg_y.js +1 -0
  39. package/client/dist/assets/browser-Bulb-cB1.js +1 -0
  40. package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
  41. package/client/dist/assets/browser-CG0PE1j_.js +1 -0
  42. package/client/dist/assets/browser-CMzaqAjo.js +1 -0
  43. package/client/dist/assets/browser-DMajYdNX.js +1 -0
  44. package/client/dist/assets/browser-DxcOHrdP.js +1 -0
  45. package/client/dist/assets/browser-ObE7xVo3.js +1 -0
  46. package/client/dist/assets/builder-765NeYTp.js +1 -0
  47. package/client/dist/assets/builder-8MG3V6ea.js +1 -0
  48. package/client/dist/assets/builder-B-1UBs5K.js +1 -0
  49. package/client/dist/assets/builder-BTiGnPT0.js +1 -0
  50. package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
  51. package/client/dist/assets/builder-DKFMELBl.js +1 -0
  52. package/client/dist/assets/builder-OcmKrC0h.js +1 -0
  53. package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
  54. package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
  55. package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
  56. package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
  57. package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
  58. package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
  59. package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
  60. package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
  61. package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
  62. package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
  63. package/client/dist/assets/commands-0XjAftla.js +1 -0
  64. package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
  65. package/client/dist/assets/commands-BBwowgiE.js +1 -0
  66. package/client/dist/assets/commands-BacjIZLN.js +1 -0
  67. package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
  68. package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
  69. package/client/dist/assets/commands-DlbFzgrp.js +1 -0
  70. package/client/dist/assets/commands-LjPSsi5F.js +1 -0
  71. package/client/dist/assets/common-B5AZNYMH.js +1 -0
  72. package/client/dist/assets/common-BFZ0c_vN.js +1 -0
  73. package/client/dist/assets/common-BrlwEnL7.js +1 -0
  74. package/client/dist/assets/common-BttQx3z4.js +1 -0
  75. package/client/dist/assets/common-CM65k_-e.js +1 -0
  76. package/client/dist/assets/common-D4mvFFlu.js +1 -0
  77. package/client/dist/assets/common-TZ9Ny96S.js +1 -0
  78. package/client/dist/assets/common-nFpSF4GC.js +1 -0
  79. package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
  80. package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
  81. package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
  82. package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
  83. package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
  84. package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
  85. package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
  86. package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
  87. package/client/dist/assets/dialog-SRw01_QU.js +45 -0
  88. package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
  89. package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
  90. package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
  91. package/client/dist/assets/index-DwAmL7m5.css +2 -0
  92. package/client/dist/assets/index-EzrkUb8B.js +76 -0
  93. package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
  94. package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
  95. package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
  96. package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
  97. package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
  98. package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
  99. package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
  100. package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
  101. package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
  102. package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
  103. package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
  104. package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
  105. package/client/dist/assets/mcp-CCpSelOh.js +1 -0
  106. package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
  107. package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
  108. package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
  109. package/client/dist/assets/mcp-DUVQamDz.js +1 -0
  110. package/client/dist/assets/narration-B-ynWTpn.js +1 -0
  111. package/client/dist/assets/narration-Bj_laZwg.js +1 -0
  112. package/client/dist/assets/narration-BxHpmMv_.js +1 -0
  113. package/client/dist/assets/narration-C1Px0-0j.js +1 -0
  114. package/client/dist/assets/narration-CAXhGbzM.js +1 -0
  115. package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
  116. package/client/dist/assets/narration-XISXwqZd.js +1 -0
  117. package/client/dist/assets/narration-kwzfglQo.js +1 -0
  118. package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
  119. package/client/dist/assets/nav-B34QkESm.js +1 -0
  120. package/client/dist/assets/nav-Bap_UXAq.js +1 -0
  121. package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
  122. package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
  123. package/client/dist/assets/nav-CygC_WN-.js +1 -0
  124. package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
  125. package/client/dist/assets/nav-m00uxYUG.js +1 -0
  126. package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
  127. package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
  128. package/client/dist/assets/packet-C3Ievb9R.js +1 -0
  129. package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
  130. package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
  131. package/client/dist/assets/packet-DUTGX52M.js +1 -0
  132. package/client/dist/assets/packet-DhsKtUPB.js +1 -0
  133. package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
  134. package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
  135. package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
  136. package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
  137. package/client/dist/assets/settings-BJB9RKJj.js +1 -0
  138. package/client/dist/assets/settings-CHwQR2rB.js +1 -0
  139. package/client/dist/assets/settings-CrMi3kSi.js +1 -0
  140. package/client/dist/assets/settings-DKyhDGD1.js +1 -0
  141. package/client/dist/assets/settings-Deupji9c.js +1 -0
  142. package/client/dist/assets/settings-DqRMmwAM.js +1 -0
  143. package/client/dist/assets/settings-J4rj6vo-.js +1 -0
  144. package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
  145. package/client/dist/assets/setup-Ba11l2JA.js +1 -0
  146. package/client/dist/assets/setup-BzW4kMok.js +1 -0
  147. package/client/dist/assets/setup-COzwYhkJ.js +1 -0
  148. package/client/dist/assets/setup-CRVULhtF.js +1 -0
  149. package/client/dist/assets/setup-DgvlxwpE.js +1 -0
  150. package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
  151. package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
  152. package/client/dist/assets/spending-BqbdPm_4.js +1 -0
  153. package/client/dist/assets/square-Ci-PFcFe.js +1 -0
  154. package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
  155. package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
  156. package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
  157. package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
  158. package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
  159. package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
  160. package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
  161. package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
  162. package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
  163. package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
  164. package/client/dist/index.html +34 -35
  165. package/docs/README.md +3 -0
  166. package/docs/agent-live-steering.md +57 -0
  167. package/docs/ci-cd.md +57 -0
  168. package/docs/code-explorer.md +48 -0
  169. package/docs/codex.md +12 -1
  170. package/docs/features/detachable-mission-windows.md +59 -0
  171. package/docs/guide/de/integrations/5-mcp-server.md +13 -9
  172. package/docs/guide/de/integrations/6-agent-chat.md +1 -0
  173. package/docs/guide/en/integrations/5-mcp-server.md +13 -9
  174. package/docs/guide/en/integrations/6-agent-chat.md +1 -0
  175. package/docs/guide/es/integrations/5-mcp-server.md +13 -9
  176. package/docs/guide/es/integrations/6-agent-chat.md +1 -0
  177. package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
  178. package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
  179. package/docs/guide/it/integrations/5-mcp-server.md +13 -9
  180. package/docs/guide/it/integrations/6-agent-chat.md +1 -0
  181. package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
  182. package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
  183. package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
  184. package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
  185. package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
  186. package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
  187. package/docs/internals/README.md +1 -0
  188. package/docs/internals/browser-capture-performance.md +55 -3
  189. package/docs/internals/browser-login-popups.md +45 -0
  190. package/docs/internals/browser-native-retina-audit.md +39 -0
  191. package/docs/internals/ci-cd-audit.md +39 -0
  192. package/docs/internals/core-runtime-updates.md +68 -0
  193. package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
  194. package/docs/internals/implementation-reliability-audit.md +93 -0
  195. package/docs/internals/interactive-jobs.md +110 -6
  196. package/docs/internals/mcp-mission-audit.md +76 -0
  197. package/docs/internals/project-builder.md +404 -42
  198. package/docs/internals/safe-pr-review-flow.md +52 -10
  199. package/docs/internals/startup-recovery-audit.md +58 -0
  200. package/docs/mcp.md +74 -41
  201. package/docs/mission-processes.md +59 -0
  202. package/docs/multi-repo-projects.md +51 -0
  203. package/docs/platforms/windows-parity.md +70 -0
  204. package/docs/platforms/windows.md +13 -11
  205. package/docs/running-pipelines.md +14 -0
  206. package/mcp-bridge/dist/specrails-mcp.js +7186 -0
  207. package/package.json +9 -4
  208. package/server/dist/agent-chat-manager.js +537 -68
  209. package/server/dist/agent-chat-router.js +87 -6
  210. package/server/dist/agent-context-resolver.js +129 -25
  211. package/server/dist/agent-defaults.js +19 -11
  212. package/server/dist/agent-input-store.js +206 -0
  213. package/server/dist/agent-mcp-config.js +54 -15
  214. package/server/dist/agent-operator-prompt.js +255 -65
  215. package/server/dist/agent-spec-framing.js +202 -0
  216. package/server/dist/agent-steering.js +218 -0
  217. package/server/dist/agent-store.js +153 -0
  218. package/server/dist/api-not-found.js +8 -0
  219. package/server/dist/background-process-control.js +164 -0
  220. package/server/dist/background-process-service.js +61 -0
  221. package/server/dist/background-process-store.js +242 -0
  222. package/server/dist/background-windows-bootstrap.js +238 -0
  223. package/server/dist/blueprint-chat-manager.js +476 -62
  224. package/server/dist/blueprint-commit.js +21 -1
  225. package/server/dist/blueprint-draft-parser.js +122 -17
  226. package/server/dist/blueprint-generation.js +238 -0
  227. package/server/dist/blueprint-operator-prompt.js +91 -63
  228. package/server/dist/blueprint-router.js +55 -3
  229. package/server/dist/blueprint-spec-fixtures.js +85 -0
  230. package/server/dist/blueprint-spec-quality.js +57 -16
  231. package/server/dist/blueprint-store.js +92 -1
  232. package/server/dist/browser-capture-manager.js +86 -30
  233. package/server/dist/browser-playwright.js +168 -45
  234. package/server/dist/browser-viewport.js +26 -0
  235. package/server/dist/chat-manager.js +23 -15
  236. package/server/dist/chromium-archive.cjs +83 -0
  237. package/server/dist/chromium-resolver.js +72 -64
  238. package/server/dist/code-activity.js +145 -0
  239. package/server/dist/code-explorer-router.js +714 -114
  240. package/server/dist/core-compat.js +28 -1
  241. package/server/dist/core-execution.js +111 -0
  242. package/server/dist/core-package.js +4 -16
  243. package/server/dist/core-runtime.js +202 -0
  244. package/server/dist/core-update-manager.js +161 -60
  245. package/server/dist/core-update-state.js +29 -0
  246. package/server/dist/db.js +66 -35
  247. package/server/dist/delivery-evidence.js +142 -39
  248. package/server/dist/desktop-db.js +239 -43
  249. package/server/dist/desktop-router.js +109 -9
  250. package/server/dist/dev-ports.js +8 -3
  251. package/server/dist/file-provenance.js +34 -22
  252. package/server/dist/file-story-manager.js +62 -64
  253. package/server/dist/file-story.js +118 -35
  254. package/server/dist/file-summary-generator.js +46 -5
  255. package/server/dist/file-summary-manager.js +355 -106
  256. package/server/dist/framework-manager.js +98 -110
  257. package/server/dist/framework-reseed.js +76 -10
  258. package/server/dist/git-diagnostics.js +2 -2
  259. package/server/dist/headroom-manager.js +38 -3
  260. package/server/dist/host-control.js +52 -0
  261. package/server/dist/index.js +43 -2
  262. package/server/dist/interactive-job-session.js +149 -16
  263. package/server/dist/internal-api.js +48 -0
  264. package/server/dist/jira/jira-materializer.js +1 -0
  265. package/server/dist/json-tolerant.js +174 -0
  266. package/server/dist/legacy-migration.js +4 -2
  267. package/server/dist/loop-command-catalog.js +83 -21
  268. package/server/dist/loop-constants.js +6 -1
  269. package/server/dist/loop-decider.js +11 -1
  270. package/server/dist/loop-executors.js +194 -45
  271. package/server/dist/loop-factory.js +11 -4
  272. package/server/dist/loop-graph.js +90 -4
  273. package/server/dist/loop-run-manager.js +301 -69
  274. package/server/dist/loop-shell-invocation.js +21 -0
  275. package/server/dist/loop-step-idle.js +55 -0
  276. package/server/dist/loop-templates.js +31 -7
  277. package/server/dist/mcp/agent-capability.js +37 -4
  278. package/server/dist/mcp/guide.js +149 -36
  279. package/server/dist/mcp/mcp-admin-router.js +20 -4
  280. package/server/dist/mcp/mcp-server.js +89 -43
  281. package/server/dist/mcp/mcp-tiers.js +6 -5
  282. package/server/dist/mcp/mcp-token.js +3 -1
  283. package/server/dist/mcp/resources.js +5 -4
  284. package/server/dist/mcp/tools/catalog.js +4 -0
  285. package/server/dist/mcp/tools/code.js +125 -14
  286. package/server/dist/mcp/tools/context.js +153 -0
  287. package/server/dist/mcp/tools/git.js +15 -5
  288. package/server/dist/mcp/tools/jobs.js +84 -71
  289. package/server/dist/mcp/tools/loops.js +17 -6
  290. package/server/dist/mcp/tools/meta.js +74 -27
  291. package/server/dist/mcp/tools/mission.js +29 -0
  292. package/server/dist/mcp/tools/projects.js +22 -6
  293. package/server/dist/mcp/tools/rails.js +48 -11
  294. package/server/dist/mcp/tools/specs.js +86 -14
  295. package/server/dist/mcp/tools/support.js +1 -1
  296. package/server/dist/mcp/tools/types.js +117 -43
  297. package/server/dist/mcp/tools/watch.js +144 -51
  298. package/server/dist/milestone-chain-store.js +164 -0
  299. package/server/dist/milestone-chain.js +487 -0
  300. package/server/dist/milestone-progress.js +348 -0
  301. package/server/dist/mobile/mobile-devices.js +10 -7
  302. package/server/dist/mobile/mobile-gateway.js +2 -0
  303. package/server/dist/mobile/mobile-missions.js +214 -0
  304. package/server/dist/mobile/mobile-redact.js +17 -3
  305. package/server/dist/mobile/mobile-router.js +46 -4
  306. package/server/dist/mobile/mobile-ws.js +49 -4
  307. package/server/dist/multi-repo-bases.js +56 -0
  308. package/server/dist/multi-repo-checkout.js +101 -0
  309. package/server/dist/multi-repo-delivery.js +100 -0
  310. package/server/dist/multi-repo-execution-store.js +65 -0
  311. package/server/dist/multi-repo-execution.js +453 -0
  312. package/server/dist/offline-assemble.js +10 -8
  313. package/server/dist/openspec-runtime-plugin-commands.json +18 -0
  314. package/server/dist/openspec-runtime-plugin.js +120 -0
  315. package/server/dist/plugins/codex-spawn.js +49 -0
  316. package/server/dist/plugins/serena/manifest.js +2 -2
  317. package/server/dist/plugins/serena/templates/instructions.md +16 -0
  318. package/server/dist/pr-publisher.js +1 -1
  319. package/server/dist/profiles-router.js +3 -18
  320. package/server/dist/project-code-discovery.js +142 -0
  321. package/server/dist/project-git.js +19 -21
  322. package/server/dist/project-registry.js +272 -20
  323. package/server/dist/project-repositories.js +157 -0
  324. package/server/dist/project-repository-provenance.js +31 -0
  325. package/server/dist/project-router-background-processes.js +105 -0
  326. package/server/dist/project-router-git.js +75 -16
  327. package/server/dist/project-router-jobs.js +2 -126
  328. package/server/dist/project-router-loop-runs.js +72 -2
  329. package/server/dist/project-router-tickets.js +53 -8
  330. package/server/dist/project-router.js +239 -38
  331. package/server/dist/provider-detection.js +18 -6
  332. package/server/dist/provider-limit.js +64 -0
  333. package/server/dist/providers/claude-adapter.js +26 -1
  334. package/server/dist/providers/claude-background-tasks.js +18 -0
  335. package/server/dist/providers/claude-live-session.js +383 -0
  336. package/server/dist/providers/codex-adapter.js +19 -0
  337. package/server/dist/providers/codex-live-session.js +545 -0
  338. package/server/dist/providers/gemini-adapter.js +1 -0
  339. package/server/dist/providers/live-session-types.js +12 -0
  340. package/server/dist/providers/live-session.js +13 -0
  341. package/server/dist/providers/terminal-result.js +24 -0
  342. package/server/dist/queue-manager.js +2 -0
  343. package/server/dist/rail-isolated-launch.js +141 -16
  344. package/server/dist/rail-pr-decision.js +364 -99
  345. package/server/dist/rail-pr-store.js +24 -10
  346. package/server/dist/rail-worktree-release.js +34 -2
  347. package/server/dist/rail-worktrees-store.js +4 -4
  348. package/server/dist/rails-router.js +249 -55
  349. package/server/dist/repo-lock.js +92 -17
  350. package/server/dist/review-packet.js +23 -13
  351. package/server/dist/schemas/file-summary.v1.json +59 -59
  352. package/server/dist/schemas/profile.v1.json +137 -137
  353. package/server/dist/setup-manager.js +52 -8
  354. package/server/dist/shell-integration/bash-shim.bash +45 -0
  355. package/server/dist/shell-integration/fish-shim.fish +36 -0
  356. package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
  357. package/server/dist/shell-integration/zsh-shim.zsh +47 -0
  358. package/server/dist/smash-runner.js +1 -0
  359. package/server/dist/spawn-lifecycle.js +44 -27
  360. package/server/dist/spec-contract-prompt.js +143 -0
  361. package/server/dist/spec-draft-parser.js +6 -1
  362. package/server/dist/spec-models.js +1 -0
  363. package/server/dist/stuck-run-detector.js +1 -0
  364. package/server/dist/terminal-manager.js +21 -1
  365. package/server/dist/terminal-shell-integration.js +4 -1
  366. package/server/dist/transient-children.js +619 -230
  367. package/server/dist/util/cli-prompt.js +78 -5
  368. package/server/dist/util/sqlite-migrations.js +20 -0
  369. package/server/dist/util/win-spawn.js +18 -3
  370. package/server/dist/util/windows-provider-shell.js +29 -0
  371. package/server/dist/verification-sentinel.js +14 -0
  372. package/server/dist/vitest-setup.js +2 -0
  373. package/server/dist/windows-job-supervisor.js +133 -0
  374. package/server/dist/workspace-manager.js +9 -30
  375. package/server/dist/worktree-manager.js +17 -2
  376. package/server/dist/worktree-overlay.js +3 -1
  377. package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
  378. package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
  379. package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
  380. package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
  381. package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
  382. package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
  383. package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
  384. package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
  385. package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
  386. package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
  387. package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
  388. package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
  389. package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
  390. package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
  391. package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
  392. package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
  393. package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
  394. package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
  395. package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
  396. package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
  397. package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
  398. package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
  399. package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
  400. package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
  401. package/client/dist/assets/agent-BQgKv_2F.js +0 -1
  402. package/client/dist/assets/agent-C1NRDJqL.js +0 -1
  403. package/client/dist/assets/agent-CLNleiy6.js +0 -1
  404. package/client/dist/assets/agent-CmceArkQ.js +0 -1
  405. package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
  406. package/client/dist/assets/agent-DB1JeE64.js +0 -1
  407. package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
  408. package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
  409. package/client/dist/assets/auth-D_5bQv5W.js +0 -1
  410. package/client/dist/assets/ban-E4EaZe3a.js +0 -1
  411. package/client/dist/assets/bot-DzuuuC5R.js +0 -1
  412. package/client/dist/assets/browser-BgqHDMXP.js +0 -1
  413. package/client/dist/assets/browser-CsKJtrpv.js +0 -1
  414. package/client/dist/assets/browser-Cu58EpZU.js +0 -1
  415. package/client/dist/assets/browser-CvXOJm-j.js +0 -1
  416. package/client/dist/assets/browser-DOrFh1c7.js +0 -1
  417. package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
  418. package/client/dist/assets/browser-LPgxvJu2.js +0 -1
  419. package/client/dist/assets/browser-eL-3MIEI.js +0 -1
  420. package/client/dist/assets/builder-0IWtosUm.js +0 -1
  421. package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
  422. package/client/dist/assets/builder-BnqaaBkT.js +0 -1
  423. package/client/dist/assets/builder-BxxfGgAO.js +0 -1
  424. package/client/dist/assets/builder-C5IIgWsq.js +0 -1
  425. package/client/dist/assets/builder-CHuahItu.js +0 -1
  426. package/client/dist/assets/builder-CjWosXL_.js +0 -1
  427. package/client/dist/assets/builder-CpCIOoKj.js +0 -1
  428. package/client/dist/assets/code-CHPkOkDS.js +0 -1
  429. package/client/dist/assets/commands-6tYQih5m.js +0 -1
  430. package/client/dist/assets/commands-BK2QinXN.js +0 -1
  431. package/client/dist/assets/commands-C2Nca76h.js +0 -1
  432. package/client/dist/assets/commands-CKdLihNX.js +0 -1
  433. package/client/dist/assets/commands-CYIBTAlc.js +0 -1
  434. package/client/dist/assets/commands-CjUtYJIM.js +0 -1
  435. package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
  436. package/client/dist/assets/commands-D_ZHYQug.js +0 -1
  437. package/client/dist/assets/common-6MLbD-jM.js +0 -1
  438. package/client/dist/assets/common-BKGEu4rm.js +0 -1
  439. package/client/dist/assets/common-BNbnv8cE.js +0 -1
  440. package/client/dist/assets/common-BqGWtDbt.js +0 -1
  441. package/client/dist/assets/common-DBtTzeYm.js +0 -1
  442. package/client/dist/assets/common-DEW3Nr8n.js +0 -1
  443. package/client/dist/assets/common-DHsv9N5d.js +0 -1
  444. package/client/dist/assets/common-mkrEcW-Y.js +0 -1
  445. package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
  446. package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
  447. package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
  448. package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
  449. package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
  450. package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
  451. package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
  452. package/client/dist/assets/dialog-CdWCb02j.js +0 -45
  453. package/client/dist/assets/dist-bVqfYmby.js +0 -1
  454. package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
  455. package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
  456. package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
  457. package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
  458. package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
  459. package/client/dist/assets/index-DIhMEAVw.js +0 -66
  460. package/client/dist/assets/index-DwygcU7s.css +0 -2
  461. package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
  462. package/client/dist/assets/mcp-C2AojZZN.js +0 -1
  463. package/client/dist/assets/mcp-D8BT78_L.js +0 -1
  464. package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
  465. package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
  466. package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
  467. package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
  468. package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
  469. package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
  470. package/client/dist/assets/narration-BGuiEDj3.js +0 -1
  471. package/client/dist/assets/narration-BuSoqRmv.js +0 -1
  472. package/client/dist/assets/narration-CB9t29fy.js +0 -1
  473. package/client/dist/assets/narration-CU3x8FHD.js +0 -1
  474. package/client/dist/assets/narration-CWeXhfP5.js +0 -1
  475. package/client/dist/assets/narration-DRaDnhT5.js +0 -1
  476. package/client/dist/assets/narration-DtEbQgCw.js +0 -1
  477. package/client/dist/assets/nav-ApJVg3xu.js +0 -1
  478. package/client/dist/assets/nav-BKNky1Bg.js +0 -1
  479. package/client/dist/assets/nav-C6vWWKYx.js +0 -1
  480. package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
  481. package/client/dist/assets/nav-CsmFBIDf.js +0 -1
  482. package/client/dist/assets/nav-DjkmQJjd.js +0 -1
  483. package/client/dist/assets/packet-B1IxaL5f.js +0 -1
  484. package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
  485. package/client/dist/assets/packet-By8fqYxW.js +0 -1
  486. package/client/dist/assets/packet-CZdvA44E.js +0 -1
  487. package/client/dist/assets/packet-CrjHRQCP.js +0 -1
  488. package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
  489. package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
  490. package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
  491. package/client/dist/assets/play-DNlrawbS.js +0 -1
  492. package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
  493. package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
  494. package/client/dist/assets/settings-C1-7cXzC.js +0 -1
  495. package/client/dist/assets/settings-CAFaT38b.js +0 -1
  496. package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
  497. package/client/dist/assets/settings-DHh7_IF5.js +0 -1
  498. package/client/dist/assets/settings-DQXAmdcv.js +0 -1
  499. package/client/dist/assets/settings-guzlhpup.js +0 -1
  500. package/client/dist/assets/settings-sZfTPMEH.js +0 -1
  501. package/client/dist/assets/setup-3jWaoVUO.js +0 -1
  502. package/client/dist/assets/setup-BjgRneb2.js +0 -1
  503. package/client/dist/assets/setup-BzFIjv-f.js +0 -1
  504. package/client/dist/assets/setup-C4VfQsxE.js +0 -1
  505. package/client/dist/assets/setup-DfUkpDbN.js +0 -1
  506. package/client/dist/assets/setup-ECe90Pmu.js +0 -1
  507. package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
  508. package/client/dist/assets/setup-jwfLMiM2.js +0 -1
  509. package/client/dist/assets/spending-EC9xqio-.js +0 -1
  510. package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
@@ -36,9 +36,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.FileSummaryManager = exports.SUMMARY_MAX_LENGTH = void 0;
39
+ exports.FileSummaryManager = exports.WATCH_DEBOUNCE_MS = exports.SUMMARY_MAX_LENGTH = exports.CURRENT_PROMPT_VERSION = void 0;
40
40
  exports.isValidSummaryPayload = isValidSummaryPayload;
41
+ exports.isSummaryMetadataStale = isSummaryMetadataStale;
41
42
  exports.summariesDir = summariesDir;
43
+ exports.repositorySummaryRoot = repositorySummaryRoot;
42
44
  exports.pathHash = pathHash;
43
45
  exports.summaryFilePath = summaryFilePath;
44
46
  exports.computeFileHash = computeFileHash;
@@ -46,6 +48,7 @@ exports.readSummary = readSummary;
46
48
  exports.writeSummary = writeSummary;
47
49
  exports.ensureGitignoreLine = ensureGitignoreLine;
48
50
  exports.sweepOrphans = sweepOrphans;
51
+ exports.resolveWatchEngine = resolveWatchEngine;
49
52
  exports.__resetDesktopSummaryStateForTests = __resetDesktopSummaryStateForTests;
50
53
  const crypto_1 = require("crypto");
51
54
  const fs = __importStar(require("fs"));
@@ -58,7 +61,7 @@ const file_summary_v1_json_1 = __importDefault(require("./schemas/file-summary.v
58
61
  const SUMMARIES_REL = path.join('.specrails', 'file-summaries');
59
62
  // Current summary prompt version. Bump when buildSystemPrompt changes materially
60
63
  // so existing summaries are treated as stale and regenerated on next request.
61
- const CURRENT_PROMPT_VERSION = 1;
64
+ exports.CURRENT_PROMPT_VERSION = 2;
62
65
  // Defensive bound on the per-job counter map so it cannot grow without limit
63
66
  // across a long-lived app session (the per-job cap is best-effort).
64
67
  const MAX_JOB_COUNTERS = 2000;
@@ -97,17 +100,61 @@ function getSummaryValidator() {
97
100
  /** True when `payload` conforms to file-summary.v1.json (with the maxLength
98
101
  * bound on `summary`). Exported so callers/tests can pre-validate. */
99
102
  function isValidSummaryPayload(payload) {
100
- return getSummaryValidator()(payload) === true;
103
+ if (getSummaryValidator()(payload) !== true)
104
+ return false;
105
+ const summary = payload;
106
+ return summary.summary.trim().length > 0 && Number.isFinite(Date.parse(summary.generatedAt));
107
+ }
108
+ /** Legacy caches remain readable, but new prompts/language require regeneration. */
109
+ function isSummaryMetadataStale(summary, language) {
110
+ return summary.language !== language || summary.generatedBy.promptVersion !== exports.CURRENT_PROMPT_VERSION;
101
111
  }
102
112
  function summariesDir(projectPath) {
103
113
  return path.join(projectPath, SUMMARIES_REL);
104
114
  }
115
+ /** Secondary artifacts belong to the logical project's workspace, never to a
116
+ * member's own Core registry/backlog. The primary keeps its historical layout. */
117
+ function repositorySummaryRoot(artifactRoot, repository) {
118
+ return repository.isPrimary ? artifactRoot : path.join(artifactRoot, '.specrails', 'repository-context', pathHash(repository.id));
119
+ }
120
+ function repositoryKey(projectId, repositoryId) {
121
+ return JSON.stringify([projectId, repositoryId ?? null]);
122
+ }
105
123
  function pathHash(relPath) {
106
124
  return (0, crypto_1.createHash)('sha256').update(Buffer.from(relPath, 'utf8')).digest('hex');
107
125
  }
108
126
  function summaryFilePath(projectPath, relPath) {
109
127
  return path.join(summariesDir(projectPath), `${pathHash(relPath)}.json`);
110
128
  }
129
+ /** Revalidate the queued target and bound reads before handing any bytes to AI. */
130
+ function readSourceSnapshot(projectPath, relPath) {
131
+ const root = fs.realpathSync(projectPath);
132
+ const absolute = fs.realpathSync(path.resolve(root, relPath));
133
+ const relative = path.relative(root, absolute);
134
+ if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative))
135
+ throw new Error('Source escapes repository');
136
+ const fd = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
137
+ try {
138
+ const maximum = 2 * 1024 * 1024;
139
+ const metadata = fs.fstatSync(fd);
140
+ if (!metadata.isFile() || metadata.size > maximum)
141
+ throw new Error('Source is not a supported file');
142
+ const bytes = Buffer.alloc(Math.min(maximum + 1, metadata.size + 1));
143
+ let length = 0;
144
+ while (length < bytes.length) {
145
+ const read = fs.readSync(fd, bytes, length, bytes.length - length, null);
146
+ if (!read)
147
+ break;
148
+ length += read;
149
+ }
150
+ if (length > metadata.size || length > maximum || bytes.subarray(0, Math.min(length, 8192)).includes(0))
151
+ throw new Error('Source changed size or is binary');
152
+ return bytes.subarray(0, length);
153
+ }
154
+ finally {
155
+ fs.closeSync(fd);
156
+ }
157
+ }
111
158
  async function computeFileHash(absolutePath) {
112
159
  // Use streaming hash so very large files do not balloon memory.
113
160
  return await new Promise((resolve, reject) => {
@@ -121,12 +168,15 @@ async function computeFileHash(absolutePath) {
121
168
  function readSummary(projectPath, relPath) {
122
169
  const file = summaryFilePath(projectPath, relPath);
123
170
  try {
171
+ const metadata = fs.lstatSync(file);
172
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 128 * 1024)
173
+ return null;
124
174
  const raw = fs.readFileSync(file, 'utf8');
125
175
  const parsed = JSON.parse(raw);
126
176
  // Validate against file-summary.v1.json. A corrupt / hand-edited /
127
177
  // cross-version / oversized summary is treated as ABSENT (null) instead of
128
178
  // being trusted and surfaced verbatim — so the next request regenerates it.
129
- if (!isValidSummaryPayload(parsed))
179
+ if (!isValidSummaryPayload(parsed) || parsed.path !== relPath)
130
180
  return null;
131
181
  return parsed;
132
182
  }
@@ -142,7 +192,7 @@ function writeSummary(summaryRoot, relPath, payload,
142
192
  appendGitignore = true) {
143
193
  // Reject a non-conformant payload before it ever lands on disk so the
144
194
  // documented "schema validated" invariant is real, not aspirational.
145
- if (!isValidSummaryPayload(payload)) {
195
+ if (!isValidSummaryPayload(payload) || payload.path !== relPath) {
146
196
  throw new Error('writeSummary: payload failed file-summary.v1 schema validation');
147
197
  }
148
198
  const dir = summariesDir(summaryRoot);
@@ -225,6 +275,36 @@ sourceRoot) {
225
275
  }
226
276
  return { deleted, remaining };
227
277
  }
278
+ /** Trailing debounce applied to source-change events before the stale check. */
279
+ exports.WATCH_DEBOUNCE_MS = 200;
280
+ /** Errno codes meaning the watcher is consuming a process-wide resource it can
281
+ * no longer get (kqueue file descriptors on macOS, inotify watches on Linux).
282
+ * Keeping such a watcher alive keeps the leak — and, before this guard, an
283
+ * unhandled `error` event on the recursive watcher took the whole server down
284
+ * (`Error: EMFILE: too many open files, watch`). Close + degrade instead. */
285
+ const WATCHER_EXHAUSTION_CODES = new Set(['EMFILE', 'ENFILE', 'ENOSPC']);
286
+ /**
287
+ * Pick the source-watch engine for a platform.
288
+ *
289
+ * macOS and Windows get the kernel's own recursive watch —
290
+ * `fs.watch(dir, { recursive: true })` = FSEvents / ReadDirectoryChangesW —
291
+ * which costs ONE handle for the whole tree. chokidar ≥ 4 dropped fsevents and
292
+ * watches every file with its own `fs.watch`; on macOS that is one kqueue fd
293
+ * PER FILE, so any repo past the ~10k-fd limit blew the process up with
294
+ * `EMFILE: too many open files, watch`. Linux keeps chokidar: inotify watches
295
+ * are not fds, and chokidar's `ignored` pruning keeps `node_modules` / build
296
+ * trees out of the watch set (Node's own recursive implementation on Linux
297
+ * walks and watches everything, `node_modules` included).
298
+ */
299
+ function resolveWatchEngine(platform = process.platform) {
300
+ return platform === 'darwin' || platform === 'win32' ? 'native' : 'chokidar';
301
+ }
302
+ function errnoCode(err) {
303
+ return err?.code;
304
+ }
305
+ function errMessage(err) {
306
+ return err instanceof Error ? err.message : String(err);
307
+ }
228
308
  // App-wide concurrency is shared across EVERY FileSummaryManager instance.
229
309
  // Production constructs one manager per project (each with its own db/broadcast/
230
310
  // generate), so a per-instance counter would only ever cap per-project. This
@@ -271,9 +351,9 @@ class FileSummaryManager {
271
351
  _disposed = false;
272
352
  // Per-project SUPERSET of relPaths that have a summary on disk. Seeded from
273
353
  // the summaries dir at attachWatcher and only ever added to (on write), so a
274
- // path absent from the set provably has no summary — letting the chokidar
275
- // 'change' handler skip the readSummary disk hit for the common no-summary
276
- // file. A stale entry (after sweep) just costs one harmless failed read.
354
+ // path absent from the set provably has no summary — letting the watcher's
355
+ // change funnel skip the disk hits for the common no-summary file. A stale
356
+ // entry (after sweep) just costs one harmless failed read.
277
357
  knownSummaries = new Map();
278
358
  // Tracks pending generation promises so flush() can await them in tests.
279
359
  pending = new Set();
@@ -289,23 +369,26 @@ class FileSummaryManager {
289
369
  // NOT async: returning the in-flight promise verbatim is what makes the dedupe
290
370
  // a true coalesce (the second caller gets the SAME promise, not a wrapper).
291
371
  enqueue(req) {
372
+ if (this._disposed)
373
+ return Promise.resolve('skipped:not-found');
292
374
  // Per-(project,relPath) in-flight dedupe: a second enqueue for the same file
293
375
  // while one is still running coalesces onto the first promise, so the
294
376
  // provider is spawned once and ai_invocations is billed once (fixes
295
377
  // concurrent-regenerate double-billing across tabs/clients).
296
- const dedupeKey = `${req.projectId}:${req.relPath}`;
378
+ const dedupeKey = JSON.stringify([req.projectId, req.repositoryId ?? null, req.relPath]);
297
379
  const existing = this.inFlightByKey.get(dedupeKey);
298
380
  if (existing)
299
381
  return existing;
300
- const p = this._enqueueInner(req);
301
- this.inFlightByKey.set(dedupeKey, p);
302
- void p.catch(() => undefined).finally(() => {
382
+ const p = this._enqueueInner(req).finally(() => {
303
383
  if (this.inFlightByKey.get(dedupeKey) === p)
304
384
  this.inFlightByKey.delete(dedupeKey);
305
385
  });
386
+ this.inFlightByKey.set(dedupeKey, p);
306
387
  return p;
307
388
  }
308
389
  async _enqueueInner(req) {
390
+ if (this._disposed)
391
+ return 'skipped:not-found';
309
392
  const absolutePath = path.join(req.projectPath, req.relPath);
310
393
  // Step 1: file readability check.
311
394
  let newHash;
@@ -316,8 +399,12 @@ class FileSummaryManager {
316
399
  return 'skipped:not-found';
317
400
  }
318
401
  newHash = await computeFileHash(absolutePath);
402
+ if (this._disposed)
403
+ return 'skipped:not-found';
319
404
  }
320
405
  catch {
406
+ if (this._disposed)
407
+ return 'skipped:not-found';
321
408
  this.emitSkipped(req, 'not-found');
322
409
  return 'skipped:not-found';
323
410
  }
@@ -333,8 +420,8 @@ class FileSummaryManager {
333
420
  existing &&
334
421
  existing.fileHash === newHash &&
335
422
  existing.language === currentLang &&
336
- existing.generatedBy?.promptVersion === CURRENT_PROMPT_VERSION) {
337
- this.deps.broadcast(buildSummaryUpdated(req.projectId, existing, false));
423
+ existing.generatedBy?.promptVersion === exports.CURRENT_PROMPT_VERSION) {
424
+ this.deps.broadcast(buildSummaryUpdated(req.projectId, existing, false, req.repositoryId));
338
425
  return 'skipped:hash';
339
426
  }
340
427
  // Step 3: per-job cap — PRE-CHECK ONLY. The counter is incremented in pump()
@@ -361,21 +448,28 @@ class FileSummaryManager {
361
448
  return 'skipped:budget';
362
449
  }
363
450
  }
364
- // Step 5: enqueue or run.
365
- const result = await new Promise((resolve, reject) => {
366
- const entry = {
367
- req: { ...req },
368
- enqueuedAt: (this.deps.now ?? Date.now)(),
369
- newHash,
370
- resolve,
371
- reject,
372
- };
373
- const queue = this.queues.get(req.projectId) ?? [];
374
- queue.push(entry);
451
+ // Hash was only an admission/cache optimization. The worker snapshots fresh
452
+ // bytes and hashes exactly those bytes when its queue slot actually starts.
453
+ return this.queueTask(req, (signal) => this.runOne(req, signal), (reason) => this.emitSkipped(req, reason));
454
+ }
455
+ getLanguage() { return this.deps.language?.() ?? 'en'; }
456
+ /** Stories share this manager's project/app concurrency, spend reservations
457
+ * and disposal. Only the task itself records its billable usage. */
458
+ scheduleTask(req, task) {
459
+ return this.queueTask(req, async (signal) => await task(signal) ? 'enqueued' : 'failed', () => { });
460
+ }
461
+ queueTask(req, run, skipped) {
462
+ if (this._disposed)
463
+ return Promise.resolve('skipped:not-found');
464
+ const queue = this.queues.get(req.projectId) ?? [];
465
+ // Bound retained request/context memory even when provider capacity stalls.
466
+ if (queue.length >= 200)
467
+ return Promise.resolve('failed');
468
+ return new Promise((resolve, reject) => {
469
+ queue.push({ req: { ...req }, enqueuedAt: (this.deps.now ?? Date.now)(), run, skipped, resolve, reject });
375
470
  this.queues.set(req.projectId, queue);
376
471
  this.pump(req.projectId);
377
472
  });
378
- return result;
379
473
  }
380
474
  // Recorded month-to-date spend PLUS the optimistically-reserved spend of
381
475
  // generations already in flight for this project. Both budget gates read this
@@ -408,8 +502,7 @@ class FileSummaryManager {
408
502
  // TTL drop before starting. Distinct 'skipped:ttl' (not 'skipped:hash') so
409
503
  // the regenerate route can tell the user it was dropped, not silently 202.
410
504
  if (now - entry.enqueuedAt > this.queueTtlMs) {
411
- this.emitSkipped(entry.req, 'ttl');
412
- entry.resolve('skipped:ttl');
505
+ this.skipEntry(entry, 'ttl');
413
506
  continue;
414
507
  }
415
508
  // Budget re-check at dequeue: an entry that crossed the monthly cap while
@@ -421,8 +514,7 @@ class FileSummaryManager {
421
514
  const spend = this.effectiveSpend(entry.req.projectId);
422
515
  const budget = this.deps.monthlyBudgetUsd();
423
516
  if (spend >= budget) {
424
- this.emitSkipped(entry.req, 'budget');
425
- entry.resolve('skipped:budget');
517
+ this.skipEntry(entry, 'budget');
426
518
  continue;
427
519
  }
428
520
  }
@@ -432,8 +524,7 @@ class FileSummaryManager {
432
524
  if (entry.req.jobId) {
433
525
  const count = this.jobCounter.get(entry.req.jobId) ?? 0;
434
526
  if (count >= this.perJobCap) {
435
- this.emitSkipped(entry.req, 'per-job-cap');
436
- entry.resolve('skipped:per-job-cap');
527
+ this.skipEntry(entry, 'per-job-cap');
437
528
  continue;
438
529
  }
439
530
  if (!this.jobCounter.has(entry.req.jobId) && this.jobCounter.size >= this.maxJobCounters) {
@@ -452,7 +543,8 @@ class FileSummaryManager {
452
543
  if (!entry.req.overrideBudget)
453
544
  this.reserveSpend(projectId);
454
545
  const reservedSpend = !entry.req.overrideBudget;
455
- const p = this.runOne(entry)
546
+ const p = this.runEntry(entry)
547
+ .then(entry.resolve)
456
548
  .catch((err) => entry.reject(err))
457
549
  .finally(() => {
458
550
  if (reservedSpend)
@@ -479,22 +571,37 @@ class FileSummaryManager {
479
571
  for (const pid of [...this.queues.keys()])
480
572
  this.pump(pid);
481
573
  }
482
- async runOne(entry) {
483
- const { req } = entry;
574
+ skipEntry(entry, reason) {
575
+ try {
576
+ entry.skipped(reason);
577
+ }
578
+ catch { /* a closed WS transport cannot strand a request */ }
579
+ entry.resolve(`skipped:${reason}`);
580
+ }
581
+ async runEntry(entry) {
582
+ const controller = new AbortController();
583
+ this.activeControllers.add(controller);
584
+ try {
585
+ const result = await entry.run(controller.signal);
586
+ return controller.signal.aborted ? 'failed' : result;
587
+ }
588
+ finally {
589
+ this.activeControllers.delete(controller);
590
+ }
591
+ }
592
+ async runOne(req, signal) {
484
593
  const absolutePath = path.join(req.projectPath, req.relPath);
485
594
  const startedIso = new Date((this.deps.now ?? Date.now)()).toISOString();
486
595
  let contents;
487
596
  let fileHash;
488
597
  try {
489
- contents = fs.readFileSync(absolutePath, 'utf8');
490
- // Use the hash captured for THIS entry at enqueue time (never another
491
- // entry's). Fall back to a recompute only if it was somehow not set.
492
- fileHash = entry.newHash || (await computeFileHash(absolutePath));
598
+ const snapshot = readSourceSnapshot(req.projectPath, req.relPath);
599
+ contents = snapshot.toString('utf8');
600
+ fileHash = (0, crypto_1.createHash)('sha256').update(snapshot).digest('hex');
493
601
  }
494
602
  catch {
495
603
  this.emitSkipped(req, 'not-found');
496
- entry.resolve('skipped:not-found');
497
- return;
604
+ return 'skipped:not-found';
498
605
  }
499
606
  const tokens = Math.ceil(contents.length / TOKEN_CHARS_PER_TOKEN);
500
607
  let truncated = false;
@@ -506,20 +613,18 @@ class FileSummaryManager {
506
613
  promptContents = head + TRUNCATE_MARKER + tail;
507
614
  }
508
615
  const lang = (this.deps.language?.() ?? 'en');
509
- const controller = new AbortController();
510
- this.activeControllers.add(controller);
511
616
  try {
512
617
  const out = await this.deps.generate({
513
618
  relPath: req.relPath,
619
+ repositoryId: req.repositoryId,
514
620
  contents: promptContents,
515
621
  truncated,
516
622
  language: lang,
517
- }, controller.signal);
623
+ }, signal);
518
624
  // The project may have been removed (and its DB closed) while the provider
519
625
  // ran. Skip all DB/disk/broadcast work so we never touch a closed handle.
520
626
  if (this._disposed) {
521
- entry.resolve('failed');
522
- return;
627
+ return 'failed';
523
628
  }
524
629
  // Cap a runaway LLM summary at the schema bound so writeSummary's
525
630
  // validation never rejects a real generation (it would otherwise throw and
@@ -532,7 +637,7 @@ class FileSummaryManager {
532
637
  summary: boundedSummary,
533
638
  language: lang,
534
639
  generatedAt: new Date((this.deps.now ?? Date.now)()).toISOString(),
535
- generatedBy: { model: out.model, promptVersion: CURRENT_PROMPT_VERSION, truncated },
640
+ generatedBy: { model: out.model, promptVersion: exports.CURRENT_PROMPT_VERSION, truncated },
536
641
  triggeredBy: req.triggeredBy,
537
642
  };
538
643
  // Relocate-artifacts: summaries OUTPUT to the workspace when relocated
@@ -541,7 +646,7 @@ class FileSummaryManager {
541
646
  const summaryRoot = req.summaryRoot ?? req.projectPath;
542
647
  writeSummary(summaryRoot, req.relPath, payload, summaryRoot === req.projectPath);
543
648
  // Keep the watcher's negative-cache a correct superset.
544
- this.knownSummaries.get(req.projectId)?.add(req.relPath);
649
+ this.knownSummaries.get(repositoryKey(req.projectId, req.repositoryId))?.add(req.relPath);
545
650
  try {
546
651
  (0, ai_invocations_1.recordInvocation)(this.deps.db, {
547
652
  id: (0, crypto_1.randomUUID)(),
@@ -570,15 +675,23 @@ class FileSummaryManager {
570
675
  // spending under-counts with no trace.
571
676
  console.error('[file-summary] recordInvocation (success) failed:', err);
572
677
  }
573
- this.deps.broadcast(buildSummaryUpdated(req.projectId, payload, false));
678
+ let stale = isSummaryMetadataStale(payload, this.getLanguage());
679
+ try {
680
+ stale ||= await computeFileHash(absolutePath) !== fileHash;
681
+ }
682
+ catch {
683
+ stale = true;
684
+ }
685
+ if (this._disposed || signal.aborted)
686
+ return 'failed';
687
+ this.deps.broadcast(buildSummaryUpdated(req.projectId, payload, stale, req.repositoryId));
574
688
  this.deps.broadcast({ type: 'spending.invalidated', projectId: req.projectId });
575
- entry.resolve('enqueued');
689
+ return 'enqueued';
576
690
  }
577
691
  catch (err) {
578
692
  // Disposed mid-flight (project removed) — DB is closed; skip all writes.
579
693
  if (this._disposed) {
580
- entry.resolve('failed');
581
- return;
694
+ return 'failed';
582
695
  }
583
696
  const reason = err instanceof Error ? err.message : String(err);
584
697
  // A timeout/abort kills the child AFTER the provider may have billed tokens.
@@ -616,25 +729,23 @@ class FileSummaryManager {
616
729
  const failedMsg = {
617
730
  type: 'file.summary_failed',
618
731
  projectId: req.projectId,
732
+ ...(req.repositoryId ? { repositoryId: req.repositoryId } : {}),
619
733
  path: req.relPath,
620
734
  reason,
621
735
  };
622
736
  this.deps.broadcast(failedMsg);
623
737
  // Resolve with 'failed' (not 'enqueued') so a caller awaiting enqueue()
624
738
  // can distinguish a failed generation from a successful one.
625
- entry.resolve('failed');
626
- }
627
- finally {
628
- this.activeControllers.delete(controller);
739
+ return 'failed';
629
740
  }
630
741
  }
631
742
  /** `summaryRoot` (relocate-artifacts) is where the summary JSON lives — the
632
743
  * workspace when relocated, else === projectPath. */
633
- markStale(projectPath, projectId, relPath, summaryRoot) {
744
+ markStale(projectPath, projectId, relPath, summaryRoot, repositoryId) {
634
745
  const existing = readSummary(summaryRoot ?? projectPath, relPath);
635
746
  if (!existing)
636
747
  return;
637
- this.deps.broadcast(buildSummaryUpdated(projectId, existing, true));
748
+ this.deps.broadcast(buildSummaryUpdated(projectId, existing, true, repositoryId));
638
749
  }
639
750
  /**
640
751
  * Watch the repo SOURCE tree (`projectPath`) for edits and mark the
@@ -642,28 +753,87 @@ class FileSummaryManager {
642
753
  * summary JSON lives (workspace when relocated) — source is watched at
643
754
  * `projectPath`, summaries are scanned/swept/marked at `summaryRoot`. When
644
755
  * omitted `summaryRoot` defaults to `projectPath` (legacy, byte-identical).
756
+ *
757
+ * Never throws and never lets the watcher take the process down: a watcher
758
+ * that cannot start (or later dies of fd/inotify exhaustion) leaves the
759
+ * project attached in `degraded` status, so later Code-Explorer requests do
760
+ * not retry a doomed recursive watch on every hit.
645
761
  */
646
- attachWatcher(projectId, projectPath, summaryRoot) {
647
- if (this.watchers.has(projectId))
762
+ attachWatcher(projectId, projectPath, summaryRoot, repositoryId) {
763
+ if (this._disposed)
648
764
  return;
765
+ const key = repositoryKey(projectId, repositoryId);
766
+ const previous = this.watchers.get(key);
767
+ if (previous) {
768
+ // ReadDirectoryChangesW dies with EPERM when its root is removed. An
769
+ // explicit explorer request may reattach after it reappears, with a
770
+ // cooldown and a hard limit; resource exhaustion never retries here.
771
+ if (previous.status !== 'degraded' || previous.retryAfter === undefined ||
772
+ Date.now() < previous.retryAfter || (previous.restarts ?? 0) >= 3)
773
+ return;
774
+ try {
775
+ if (!fs.statSync(projectPath).isDirectory())
776
+ return;
777
+ }
778
+ catch {
779
+ return;
780
+ }
781
+ this.closeWatcherState(previous);
782
+ }
649
783
  const sumRoot = summaryRoot ?? projectPath;
650
- this.knownSummaries.set(projectId, this.scanKnownSummaries(sumRoot));
784
+ this.knownSummaries.set(key, this.scanKnownSummaries(sumRoot));
651
785
  // Reclaim summary JSON files whose source file was renamed/deleted since the
652
786
  // last session. Runs once per project per session (attachWatcher is
653
- // idempotent) and is capped at 200/pass inside sweepOrphans. The chokidar
654
- // watcher only sees 'change', never 'unlink', so this is the only reaper.
787
+ // idempotent) and is capped at 200/pass inside sweepOrphans. The watcher
788
+ // only marks stale, never deletes, so this is the only reaper.
655
789
  // sweepOrphans resolves source files against the repo, so it must know both:
656
790
  // summaries under sumRoot, source under projectPath.
657
791
  try {
658
792
  sweepOrphans(sumRoot, undefined, projectPath);
659
793
  }
660
794
  catch { /* best effort */ }
661
- // CRITICAL: prune build/dep trees (node_modules, dist, target, src-tauri/target,
662
- // dot-dirs, …) from the recursive watch. Watching a Rust/Tauri `target/` tree
663
- // opened ~10k file descriptors and broke terminal spawning under fd pressure.
664
- // The predicate is tested against each path relative to the project root so a
665
- // dot-segment in the absolute prefix (the user's home dir) can't false-positive.
795
+ // Registered BEFORE the watch is created so a failed start still counts as
796
+ // attached (degraded) — idempotency is what stops a retry storm.
797
+ const state = { projectPath, status: 'degraded', close: () => { }, timers: new Map(), restarts: previous ? (previous.restarts ?? 0) + 1 : 0 };
798
+ this.watchers.set(key, state);
799
+ const engine = this.deps.watchEngine ?? resolveWatchEngine();
800
+ const onChange = (raw) => this.onSourceChanged(projectId, state, raw, sumRoot, repositoryId);
801
+ const onError = (err) => this.onWatcherError(projectId, state, err);
802
+ try {
803
+ state.close = engine === 'native'
804
+ ? this.startNativeWatcher(projectPath, onChange, onError)
805
+ : this.startChokidarWatcher(projectPath, onChange, onError);
806
+ state.status = engine;
807
+ }
808
+ catch (err) {
809
+ this.degradeWatcher(projectId, state, err, 'start');
810
+ }
811
+ }
812
+ /** Current watcher status for a project, or null when never attached. */
813
+ watcherStatus(projectId, repositoryId) {
814
+ return this.watchers.get(repositoryKey(projectId, repositoryId))?.status ?? null;
815
+ }
816
+ /** One kernel-level recursive watch for the whole tree (FSEvents on macOS,
817
+ * ReadDirectoryChangesW on Windows). `filename` arrives relative to the
818
+ * watched root. `persistent: false` — the HTTP server keeps the process
819
+ * alive; a watcher never should. */
820
+ startNativeWatcher(projectPath, onChange, onError) {
821
+ const watch = this.deps.fsWatch ?? fs.watch;
822
+ const watcher = watch(projectPath, { recursive: true, persistent: false }, (_event, filename) => onChange(filename));
823
+ watcher.on('error', onError);
824
+ return () => { try {
825
+ watcher.close();
826
+ }
827
+ catch { /* best effort */ } };
828
+ }
829
+ /** chokidar (Linux): one inotify watch per file/dir, so the build/dep trees
830
+ * MUST be pruned — a Rust `target/` or `node_modules` alone is tens of
831
+ * thousands of watches. The predicate is tested against each path relative
832
+ * to the project root so a dot-segment in the absolute prefix (the user's
833
+ * home dir) can't false-positive. */
834
+ startChokidarWatcher(projectPath, onChange, onError) {
666
835
  const watcher = chokidar_1.default.watch(projectPath, {
836
+ followSymlinks: false,
667
837
  ignored: (p) => {
668
838
  const rel = path.relative(projectPath, p);
669
839
  if (!rel || rel.startsWith('..'))
@@ -671,25 +841,105 @@ class FileSummaryManager {
671
841
  return (0, build_dirs_1.isInBuildDir)(rel);
672
842
  },
673
843
  ignoreInitial: true,
674
- persistent: true,
675
- // Coalesce partial-write churn so one logical edit fires one event.
676
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },
677
- });
678
- watcher.on('change', (changed) => {
679
- // The summary store keys off POSIX forward-slash relpaths everywhere
680
- // (the REST normalizeRel, pathHash, knownSummaries seeding). On Windows
681
- // path.relative yields backslashes, so normalize before lookup/markStale —
682
- // otherwise `known.has(rel)` always misses and edits never mark stale.
683
- const rel = path.relative(projectPath, changed).split(path.sep).join('/');
684
- if (!rel || rel.startsWith('..'))
685
- return;
686
- // Skip the readSummary disk hit when this file provably has no summary.
687
- const known = this.knownSummaries.get(projectId);
688
- if (known && !known.has(rel))
689
- return;
690
- this.markStale(projectPath, projectId, rel, sumRoot);
844
+ // A dir we cannot read is not a reason to lose the whole watcher.
845
+ ignorePermissionErrors: true,
846
+ persistent: false,
691
847
  });
692
- this.watchers.set(projectId, { projectPath, watcher });
848
+ watcher.on('change', (changed) => onChange(changed));
849
+ watcher.on('unlink', (removed) => onChange(removed));
850
+ watcher.on('error', onError);
851
+ return () => { void watcher.close().catch(() => { }); };
852
+ }
853
+ /** Shared change funnel for both engines: normalise to a POSIX relpath, drop
854
+ * build/dep/dot trees and files that provably have no summary, then debounce
855
+ * per path before the (hash-checked) stale mark. */
856
+ onSourceChanged(projectId, state, raw, sumRoot, repositoryId) {
857
+ if (raw == null || this._disposed || state.status === 'degraded')
858
+ return;
859
+ const name = typeof raw === 'string' ? raw : raw.toString();
860
+ // Native engines report paths relative to the watched root (Windows with
861
+ // backslashes); chokidar reports absolute paths. Meet in the middle.
862
+ const relRaw = path.isAbsolute(name) ? path.relative(state.projectPath, name) : name;
863
+ // The summary store keys off POSIX forward-slash relpaths everywhere (the
864
+ // REST normalizeRel, pathHash, knownSummaries seeding) — normalise before
865
+ // lookup/markStale, otherwise `known.has(rel)` always misses on Windows.
866
+ const rel = relRaw.split(path.sep).join('/');
867
+ if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
868
+ return;
869
+ if ((0, build_dirs_1.isInBuildDir)(rel))
870
+ return;
871
+ // Skip the disk hits when this file provably has no summary.
872
+ const known = this.knownSummaries.get(repositoryKey(projectId, repositoryId));
873
+ if (known && !known.has(rel))
874
+ return;
875
+ const pending = state.timers.get(rel);
876
+ if (pending)
877
+ clearTimeout(pending);
878
+ const timer = setTimeout(() => {
879
+ state.timers.delete(rel);
880
+ void this.markStaleIfChanged(state.projectPath, projectId, rel, sumRoot, repositoryId);
881
+ }, exports.WATCH_DEBOUNCE_MS);
882
+ timer.unref?.();
883
+ state.timers.set(rel, timer);
884
+ }
885
+ /** Watcher-side markStale: broadcast ONLY when the source really differs from
886
+ * what the summary was generated against. A save that leaves the bytes
887
+ * unchanged, an editor's temp-file dance, or a replayed/coalesced kernel
888
+ * event must not flag a still-valid summary. A source that vanished IS
889
+ * stale — the summary describes a file that is gone. */
890
+ async markStaleIfChanged(projectPath, projectId, rel, sumRoot, repositoryId) {
891
+ const existing = readSummary(sumRoot, rel);
892
+ if (!existing)
893
+ return;
894
+ let hash = null;
895
+ try {
896
+ hash = await computeFileHash(path.join(projectPath, rel));
897
+ }
898
+ catch {
899
+ hash = null;
900
+ }
901
+ if (hash !== null && hash === existing.fileHash)
902
+ return;
903
+ if (this._disposed)
904
+ return;
905
+ this.deps.broadcast(buildSummaryUpdated(projectId, existing, true, repositoryId));
906
+ }
907
+ onWatcherError(projectId, state, err) {
908
+ const code = errnoCode(err);
909
+ if (state.status === 'native' && (code === 'EPERM' || code === 'EACCES')) {
910
+ state.retryAfter = Date.now() + 30_000;
911
+ this.degradeWatcher(projectId, state, err, 'runtime');
912
+ return;
913
+ }
914
+ if (code && !WATCHER_EXHAUSTION_CODES.has(code) && state.status !== 'degraded') {
915
+ // Transient and local (a dir vanished mid-scan, a permission hiccup on
916
+ // one subtree): keep watching, just log.
917
+ console.warn('[file-summary] watcher error', JSON.stringify({ projectId, code, error: errMessage(err) }));
918
+ return;
919
+ }
920
+ this.degradeWatcher(projectId, state, err, 'runtime');
921
+ }
922
+ /** Release the watcher (and, with it, every fd/watch it holds) and park the
923
+ * project in degraded mode. Logged once; only a terminated native root
924
+ * watcher may be reattached by a later explicit request after cooldown. */
925
+ degradeWatcher(projectId, state, err, phase) {
926
+ const alreadyDegraded = state.status === 'degraded' && phase === 'runtime';
927
+ state.status = 'degraded';
928
+ this.closeWatcherState(state);
929
+ if (alreadyDegraded)
930
+ return;
931
+ console.warn('[file-summary] watcher degraded — edits will not mark summaries stale until the next explicit read/regenerate', JSON.stringify({ projectId, projectPath: state.projectPath, phase, code: errnoCode(err) ?? null, error: errMessage(err) }));
932
+ }
933
+ closeWatcherState(state) {
934
+ const close = state.close;
935
+ state.close = () => { };
936
+ try {
937
+ close();
938
+ }
939
+ catch { /* best effort */ }
940
+ for (const timer of state.timers.values())
941
+ clearTimeout(timer);
942
+ state.timers.clear();
693
943
  }
694
944
  /** Read the relPaths of every summary on disk into a set (one-time at attach). */
695
945
  scanKnownSummaries(projectPath) {
@@ -714,13 +964,14 @@ class FileSummaryManager {
714
964
  }
715
965
  return set;
716
966
  }
717
- detachWatcher(projectId) {
718
- const state = this.watchers.get(projectId);
967
+ detachWatcher(projectId, repositoryId) {
968
+ const key = repositoryKey(projectId, repositoryId);
969
+ const state = this.watchers.get(key);
719
970
  if (!state)
720
971
  return;
721
- void state.watcher.close();
722
- this.watchers.delete(projectId);
723
- this.knownSummaries.delete(projectId);
972
+ this.closeWatcherState(state);
973
+ this.watchers.delete(key);
974
+ this.knownSummaries.delete(key);
724
975
  }
725
976
  /** Full teardown for a single manager: stop accepting work, abort any in-flight
726
977
  * provider child (so it is not orphaned past project removal), reject queued
@@ -743,34 +994,30 @@ class FileSummaryManager {
743
994
  // Reject still-queued entries so awaiting callers settle (skipped, not hung).
744
995
  for (const [, queue] of this.queues) {
745
996
  for (const entry of queue) {
746
- try {
747
- this.emitSkipped(entry.req, 'not-found');
748
- entry.resolve('skipped:not-found');
749
- }
750
- catch { /* best effort */ }
997
+ this.skipEntry(entry, 'not-found');
751
998
  }
752
999
  }
753
1000
  this.queues.clear();
754
1001
  this.inFlightByKey.clear();
755
1002
  this.pendingSpend.clear();
756
- for (const [, state] of this.watchers) {
757
- try {
758
- void state.watcher.close();
759
- }
760
- catch { /* best effort */ }
761
- }
1003
+ for (const [, state] of this.watchers)
1004
+ this.closeWatcherState(state);
762
1005
  this.watchers.clear();
763
1006
  this.knownSummaries.clear();
764
1007
  }
765
- /** Close every watcher. Called on graceful server shutdown so chokidar's
766
- * underlying fsevents/inotify handles are released, never leaked. */
1008
+ /** Close every watcher. Called on graceful server shutdown so the
1009
+ * underlying FSEvents/ReadDirectoryChangesW/inotify handles are released,
1010
+ * never leaked. */
767
1011
  disposeAll() {
768
1012
  this.dispose();
769
1013
  }
770
1014
  async flush() {
771
1015
  // Drain until no pending work remains.
772
1016
  while (this.pending.size > 0 || this.hasQueued()) {
773
- await Promise.allSettled(Array.from(this.pending));
1017
+ if (this.pending.size > 0)
1018
+ await Promise.allSettled(Array.from(this.pending));
1019
+ else
1020
+ await new Promise(resolve => setTimeout(resolve, 0)); // let another manager release the shared slot
774
1021
  }
775
1022
  }
776
1023
  hasQueued() {
@@ -783,6 +1030,7 @@ class FileSummaryManager {
783
1030
  const msg = {
784
1031
  type: 'file.summary_skipped',
785
1032
  projectId: req.projectId,
1033
+ ...(req.repositoryId ? { repositoryId: req.repositoryId } : {}),
786
1034
  path: req.relPath,
787
1035
  reason,
788
1036
  };
@@ -790,10 +1038,11 @@ class FileSummaryManager {
790
1038
  }
791
1039
  }
792
1040
  exports.FileSummaryManager = FileSummaryManager;
793
- function buildSummaryUpdated(projectId, payload, stale) {
1041
+ function buildSummaryUpdated(projectId, payload, stale, repositoryId) {
794
1042
  const msg = {
795
1043
  type: 'file.summary_updated',
796
1044
  projectId,
1045
+ ...(repositoryId ? { repositoryId } : {}),
797
1046
  path: payload.path,
798
1047
  summaryAvailable: true,
799
1048
  stale,