aicodeman 1.45.0 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (312) hide show
  1. package/README.md +26 -6
  2. package/dist/agent-codex-identity.d.ts +26 -0
  3. package/dist/agent-codex-identity.d.ts.map +1 -0
  4. package/dist/agent-codex-identity.js +253 -0
  5. package/dist/agent-codex-identity.js.map +1 -0
  6. package/dist/agent-inspection.d.ts +32 -0
  7. package/dist/agent-inspection.d.ts.map +1 -0
  8. package/dist/agent-inspection.js +196 -0
  9. package/dist/agent-inspection.js.map +1 -0
  10. package/dist/agent-staging.d.ts +130 -0
  11. package/dist/agent-staging.d.ts.map +1 -0
  12. package/dist/agent-staging.js +312 -0
  13. package/dist/agent-staging.js.map +1 -0
  14. package/dist/cli-agent.d.ts +31 -2
  15. package/dist/cli-agent.d.ts.map +1 -1
  16. package/dist/cli-agent.js +279 -34
  17. package/dist/cli-agent.js.map +1 -1
  18. package/dist/cli-history.d.ts +20 -0
  19. package/dist/cli-history.d.ts.map +1 -0
  20. package/dist/cli-history.js +61 -0
  21. package/dist/cli-history.js.map +1 -0
  22. package/dist/cli.d.ts.map +1 -1
  23. package/dist/cli.js +6 -1
  24. package/dist/cli.js.map +1 -1
  25. package/dist/codex-rollout-format.d.ts +66 -0
  26. package/dist/codex-rollout-format.d.ts.map +1 -0
  27. package/dist/codex-rollout-format.js +111 -0
  28. package/dist/codex-rollout-format.js.map +1 -0
  29. package/dist/codex-transcript.d.ts +3 -0
  30. package/dist/codex-transcript.d.ts.map +1 -1
  31. package/dist/codex-transcript.js +13 -41
  32. package/dist/codex-transcript.js.map +1 -1
  33. package/dist/config/cli-registry/schema.d.ts +12 -7
  34. package/dist/config/cli-registry/schema.d.ts.map +1 -1
  35. package/dist/config/cli-registry/schema.js +26 -0
  36. package/dist/config/cli-registry/schema.js.map +1 -1
  37. package/dist/config/cli-registry/stock.d.ts.map +1 -1
  38. package/dist/config/cli-registry/stock.js +40 -1
  39. package/dist/config/cli-registry/stock.js.map +1 -1
  40. package/dist/config/cli-registry/types.d.ts +57 -0
  41. package/dist/config/cli-registry/types.d.ts.map +1 -1
  42. package/dist/config/server-timing.d.ts +6 -0
  43. package/dist/config/server-timing.d.ts.map +1 -1
  44. package/dist/config/server-timing.js +9 -0
  45. package/dist/config/server-timing.js.map +1 -1
  46. package/dist/daemon-control.d.ts +1 -0
  47. package/dist/daemon-control.d.ts.map +1 -1
  48. package/dist/daemon-control.js +2 -0
  49. package/dist/daemon-control.js.map +1 -1
  50. package/dist/deepseek-transcript.d.ts +9 -7
  51. package/dist/deepseek-transcript.d.ts.map +1 -1
  52. package/dist/deepseek-transcript.js +9 -7
  53. package/dist/deepseek-transcript.js.map +1 -1
  54. package/dist/docker-hosts.d.ts +8 -0
  55. package/dist/docker-hosts.d.ts.map +1 -1
  56. package/dist/docker-hosts.js +29 -0
  57. package/dist/docker-hosts.js.map +1 -1
  58. package/dist/git-fetch-scheduler.d.ts +124 -0
  59. package/dist/git-fetch-scheduler.d.ts.map +1 -0
  60. package/dist/git-fetch-scheduler.js +249 -0
  61. package/dist/git-fetch-scheduler.js.map +1 -0
  62. package/dist/git-fetch.d.ts +241 -0
  63. package/dist/git-fetch.d.ts.map +1 -0
  64. package/dist/git-fetch.js +392 -0
  65. package/dist/git-fetch.js.map +1 -0
  66. package/dist/git-workspace-status.d.ts +36 -1
  67. package/dist/git-workspace-status.d.ts.map +1 -1
  68. package/dist/git-workspace-status.js +42 -5
  69. package/dist/git-workspace-status.js.map +1 -1
  70. package/dist/hooks-config.d.ts +14 -2
  71. package/dist/hooks-config.d.ts.map +1 -1
  72. package/dist/hooks-config.js +39 -3
  73. package/dist/hooks-config.js.map +1 -1
  74. package/dist/local-history.d.ts +71 -0
  75. package/dist/local-history.d.ts.map +1 -0
  76. package/dist/local-history.js +750 -0
  77. package/dist/local-history.js.map +1 -0
  78. package/dist/mcp-server.d.ts.map +1 -1
  79. package/dist/mcp-server.js +2 -2
  80. package/dist/mcp-server.js.map +1 -1
  81. package/dist/mux-interface.d.ts +5 -2
  82. package/dist/mux-interface.d.ts.map +1 -1
  83. package/dist/prompt-cache-keepwarm.d.ts.map +1 -1
  84. package/dist/prompt-cache-keepwarm.js +5 -3
  85. package/dist/prompt-cache-keepwarm.js.map +1 -1
  86. package/dist/ralph-fix-plan-watcher.d.ts +13 -1
  87. package/dist/ralph-fix-plan-watcher.d.ts.map +1 -1
  88. package/dist/ralph-fix-plan-watcher.js +21 -3
  89. package/dist/ralph-fix-plan-watcher.js.map +1 -1
  90. package/dist/ralph-tracker.d.ts +6 -0
  91. package/dist/ralph-tracker.d.ts.map +1 -1
  92. package/dist/ralph-tracker.js +8 -0
  93. package/dist/ralph-tracker.js.map +1 -1
  94. package/dist/session.d.ts +113 -0
  95. package/dist/session.d.ts.map +1 -1
  96. package/dist/session.js +341 -13
  97. package/dist/session.js.map +1 -1
  98. package/dist/terminal-input-classifier.d.ts +8 -0
  99. package/dist/terminal-input-classifier.d.ts.map +1 -0
  100. package/dist/terminal-input-classifier.js +60 -0
  101. package/dist/terminal-input-classifier.js.map +1 -0
  102. package/dist/tmux-manager.d.ts +23 -1
  103. package/dist/tmux-manager.d.ts.map +1 -1
  104. package/dist/tmux-manager.js +54 -6
  105. package/dist/tmux-manager.js.map +1 -1
  106. package/dist/types/session.d.ts +15 -0
  107. package/dist/types/session.d.ts.map +1 -1
  108. package/dist/types/session.js.map +1 -1
  109. package/dist/web/network-auth-policy.d.ts +13 -3
  110. package/dist/web/network-auth-policy.d.ts.map +1 -1
  111. package/dist/web/network-auth-policy.js +15 -3
  112. package/dist/web/network-auth-policy.js.map +1 -1
  113. package/dist/web/ports/infra-port.d.ts +2 -0
  114. package/dist/web/ports/infra-port.d.ts.map +1 -1
  115. package/dist/web/public/admin-ui.js.gz +0 -0
  116. package/dist/web/public/api-client.c9b1cddc.js.gz +0 -0
  117. package/dist/web/public/app.fe8a6b4c.js +43 -0
  118. package/dist/web/public/app.fe8a6b4c.js.br +0 -0
  119. package/dist/web/public/app.fe8a6b4c.js.gz +0 -0
  120. package/dist/web/public/approvals-ui.js.gz +0 -0
  121. package/dist/web/public/{constants.0b3cdde9.js → constants.affcdde6.js} +611 -12
  122. package/dist/web/public/constants.affcdde6.js.br +0 -0
  123. package/dist/web/public/constants.affcdde6.js.gz +0 -0
  124. package/dist/web/public/cron-ui.js.gz +0 -0
  125. package/dist/web/public/entrance-animations.js.gz +0 -0
  126. package/dist/web/public/git-status-ui.js +88 -5
  127. package/dist/web/public/git-status-ui.js.br +0 -0
  128. package/dist/web/public/git-status-ui.js.gz +0 -0
  129. package/dist/web/public/header-divider.eb2d2463.js.gz +0 -0
  130. package/dist/web/public/home-sessions.js.gz +0 -0
  131. package/dist/web/public/host-wake-ui.js.gz +0 -0
  132. package/dist/web/public/i18n.a618ef4e.js +2 -0
  133. package/dist/web/public/i18n.a618ef4e.js.br +0 -0
  134. package/dist/web/public/i18n.a618ef4e.js.gz +0 -0
  135. package/dist/web/public/{image-input.941386e7.js → image-input.a5d0f8f3.js} +18 -9
  136. package/dist/web/public/image-input.a5d0f8f3.js.br +0 -0
  137. package/dist/web/public/image-input.a5d0f8f3.js.gz +0 -0
  138. package/dist/web/public/index.html +110 -19
  139. package/dist/web/public/index.html.br +0 -0
  140. package/dist/web/public/index.html.gz +0 -0
  141. package/dist/web/public/input-cjk.8bc46081.js.gz +0 -0
  142. package/dist/web/public/{keyboard-accessory.4e8b20ac.js → keyboard-accessory.ff64fafc.js} +5 -1
  143. package/dist/web/public/keyboard-accessory.ff64fafc.js.br +0 -0
  144. package/dist/web/public/keyboard-accessory.ff64fafc.js.gz +0 -0
  145. package/dist/web/public/{mobile-handlers.1c0cbcd8.js → mobile-handlers.f7c3f692.js} +22 -0
  146. package/dist/web/public/mobile-handlers.f7c3f692.js.br +0 -0
  147. package/dist/web/public/mobile-handlers.f7c3f692.js.gz +0 -0
  148. package/dist/web/public/mobile-ime-preview.2dbaf262.js.gz +0 -0
  149. package/dist/web/public/mobile-overview.js.gz +0 -0
  150. package/dist/web/public/mobile.1bbd2d67.css +1 -0
  151. package/dist/web/public/mobile.1bbd2d67.css.br +0 -0
  152. package/dist/web/public/mobile.1bbd2d67.css.gz +0 -0
  153. package/dist/web/public/notification-manager.feb82c04.js.gz +0 -0
  154. package/dist/web/public/orchestrator-panel.js.gz +0 -0
  155. package/dist/web/public/{panels-ui.78c3defb.js → panels-ui.4a56ee63.js} +1 -1
  156. package/dist/web/public/panels-ui.4a56ee63.js.br +0 -0
  157. package/dist/web/public/panels-ui.4a56ee63.js.gz +0 -0
  158. package/dist/web/public/ralph-panel.b350e2ef.js.gz +0 -0
  159. package/dist/web/public/ralph-wizard.b6c0dd19.js.gz +0 -0
  160. package/dist/web/public/readmymind-ui.js.gz +0 -0
  161. package/dist/web/public/reboot-restore-ui.js.gz +0 -0
  162. package/dist/web/public/respawn-ui.d7adcf6a.js.gz +0 -0
  163. package/dist/web/public/sanitize-html.e7147e76.js.gz +0 -0
  164. package/dist/web/public/session-lineage.js.gz +0 -0
  165. package/dist/web/public/{session-ui.ea6b1994.js → session-ui.426fb671.js} +8 -8
  166. package/dist/web/public/session-ui.426fb671.js.br +0 -0
  167. package/dist/web/public/session-ui.426fb671.js.gz +0 -0
  168. package/dist/web/public/{settings-ui.085dd202.js → settings-ui.a191154e.js} +11 -11
  169. package/dist/web/public/settings-ui.a191154e.js.br +0 -0
  170. package/dist/web/public/settings-ui.a191154e.js.gz +0 -0
  171. package/dist/web/public/spreadsheet-preview-worker.js.gz +0 -0
  172. package/dist/web/public/spreadsheet-preview.75c751de.js.gz +0 -0
  173. package/dist/web/public/spreadsheet-xlsx-core.js.gz +0 -0
  174. package/dist/web/public/styles.4f0c67a0.css +1 -0
  175. package/dist/web/public/styles.4f0c67a0.css.br +0 -0
  176. package/dist/web/public/styles.4f0c67a0.css.gz +0 -0
  177. package/dist/web/public/subagent-windows.07277a58.js.gz +0 -0
  178. package/dist/web/public/sw.js +2 -2
  179. package/dist/web/public/sw.js.br +0 -0
  180. package/dist/web/public/sw.js.gz +0 -0
  181. package/dist/web/public/tab-layout-browser.2d6189f7.js.gz +0 -0
  182. package/dist/web/public/{tab-rail-resize.7157e0bd.js → tab-rail-resize.b5cdceaf.js} +1 -1
  183. package/dist/web/public/tab-rail-resize.b5cdceaf.js.br +0 -0
  184. package/dist/web/public/tab-rail-resize.b5cdceaf.js.gz +0 -0
  185. package/dist/web/public/terminal-keycode229-recovery.98843069.js +1 -0
  186. package/dist/web/public/terminal-keycode229-recovery.98843069.js.br +0 -0
  187. package/dist/web/public/terminal-keycode229-recovery.98843069.js.gz +0 -0
  188. package/dist/web/public/terminal-split.js.gz +0 -0
  189. package/dist/web/public/terminal-tile.js +10 -6
  190. package/dist/web/public/terminal-tile.js.br +0 -0
  191. package/dist/web/public/terminal-tile.js.gz +0 -0
  192. package/dist/web/public/terminal-ui.7e787829.js +4 -0
  193. package/dist/web/public/terminal-ui.7e787829.js.br +0 -0
  194. package/dist/web/public/terminal-ui.7e787829.js.gz +0 -0
  195. package/dist/web/public/tile-grid.js.gz +0 -0
  196. package/dist/web/public/ultracode-panel.js.gz +0 -0
  197. package/dist/web/public/ultracode-windows.js.gz +0 -0
  198. package/dist/web/public/vendor/dompurify.min.js.gz +0 -0
  199. package/dist/web/public/vendor/exceljs.min.js.gz +0 -0
  200. package/dist/web/public/vendor/fflate.min.js.gz +0 -0
  201. package/dist/web/public/vendor/marked.min.js.gz +0 -0
  202. package/dist/web/public/vendor/xterm-addon-fit.min.js.gz +0 -0
  203. package/dist/web/public/vendor/xterm-addon-serialize.min.js.gz +0 -0
  204. package/dist/web/public/vendor/xterm-addon-unicode11.min.js.gz +0 -0
  205. package/dist/web/public/vendor/xterm-addon-webgl.min.js.gz +0 -0
  206. package/dist/web/public/vendor/{xterm-predictive-echo.ae5e3d5d.js → xterm-predictive-echo.276b11d9.js} +1 -1
  207. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.br +0 -0
  208. package/dist/web/public/vendor/xterm-predictive-echo.276b11d9.js.gz +0 -0
  209. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js +5 -0
  210. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.br +0 -0
  211. package/dist/web/public/vendor/xterm-zerolag-input.1be42307.js.gz +0 -0
  212. package/dist/web/public/vendor/xterm.css.gz +0 -0
  213. package/dist/web/public/vendor/xterm.min.js.gz +0 -0
  214. package/dist/web/public/voice-input.2342c37b.js.gz +0 -0
  215. package/dist/web/public/voice-pcm-worklet.js.gz +0 -0
  216. package/dist/web/public/webview-tabs.js.gz +0 -0
  217. package/dist/web/public/whats-new.json +8 -9
  218. package/dist/web/request-body-parser.d.ts +23 -0
  219. package/dist/web/request-body-parser.d.ts.map +1 -0
  220. package/dist/web/request-body-parser.js +57 -0
  221. package/dist/web/request-body-parser.js.map +1 -0
  222. package/dist/web/routes/agent-session-routes.d.ts +20 -0
  223. package/dist/web/routes/agent-session-routes.d.ts.map +1 -0
  224. package/dist/web/routes/agent-session-routes.js +160 -0
  225. package/dist/web/routes/agent-session-routes.js.map +1 -0
  226. package/dist/web/routes/case-routes.d.ts.map +1 -1
  227. package/dist/web/routes/case-routes.js +32 -26
  228. package/dist/web/routes/case-routes.js.map +1 -1
  229. package/dist/web/routes/config-backup-routes.js +2 -2
  230. package/dist/web/routes/config-backup-routes.js.map +1 -1
  231. package/dist/web/routes/file-routes.d.ts +5 -0
  232. package/dist/web/routes/file-routes.d.ts.map +1 -1
  233. package/dist/web/routes/file-routes.js +5 -1
  234. package/dist/web/routes/file-routes.js.map +1 -1
  235. package/dist/web/routes/git-status-routes.d.ts +21 -7
  236. package/dist/web/routes/git-status-routes.d.ts.map +1 -1
  237. package/dist/web/routes/git-status-routes.js +78 -12
  238. package/dist/web/routes/git-status-routes.js.map +1 -1
  239. package/dist/web/routes/index.d.ts +1 -0
  240. package/dist/web/routes/index.d.ts.map +1 -1
  241. package/dist/web/routes/index.js +1 -0
  242. package/dist/web/routes/index.js.map +1 -1
  243. package/dist/web/routes/ralph-routes.d.ts.map +1 -1
  244. package/dist/web/routes/ralph-routes.js +52 -18
  245. package/dist/web/routes/ralph-routes.js.map +1 -1
  246. package/dist/web/routes/session-routes.d.ts.map +1 -1
  247. package/dist/web/routes/session-routes.js +101 -28
  248. package/dist/web/routes/session-routes.js.map +1 -1
  249. package/dist/web/routes/system-routes.d.ts.map +1 -1
  250. package/dist/web/routes/system-routes.js +87 -59
  251. package/dist/web/routes/system-routes.js.map +1 -1
  252. package/dist/web/routes/ws-routes.d.ts +5 -0
  253. package/dist/web/routes/ws-routes.d.ts.map +1 -1
  254. package/dist/web/routes/ws-routes.js +84 -47
  255. package/dist/web/routes/ws-routes.js.map +1 -1
  256. package/dist/web/schemas.d.ts +30 -7
  257. package/dist/web/schemas.d.ts.map +1 -1
  258. package/dist/web/schemas.js +40 -1
  259. package/dist/web/schemas.js.map +1 -1
  260. package/dist/web/server.d.ts +5 -2
  261. package/dist/web/server.d.ts.map +1 -1
  262. package/dist/web/server.js +102 -18
  263. package/dist/web/server.js.map +1 -1
  264. package/dist/web/settings-writer.d.ts +47 -0
  265. package/dist/web/settings-writer.d.ts.map +1 -0
  266. package/dist/web/settings-writer.js +110 -0
  267. package/dist/web/settings-writer.js.map +1 -0
  268. package/package.json +1 -1
  269. package/skills/codeman/SKILL.md +25 -6
  270. package/skills/codeman/reference/history.md +84 -0
  271. package/skills/codeman/reference/inspection-and-staging.md +152 -0
  272. package/skills/codeman/reference/messaging.md +1 -1
  273. package/skills/codeman/reference/verbs.md +18 -3
  274. package/dist/web/public/app.42d0abae.js +0 -43
  275. package/dist/web/public/app.42d0abae.js.br +0 -0
  276. package/dist/web/public/app.42d0abae.js.gz +0 -0
  277. package/dist/web/public/constants.0b3cdde9.js.br +0 -0
  278. package/dist/web/public/constants.0b3cdde9.js.gz +0 -0
  279. package/dist/web/public/i18n.1e68220d.js +0 -2
  280. package/dist/web/public/i18n.1e68220d.js.br +0 -0
  281. package/dist/web/public/i18n.1e68220d.js.gz +0 -0
  282. package/dist/web/public/image-input.941386e7.js.br +0 -0
  283. package/dist/web/public/image-input.941386e7.js.gz +0 -0
  284. package/dist/web/public/keyboard-accessory.4e8b20ac.js.br +0 -0
  285. package/dist/web/public/keyboard-accessory.4e8b20ac.js.gz +0 -0
  286. package/dist/web/public/mobile-handlers.1c0cbcd8.js.br +0 -0
  287. package/dist/web/public/mobile-handlers.1c0cbcd8.js.gz +0 -0
  288. package/dist/web/public/mobile.12fb18de.css +0 -1
  289. package/dist/web/public/mobile.12fb18de.css.br +0 -0
  290. package/dist/web/public/mobile.12fb18de.css.gz +0 -0
  291. package/dist/web/public/panels-ui.78c3defb.js.br +0 -0
  292. package/dist/web/public/panels-ui.78c3defb.js.gz +0 -0
  293. package/dist/web/public/session-ui.ea6b1994.js.br +0 -0
  294. package/dist/web/public/session-ui.ea6b1994.js.gz +0 -0
  295. package/dist/web/public/settings-ui.085dd202.js.br +0 -0
  296. package/dist/web/public/settings-ui.085dd202.js.gz +0 -0
  297. package/dist/web/public/styles.3b59b41d.css +0 -1
  298. package/dist/web/public/styles.3b59b41d.css.br +0 -0
  299. package/dist/web/public/styles.3b59b41d.css.gz +0 -0
  300. package/dist/web/public/tab-rail-resize.7157e0bd.js.br +0 -0
  301. package/dist/web/public/tab-rail-resize.7157e0bd.js.gz +0 -0
  302. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js +0 -1
  303. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.br +0 -0
  304. package/dist/web/public/terminal-keycode229-recovery.af09f99a.js.gz +0 -0
  305. package/dist/web/public/terminal-ui.fb2a27f5.js +0 -2
  306. package/dist/web/public/terminal-ui.fb2a27f5.js.br +0 -0
  307. package/dist/web/public/terminal-ui.fb2a27f5.js.gz +0 -0
  308. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.br +0 -0
  309. package/dist/web/public/vendor/xterm-predictive-echo.ae5e3d5d.js.gz +0 -0
  310. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js +0 -4
  311. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.br +0 -0
  312. package/dist/web/public/vendor/xterm-zerolag-input.0b69623c.js.gz +0 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * @fileoverview Git fetch on an interval: the sessions the user turned on, fetched by a tick the server owns
3
+ * (`src/git-fetch-scheduler.ts`, wired in `src/web/server.ts`).
4
+ *
5
+ * The fetch module (src/git-fetch.ts) is the only git command the Git panel runs that writes, and on a timer
6
+ * it is the only one that can run without being asked (cloning and worktree management write too, but those
7
+ * happen when the user asks for them). Which is why the whole feature is off until the user turns it on, in
8
+ * Settings → Bottom bar, per session in Session Options → Git.
9
+ *
10
+ * The scheduler holds no timers of its own. The server ticks it (`GIT_FETCH_TICK_INTERVAL_MS`) and every
11
+ * tick reads the app setting FRESH, so a live toggle takes effect on the next tick instead of at boot, and
12
+ * turning the feature off actually stops the fetching. A session can override both the switch and the
13
+ * interval; a session with no override follows the app.
14
+ *
15
+ * Rules the code keeps and the tests pin:
16
+ * - Nothing runs when nothing is on: no repository set is read and nothing is spawned, so a quiet
17
+ * install costs one settings read per tick. A session that turned itself on still runs even while the
18
+ * app-wide switch is off: that is what a per-session switch has to mean.
19
+ * - Remote and Docker sessions are never fetched, exactly as the Git panel excludes them, and the Docker
20
+ * workspace roots are checked again every tick, so a session whose case became a Docker case stops
21
+ * fetching at the next tick. A Docker case list that cannot be read fails CLOSED: the tick fetches
22
+ * nothing (a fetch runs a repository's hooks, and without the list nothing tells which folders a
23
+ * container can write to), the folders it picked stay due for the next tick, and it is logged once
24
+ * until a read succeeds again.
25
+ * - A folder is fetched by at most one run at a time, and the run belongs to the FOLDER, not to one
26
+ * session: two sessions open on the same folder are the same clone, so they share one run and both get
27
+ * its result. State is keyed by `resolve(workingDir)`, deliberately not `realpath`, because `realpath`
28
+ * is itself one of the unbounded fs calls rule #516 bounds; the bounded probe below is the fs read.
29
+ * Every session open on a folder is remembered, so closing the one holding the override does not stop
30
+ * the folder's fetching while another session is still on it. A folder is due when ANY session open on it
31
+ * is due, and the tick decides that per folder in a second pass: the order the sessions were read cannot
32
+ * change which interval wins, so a 30 s session on a folder a 3600 s session last fetched ten minutes ago
33
+ * is fetched now. The folder is probed once, not once per session on it.
34
+ * - Before anything is fetched, the folder is probed BOUNDED with the default options (rule #516): a case
35
+ * on a mount that stopped answering is skipped, never touched, and a probe that answers `unknown` is not
36
+ * read as "not a folder".
37
+ * - A schedule is stamped at SELECTION, when a session goes into `due`, not when its run starts, so a
38
+ * tick that is still running cannot pick the same folder again; and a tick is skipped while the previous
39
+ * one is still going, so the bound below is the bound for the whole tick, not per tick.
40
+ * - At most `FETCH_CONCURRENCY` git processes at a time across the whole tick, because each is a git
41
+ * process: ONE bound is shared by every run of the tick, so `FETCH_CONCURRENCY` is the tick's ceiling
42
+ * rather than each folder's own. The folders get their OWN bound (`mapLimited`), because one semaphore
43
+ * cannot serve both levels: a folder that holds a slot and then waits for one for its own fetch is
44
+ * waiting on itself, and the folders that fill the bound are exactly the ones blocking it.
45
+ * - The interval is timed from the START of a run, so a run that takes three minutes does not push the
46
+ * next one out by three minutes; it lands on the tick after its own interval elapses.
47
+ * - The per-repository limit and the git timeout stay the module's defaults (`MAX_REPOS`,
48
+ * `DEFAULT_GIT_TIMEOUT_MS`): both settings are per device and never reach the server, so the server
49
+ * cannot know what a device chose.
50
+ * - A failure is reported, not retried harder: the folder keeps its schedule and gets its next run at its
51
+ * own interval. A settings read that fails skips this tick only, rather than deciding from defaults.
52
+ * settings.json is replaced atomically, so such a failure is a file that does not parse (a hand edit) or
53
+ * a real read error, which lasts until someone fixes it: it is logged once, and again only after a read
54
+ * has succeeded in between, not every 15 s tick.
55
+ *
56
+ * @module git-fetch-scheduler
57
+ */
58
+ import { resolve } from 'node:path';
59
+ import { getErrorMessage } from './types.js';
60
+ import { FETCH_CONCURRENCY, MAX_GIT_FETCH_INTERVAL_SECONDS, MIN_GIT_FETCH_INTERVAL_SECONDS, gitFetchStatusFrom, } from './git-fetch.js';
61
+ import { clampInt, DEFAULT_GIT_TIMEOUT_MS, MAX_REPOS, createLimiter, mapLimited } from './git-workspace-status.js';
62
+ /**
63
+ * What one session will actually do: its own override wins, otherwise the app-wide setting. The interval is
64
+ * clamped again here because a persisted value can predate the schema's bounds.
65
+ */
66
+ export function effectiveGitFetch(view, settings) {
67
+ return {
68
+ enabled: view.gitFetchEnabled ?? settings.enabled,
69
+ intervalSeconds: clampInt(view.gitFetchIntervalSeconds ?? settings.intervalSeconds, MIN_GIT_FETCH_INTERVAL_SECONDS, MAX_GIT_FETCH_INTERVAL_SECONDS, MIN_GIT_FETCH_INTERVAL_SECONDS),
70
+ };
71
+ }
72
+ export class GitFetchScheduler {
73
+ ports;
74
+ /** When each FOLDER last started a run; timed from the start, not the end. */
75
+ lastRunAt = new Map();
76
+ inFlight = new Set();
77
+ /** Session → folder, so `remove()` can keep the folder's schedule while other sessions are still on it. */
78
+ folderOf = new Map();
79
+ running = false;
80
+ stopped = false;
81
+ /** The last settings read failed and was logged; cleared by the next read that succeeds. */
82
+ settingsUnreadable = false;
83
+ /** The same for the Docker case list. */
84
+ dockerCasesUnreadable = false;
85
+ constructor(ports) {
86
+ this.ports = ports;
87
+ }
88
+ /** One tick: fetch every folder whose sessions are on and due. Returns how many folders ran. */
89
+ async tick(now = Date.now()) {
90
+ if (this.stopped || this.running)
91
+ return 0;
92
+ this.running = true;
93
+ try {
94
+ let settings;
95
+ try {
96
+ settings = await this.ports.readSettings();
97
+ }
98
+ catch (err) {
99
+ // Writes are atomic, so this is a real read error or a file that does not parse (a hand edit), and it
100
+ // stays that way until it is fixed: logged once, not on every tick. The reader never puts the file's
101
+ // text in its error (readGitFetchSettings), since settings.json can hold an API key.
102
+ if (!this.settingsUnreadable) {
103
+ this.settingsUnreadable = true;
104
+ this.ports.log?.(`git fetch: settings unreadable, ticks skipped until it reads again: ${getErrorMessage(err)}`);
105
+ }
106
+ return 0;
107
+ }
108
+ this.settingsUnreadable = false;
109
+ // Pass 1: who is on what, before anything is decided. Every session open on a folder is recorded here,
110
+ // not only the one that would get picked, so `remove()` can still see who is open on it when the session
111
+ // holding the override is the one that closes.
112
+ const groups = new Map();
113
+ for (const id of this.ports.sessionIds()) {
114
+ const view = this.ports.getSession(id);
115
+ if (!view || view.remote || view.docker)
116
+ continue;
117
+ const effective = effectiveGitFetch(view, settings);
118
+ if (!effective.enabled)
119
+ continue;
120
+ const folder = resolve(view.workingDir);
121
+ this.folderOf.set(id, folder);
122
+ const group = groups.get(folder);
123
+ const member = { view, intervalSeconds: effective.intervalSeconds };
124
+ if (group)
125
+ group.push(member);
126
+ else
127
+ groups.set(folder, [member]);
128
+ }
129
+ // Pass 2: the folder's turn, decided from the whole group rather than from whichever session happened to
130
+ // be read first. A folder is due when ANY session open on it is due: a session on a 30 s interval on a
131
+ // folder a 3600 s session last fetched ten minutes ago has to be fetched now, because that same run
132
+ // satisfies the long interval too. Deciding in one pass means the session read first owns the clock: a
133
+ // group ordered [short, long] and a group ordered [long, short] would have behaved differently, and the
134
+ // run belongs to the folder, so no session in the group should.
135
+ const due = new Map();
136
+ // What each picked folder's clock said before this tick stamped it, so a tick that ends up fetching
137
+ // nothing (an unreadable Docker case list, below) can leave the folders due again.
138
+ const stampedOver = new Map();
139
+ for (const [folder, group] of groups) {
140
+ if (this.inFlight.has(folder))
141
+ continue;
142
+ const started = this.lastRunAt.get(folder);
143
+ const isDue = started === undefined || group.some((m) => now - started >= m.intervalSeconds * 1000);
144
+ if (!isDue)
145
+ continue;
146
+ // Bounded probe (rule #516), once per folder rather than once per session on it.
147
+ const kind = await this.ports.probePath(folder);
148
+ if (kind !== 'directory') {
149
+ this.ports.log?.(`git fetch: ${folder} skipped, answered ${kind}`);
150
+ continue;
151
+ }
152
+ this.inFlight.add(folder);
153
+ // The clock starts at the selection, so a slow folder does not shift its own schedule.
154
+ stampedOver.set(folder, started);
155
+ this.lastRunAt.set(folder, now);
156
+ due.set(folder, group);
157
+ }
158
+ if (!due.size)
159
+ return 0;
160
+ // Only now, because nothing above needs it: a quiet tick does not read the Docker case list.
161
+ let dockerWorkspaces;
162
+ try {
163
+ dockerWorkspaces = await this.ports.dockerWorkspaces();
164
+ }
165
+ catch (err) {
166
+ // Fail CLOSED: a fetch runs the repository's hooks, and without the list nothing tells which folders a
167
+ // container can write to. Nothing ran, so the picks are undone (a stamp a `remove()` already cleared
168
+ // stays cleared) and the folders are due again on the next tick instead of after their interval.
169
+ for (const [folder, started] of stampedOver) {
170
+ this.inFlight.delete(folder);
171
+ if (this.lastRunAt.get(folder) !== now)
172
+ continue;
173
+ if (started === undefined)
174
+ this.lastRunAt.delete(folder);
175
+ else
176
+ this.lastRunAt.set(folder, started);
177
+ }
178
+ if (!this.dockerCasesUnreadable) {
179
+ this.dockerCasesUnreadable = true;
180
+ this.ports.log?.(`git fetch: Docker case list unreadable, nothing fetched until it reads again: ${getErrorMessage(err)}`);
181
+ }
182
+ return 0;
183
+ }
184
+ this.dockerCasesUnreadable = false;
185
+ // ONE bound for the git processes of the whole tick, handed to every run so no folder gets its own:
186
+ // each fetch is one git process, and a folder can hold up to `MAX_REPOS_LIMIT` of them, so a bound per
187
+ // run would let `FETCH_CONCURRENCY` folders × `MAX_REPOS_LIMIT` processes run at once.
188
+ const fetches = createLimiter(FETCH_CONCURRENCY);
189
+ // The folders get their OWN bound, because the same semaphore cannot serve both levels: a folder that
190
+ // holds a slot and then waits for a slot for its own fetch is waiting on itself: once
191
+ // `FETCH_CONCURRENCY` folders fill the bound, the ones holding slots are exactly the ones that can
192
+ // never get one back. Both bounds are `FETCH_CONCURRENCY`, so the tick still never runs more git
193
+ // processes than the module's ceiling.
194
+ await mapLimited([...due.entries()], FETCH_CONCURRENCY, async ([folder, group]) => {
195
+ try {
196
+ const run = await this.ports.fetch(folder, {
197
+ dockerWorkspaces,
198
+ maxRepos: MAX_REPOS,
199
+ timeoutMs: DEFAULT_GIT_TIMEOUT_MS,
200
+ limiter: fetches,
201
+ }, 'interval');
202
+ // Every session on the folder gets the SAME run, with the trigger of the run that actually happened:
203
+ // if this tick joined a run a click had already started, the row has to say it was a click, because
204
+ // that is what fetched it.
205
+ for (const member of group)
206
+ this.ports.publish(member.view.id, gitFetchStatusFrom(run.report, run.trigger));
207
+ }
208
+ catch (err) {
209
+ // The run that failed keeps its reason in the panel's own row; here only the folder-level failure is
210
+ // worth a line, because nothing else would say a scheduled fetch could not run at all.
211
+ this.ports.log?.(`git fetch: ${folder} could not run: ${getErrorMessage(err)}`);
212
+ }
213
+ finally {
214
+ this.inFlight.delete(folder);
215
+ }
216
+ });
217
+ return due.size;
218
+ }
219
+ finally {
220
+ this.running = false;
221
+ }
222
+ }
223
+ /**
224
+ * A session that closed or was detached: its schedule goes with it, but the FOLDER's stays while another
225
+ * session is still open on it: the run belongs to the clone, not to the session that asked for it.
226
+ */
227
+ remove(id) {
228
+ const folder = this.folderOf.get(id);
229
+ this.folderOf.delete(id);
230
+ if (!folder)
231
+ return;
232
+ const stillOpen = [...this.folderOf.values()].includes(folder);
233
+ if (!stillOpen) {
234
+ this.lastRunAt.delete(folder);
235
+ this.inFlight.delete(folder);
236
+ }
237
+ }
238
+ stop() {
239
+ this.stopped = true;
240
+ this.lastRunAt.clear();
241
+ this.inFlight.clear();
242
+ this.folderOf.clear();
243
+ }
244
+ /** Folders the scheduler still remembers a run for, so a test can see a `remove()` actually took. */
245
+ tracked() {
246
+ return new Set([...this.lastRunAt.keys(), ...this.inFlight]).size;
247
+ }
248
+ }
249
+ //# sourceMappingURL=git-fetch-scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-fetch-scheduler.js","sourceRoot":"","sources":["../src/git-fetch-scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,241 @@
1
+ /**
2
+ * @fileoverview `git fetch` for the repositories the Git panel shows, on the user's click
3
+ * (`POST /api/sessions/:id/git-fetch`) or on an interval the user turned on
4
+ * (`src/git-fetch-scheduler.ts`).
5
+ *
6
+ * The status module (src/git-workspace-status.ts) is deliberately offline: "behind" is as of the last
7
+ * fetch, so a number that is stale can only be made current by fetching. This is the only git command the
8
+ * Git panel runs that writes, and the only git command Codeman runs on a timer. Cloning a repository and
9
+ * managing worktrees write too, but those are things the user asked for at the moment they asked; this is
10
+ * the one that can happen later, by itself. Nothing here commits, checks out, merges or touches the working
11
+ * tree: `git fetch` updates remote-tracking
12
+ * refs and nothing else, and `FETCH_CONFIG_ARGS` keeps it from starting a gc in a repository an agent is
13
+ * already using. It is not harmless in the way a read is: a fetch holds git's ref lock, so an agent's own
14
+ * `git pull` in the same clone can collide with it (`cannot lock ref`), which is why one folder is fetched
15
+ * by at most one run at a time (`fetchWorkspaceOnce`).
16
+ *
17
+ * Same split as git-clone.ts: the pure half (`buildFetchArgs`, `describeFetchFailure`, `fetchEnvFor`,
18
+ * `gitFetchSettingsFrom`, `gitFetchStatusFrom`) holds every decision and tests without spawning, and the IO
19
+ * half runs one bounded `git fetch --quiet` per repository.
20
+ *
21
+ * Rules the code keeps and the tests pin:
22
+ * - The repository set is `resolveWorkspaceRepos`, the single source the panel and the diff route
23
+ * already use, so Fetch cannot reach a repository the panel would not show. The Docker rule is
24
+ * checked before any git runs: a repository a container can write to is refused outright, because a
25
+ * planted config would run on the host, and `git fetch` is exactly the command that can run a
26
+ * repository's own hooks (`reference-transaction`, and `pre-auto-gc` through the automatic maintenance
27
+ * that follows a fetch) and its config commands such as `credential.helper`, which is why a timer only
28
+ * runs it after the user opts in.
29
+ * - One run per folder at a time: `fetchWorkspaceOnce` joins a run already going for the same folder
30
+ * instead of starting a second one, because two fetches in one clone collide on the ref lock.
31
+ * - Bounded: the git timeout from Settings → Bottom bar per repository, `MAX_REPOS_LIMIT` repositories at
32
+ * most, a hard wall-clock bound on each spawn, and `FETCH_CONCURRENCY` git processes at a time for the
33
+ * whole run: one `Limiter`, shared by the scheduler's tick, not one bound per folder.
34
+ * - Non-interactive: `GIT_TERMINAL_PROMPT=0` and BatchMode ssh, so a private repository that needs a
35
+ * login fails fast instead of hanging the request on an invisible username prompt. Codeman collects
36
+ * no credentials; a repository the user's own git is already signed in to works because that is the
37
+ * user's existing setup, so unlike a clone no credential helper is disabled here. That BatchMode ssh is
38
+ * a default, never an override: `fetchEnvFor` keeps whatever the repository's `core.sshCommand`, the
39
+ * user's own `GIT_SSH_COMMAND` or a `GIT_SSH` already says, because on a machine with several ssh setups
40
+ * that choice is usually the one that works. ⚠️ The default still leaves a record: `StrictHostKeyChecking=accept-new`
41
+ * writes an unknown host key into `~/.ssh/known_hosts` without asking: BatchMode stops a prompt, not a
42
+ * write.
43
+ * - The timeout kills the PROCESS GROUP, not just the `git` parent. A fetch over http spawns
44
+ * `git-remote-http` children, and `execFile`'s `timeout` signals the parent only, so the answer can go
45
+ * out while a helper is still running, and on a timer that repeats every tick with nobody watching.
46
+ * `spawn` with `detached: true` puts the tree in its own group so the escalation (SIGTERM, then SIGKILL
47
+ * `FETCH_KILL_ESCALATION_MS` later) reaches the children too, which is why the negative pid is used
48
+ * rather than `child.kill()`. Same shape as `runGit` in git-clone.ts, without that module's slot queue:
49
+ * fetch is bounded by `FETCH_CONCURRENCY`, not by a clone queue.
50
+ * - Failure text reaching a client is control-stripped and credential-redacted (`sanitizeGitOutput`),
51
+ * and the browser writes it with `textContent` only.
52
+ *
53
+ * @module git-fetch
54
+ */
55
+ import { type GitFailureCode } from './git-clone.js';
56
+ import { type Limiter, type GitRunner } from './git-workspace-status.js';
57
+ /**
58
+ * Git processes in flight at once, for one run, and for a whole scheduler tick, because the tick hands ONE
59
+ * bound to every run (see `createLimiter`): each fetch is one git process, and a folder can hold up to
60
+ * `MAX_REPOS_LIMIT`. The tick bounds its folders separately, because the same semaphore cannot serve both
61
+ * levels: a folder holding a slot would then wait for a slot its own fetch needs.
62
+ */
63
+ export declare const FETCH_CONCURRENCY = 4;
64
+ /**
65
+ * How long the process group gets to answer SIGTERM before the same escalation goes to SIGKILL. Kept as a
66
+ * const so a test can say how long it is willing to wait for the group to actually die.
67
+ */
68
+ export declare const FETCH_KILL_ESCALATION_MS = 3000;
69
+ /**
70
+ * Config overrides set ahead of the argv, so an unattended fetch cannot do more than move refs: no
71
+ * fsmonitor hook, and no automatic maintenance or gc. A fetch is the git command that can run a
72
+ * repository's own hooks (`reference-transaction`, and `pre-auto-gc` through the maintenance that follows a
73
+ * fetch), and a gc started by a timer in a repository an agent is working in is exactly the kind of extra
74
+ * write this module must not do. Kept as a const so a test can pin it without spawning git.
75
+ */
76
+ export declare const FETCH_CONFIG_ARGS: string[];
77
+ /** The interval the user can turn on (Settings → Bottom bar, and Session Options → Git): bounds and default. */
78
+ export declare const DEFAULT_GIT_FETCH_INTERVAL_SECONDS = 300;
79
+ export declare const MIN_GIT_FETCH_INTERVAL_SECONDS = 30;
80
+ export declare const MAX_GIT_FETCH_INTERVAL_SECONDS = 3600;
81
+ export interface GitFetchSettings {
82
+ enabled: boolean;
83
+ intervalSeconds: number;
84
+ }
85
+ /**
86
+ * A settings object as the scheduler should read it: absent means OFF, because nothing runs on a timer
87
+ * until the user turns this on, and a number outside the schema's bounds is clamped rather than trusted.
88
+ * Pure, so the reading itself is testable without a settings file.
89
+ */
90
+ export declare function gitFetchSettingsFrom(settings: Record<string, unknown> | undefined): GitFetchSettings;
91
+ /**
92
+ * The only git command this module runs. Kept as a function so a test can pin the verb: nothing here
93
+ * commits, checks out, merges or prunes, and no operand is interpolated: the repository is the process
94
+ * `cwd` and the argv is fixed.
95
+ */
96
+ export declare function buildFetchArgs(): string[];
97
+ /**
98
+ * The `git` process also gets the `FETCH_CONFIG_ARGS` overrides ahead of this argv; they are kept separate
99
+ * so pinning the verb here does not have to say anything about maintenance, and so both can be tested
100
+ * without spawning.
101
+ *
102
+ * A repository with no remote is a fact, not a fault. Git's wordings for it differ by version (2.47
103
+ * exits quietly and this code is never reached, older git says "no default remote repository"), so the
104
+ * branch is pinned on literal fixtures and the code the panel shows is the code this writes.
105
+ */
106
+ export type FetchFailureCode = GitFailureCode | 'NO_REMOTE';
107
+ export interface FetchFailure {
108
+ code: FetchFailureCode;
109
+ /** User-facing summary. */
110
+ message: string;
111
+ /** Tail of git's own stderr, control-stripped and credential-redacted. */
112
+ stderr: string;
113
+ }
114
+ /**
115
+ * A thrown git error as the panel should show it. `classifyGitFailure` (git-clone.ts) already knows the
116
+ * network reasons (auth, an unreachable host, a missing git), so they are reused here; only its timeout
117
+ * wording is written for clones, so the fetch path replaces it with what the user can actually do.
118
+ */
119
+ export declare function describeFetchFailure(err: unknown): FetchFailure;
120
+ /**
121
+ * The environment a fetch gets, as a pure decision so it can be pinned without spawning git.
122
+ *
123
+ * `gitNonInteractiveEnv` supplies the BatchMode default, and the default is only a default: git reads
124
+ * `GIT_SSH_COMMAND` from the environment ahead of the repository's `core.sshCommand`, so setting it would
125
+ * override a repository that already chose its own ssh (an alternative host, an identity file, a jump box)
126
+ * and break a setup that works today. The server's own `GIT_SSH` is the same kind of answer: git reads it
127
+ * after both of those (`GIT_SSH_COMMAND`, then `core.sshCommand`, then `GIT_SSH`), so a written default would
128
+ * hide it too. That is the only case where this module makes an ssh choice on the user's behalf, so it is the
129
+ * whole decision here: the caller supplies the value it read, and this decides between the user's
130
+ * environment and the repository's own answer.
131
+ */
132
+ export declare function fetchEnvFor(base: NodeJS.ProcessEnv, repoSshCommand: string | null): NodeJS.ProcessEnv;
133
+ /**
134
+ * The repository's own `core.sshCommand`, or null. One bounded, read-only `git config --get` per fetch:
135
+ * git would honour that config itself if we did not write `GIT_SSH_COMMAND` over it, so honouring it means
136
+ * not writing one. `--get` reads local and global config, which is the value git would have used, and no
137
+ * hook or filter runs on the way to it.
138
+ *
139
+ * The read happens inside the same slot and the same wall-clock bound as the fetch it serves, so it cannot
140
+ * add a fifth git process or push a fetch past the user's timeout: it spends part of it.
141
+ */
142
+ export declare function readRepoSshCommand(cwd: string, timeoutMs: number): Promise<string | null>;
143
+ /**
144
+ * Fetch is allowed to write refs, so this runner does not carry `--no-optional-locks`: fetch takes a
145
+ * required lock anyway.
146
+ *
147
+ * `spawn`, not `execFile`, because the bound has to reach the whole tree (see the fileoverview): the
148
+ * caller is answered at the timeout, and the SIGTERM → SIGKILL escalation is deliberately NOT cancelled
149
+ * when the answer goes out, because the promise is a promise about the answer while the group still has to
150
+ * die. That is the difference from `runGit` in git-clone.ts, which disarms the escalation at `finish` and so
151
+ * can leave a `git-remote-http` child running after a timed-out call.
152
+ *
153
+ * The thrown error keeps the shape `describeFetchFailure` reads: `stderr` for the reason, `message` for a
154
+ * spawn failure (`spawn git ENOENT` → GIT_MISSING), `killed` and `code` for the timeout. A non-zero exit
155
+ * rejects the way `execFile` would (`code` is then the exit code), because resolving it would report a real
156
+ * failure as a clean run with an empty reason.
157
+ *
158
+ * `timeoutMs` bounds the `core.sshCommand` read and the fetch TOGETHER: the spawn is armed with what the read
159
+ * left, so a `git` that never answers costs one timeout, not two. A read that used the whole bound answers
160
+ * as a timeout without starting the fetch.
161
+ */
162
+ export declare const runFetch: GitRunner;
163
+ export interface GitFetchEntry {
164
+ /** Folder name of the repository (its root's basename), as the panel names it. */
165
+ name: string;
166
+ /** The repository root relative to the working directory: `.`, `..`, `api`, `apps/web`. */
167
+ path: string;
168
+ ok: boolean;
169
+ failure?: FetchFailure;
170
+ }
171
+ export interface GitFetchReport {
172
+ /** `ok` when at least one repository was fetched (or attempted); the other states are as in the overview. */
173
+ state: 'ok' | 'not-a-repo' | 'unsupported' | 'error';
174
+ reason?: 'remote' | 'docker';
175
+ error?: string;
176
+ /** One entry per repository fetched, in the same order the panel lists them. */
177
+ repos: GitFetchEntry[];
178
+ /** More than `repoLimit` repositories were found; only the first were fetched. */
179
+ reposTruncated: boolean;
180
+ /** The most repositories this fetch would list (the caller's setting, or `MAX_REPOS`). */
181
+ repoLimit?: number;
182
+ /** When the last repository finished, so the panel can say what the "behind" number is now based on. */
183
+ fetchedAt: number;
184
+ }
185
+ /**
186
+ * What one run did, in the shape a session keeps and the Session Options Git row shows. `trigger` says
187
+ * whether the user clicked or the interval fired, because a number the user did not ask for should read
188
+ * differently from one they did.
189
+ */
190
+ /** What a run was, and why it happened. */
191
+ export type GitFetchTrigger = 'click' | 'interval';
192
+ export interface GitFetchStatus {
193
+ ranAt: number;
194
+ trigger: GitFetchTrigger;
195
+ /** As in the report: `ok`, or the state that stopped the run. */
196
+ state: GitFetchReport['state'];
197
+ /** Repositories this run covered. */
198
+ repos: number;
199
+ /** How many of them could not be fetched. */
200
+ failed: number;
201
+ /** The first failure, named: enough for one row without reprinting the whole list. */
202
+ firstFailure?: {
203
+ name: string;
204
+ code: FetchFailureCode;
205
+ };
206
+ }
207
+ /**
208
+ * One run, with the trigger that caused it. A joiner gets the run already going, so what it reports is that
209
+ * run's trigger rather than the one that made it ask: a click that joins an interval run did not fetch
210
+ * anything itself, and the row the panel shows has to describe the fetch that actually happened.
211
+ */
212
+ export interface GitFetchRun {
213
+ report: GitFetchReport;
214
+ trigger: GitFetchTrigger;
215
+ }
216
+ export declare function gitFetchStatusFrom(report: GitFetchReport, trigger: GitFetchTrigger): GitFetchStatus;
217
+ /** Options for the fetch path: the same repository rules and limits as an overview, without its cache clock. */
218
+ export interface GitFetchOptions {
219
+ git?: GitRunner;
220
+ home?: string;
221
+ dockerWorkspaces?: string[];
222
+ maxRepos?: number;
223
+ timeoutMs?: number;
224
+ /**
225
+ * The shared slot count for a whole scheduler tick. Passed in so `FETCH_CONCURRENCY` bounds the tick
226
+ * rather than each folder separately; without it, a tick over more than `FETCH_CONCURRENCY` folders could
227
+ * have that many git processes going at once inside each one.
228
+ */
229
+ limiter?: Limiter;
230
+ }
231
+ export declare const emptyReport: (state: GitFetchReport["state"], extra?: Partial<GitFetchReport>) => GitFetchReport;
232
+ /**
233
+ * Fetch every repository the panel would show for `cwd`, through the same resolver so the two cannot
234
+ * disagree. A repository that failed keeps its row with the reason, exactly as an unreadable repository
235
+ * stays in the status list: the panel must show that something was not fetched rather than quietly skip it.
236
+ */
237
+ export declare function fetchGitWorkspaceOverview(cwd: string, opts?: GitFetchOptions): Promise<GitFetchReport>;
238
+ export declare function fetchWorkspaceOnce(folder: string, opts: GitFetchOptions | undefined, trigger: GitFetchTrigger): Promise<GitFetchRun>;
239
+ /** Test seam: an in-memory lane is not cleared by stopping the scheduler, so a test has to clear it itself. */
240
+ export declare function clearGitFetchLane(): void;
241
+ //# sourceMappingURL=git-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-fetch.d.ts","sourceRoot":"","sources":["../src/git-fetch.ts"],"names":[],"mappings":"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"}