@rubytech/create-maxy-code 0.1.604 → 0.1.606

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 (256) hide show
  1. package/dist/__tests__/claude-trust-seed.test.js +125 -0
  2. package/dist/claude-trust-seed.js +52 -0
  3. package/dist/index.js +68 -33
  4. package/package.json +1 -1
  5. package/payload/platform/lib/routine-templates/dist/roster.d.ts +30 -2
  6. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  7. package/payload/platform/lib/routine-templates/dist/roster.js +76 -32
  8. package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
  9. package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +0 -0
  10. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +22 -1
  11. package/payload/platform/lib/routine-templates/src/roster.ts +108 -35
  12. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +18 -3
  13. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +10 -0
  14. package/payload/platform/plugins/connector/PLUGIN.md +9 -1
  15. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts +2 -0
  16. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.d.ts.map +1 -0
  17. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js +67 -0
  18. package/payload/platform/plugins/connector/mcp/dist/__tests__/queue-descriptor.test.js.map +1 -0
  19. package/payload/platform/plugins/connector/mcp/dist/index.js +36 -5
  20. package/payload/platform/plugins/connector/mcp/dist/index.js.map +1 -1
  21. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts +35 -0
  22. package/payload/platform/plugins/connector/mcp/dist/lib/store.d.ts.map +1 -1
  23. package/payload/platform/plugins/connector/mcp/dist/lib/store.js +33 -0
  24. package/payload/platform/plugins/connector/mcp/dist/lib/store.js.map +1 -1
  25. package/payload/platform/plugins/connector/mcp/package.json +4 -2
  26. package/payload/platform/plugins/connector/mcp/vitest.config.ts +8 -0
  27. package/payload/platform/plugins/docs/references/admin-ui.md +15 -2
  28. package/payload/platform/plugins/docs/references/platform.md +2 -0
  29. package/payload/platform/plugins/email/PLUGIN.md +4 -0
  30. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts +2 -0
  31. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js +233 -0
  33. package/payload/platform/plugins/email/mcp/dist/sampler/__tests__/retention.test.js.map +1 -0
  34. package/payload/platform/plugins/email/mcp/dist/sampler/main.js +19 -0
  35. package/payload/platform/plugins/email/mcp/dist/sampler/main.js.map +1 -1
  36. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts +3 -0
  37. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.d.ts.map +1 -0
  38. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js +146 -0
  39. package/payload/platform/plugins/email/mcp/dist/sampler/retention-sources.js.map +1 -0
  40. package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts +150 -0
  41. package/payload/platform/plugins/email/mcp/dist/sampler/retention.d.ts.map +1 -0
  42. package/payload/platform/plugins/email/mcp/dist/sampler/retention.js +199 -0
  43. package/payload/platform/plugins/email/mcp/dist/sampler/retention.js.map +1 -0
  44. package/payload/platform/plugins/scheduling/PLUGIN.md +8 -1
  45. package/payload/platform/plugins/scheduling/mcp/dist/index.js +1 -1
  46. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  47. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +40 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -1
  49. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +322 -4
  50. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -1
  51. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  52. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +21 -1
  53. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  54. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +236 -6
  55. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +452 -7
  57. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -1
  58. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts +34 -0
  59. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.d.ts.map +1 -0
  60. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js +83 -0
  61. package/payload/platform/services/claude-session-manager/dist/claude-trust-census.js.map +1 -0
  62. package/payload/platform/services/claude-session-manager/dist/index.js +13 -0
  63. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  64. package/payload/server/public/activity.html +6 -6
  65. package/payload/server/public/agents.html +5 -5
  66. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js +1 -0
  67. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.br +0 -0
  68. package/payload/server/public/assets/AdminLoginScreens-DuQ3ax5U.js.gz +0 -0
  69. package/payload/server/public/assets/AdminShell-1HyATV_t.js +3 -0
  70. package/payload/server/public/assets/AdminShell-1HyATV_t.js.br +0 -0
  71. package/payload/server/public/assets/AdminShell-1HyATV_t.js.gz +0 -0
  72. package/payload/server/public/assets/activity-C2qWlcJ5.js +1 -0
  73. package/payload/server/public/assets/activity-C2qWlcJ5.js.br +0 -0
  74. package/payload/server/public/assets/activity-C2qWlcJ5.js.gz +0 -0
  75. package/payload/server/public/assets/admin-DqpfY8-t.js +1 -0
  76. package/payload/server/public/assets/admin-DqpfY8-t.js.br +0 -0
  77. package/payload/server/public/assets/admin-DqpfY8-t.js.gz +0 -0
  78. package/payload/server/public/assets/agents-DoGVAjIT.js +1 -0
  79. package/payload/server/public/assets/agents-DoGVAjIT.js.br +0 -0
  80. package/payload/server/public/assets/agents-DoGVAjIT.js.gz +0 -0
  81. package/payload/server/public/assets/{browser-CqDBcKwU.js → browser-DjqS897k.js} +1 -1
  82. package/payload/server/public/assets/browser-DjqS897k.js.br +0 -0
  83. package/payload/server/public/assets/browser-DjqS897k.js.gz +0 -0
  84. package/payload/server/public/assets/calendar-DJeOm0WS.js +1 -0
  85. package/payload/server/public/assets/calendar-DJeOm0WS.js.br +0 -0
  86. package/payload/server/public/assets/calendar-DJeOm0WS.js.gz +0 -0
  87. package/payload/server/public/assets/chat-BrRE0Dck.js +1 -0
  88. package/payload/server/public/assets/chat-BrRE0Dck.js.br +0 -0
  89. package/payload/server/public/assets/chat-BrRE0Dck.js.gz +0 -0
  90. package/payload/server/public/assets/chevron-left-D9z1aK3p.js +1 -0
  91. package/payload/server/public/assets/chevron-left-D9z1aK3p.js.br +0 -0
  92. package/payload/server/public/assets/clock-BqHWMICF.js +1 -0
  93. package/payload/server/public/assets/clock-BqHWMICF.js.br +0 -0
  94. package/payload/server/public/assets/clock-BqHWMICF.js.gz +0 -0
  95. package/payload/server/public/assets/copy-p9ZM9fnn.js +1 -0
  96. package/payload/server/public/assets/copy-p9ZM9fnn.js.br +0 -0
  97. package/payload/server/public/assets/copy-p9ZM9fnn.js.gz +0 -0
  98. package/payload/server/public/assets/data-CPsqW0qW.js +1 -0
  99. package/payload/server/public/assets/data-CPsqW0qW.js.br +0 -0
  100. package/payload/server/public/assets/data-CPsqW0qW.js.gz +0 -0
  101. package/payload/server/public/assets/{field-X4OVA3vx.js → field-WRk67lvr.js} +1 -1
  102. package/payload/server/public/assets/field-WRk67lvr.js.br +0 -0
  103. package/payload/server/public/assets/field-WRk67lvr.js.gz +0 -0
  104. package/payload/server/public/assets/file-text-BJDCb334.js +1 -0
  105. package/payload/server/public/assets/file-text-BJDCb334.js.br +0 -0
  106. package/payload/server/public/assets/file-text-BJDCb334.js.gz +0 -0
  107. package/payload/server/public/assets/{graph-l7tzHqvm.js → graph-CAU2xh2Z.js} +2 -2
  108. package/payload/server/public/assets/graph-CAU2xh2Z.js.br +0 -0
  109. package/payload/server/public/assets/graph-CAU2xh2Z.js.gz +0 -0
  110. package/payload/server/public/assets/graph-labels-CEDmX7qW.js +1 -0
  111. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.br +0 -0
  112. package/payload/server/public/assets/graph-labels-CEDmX7qW.js.gz +0 -0
  113. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css → jsx-runtime-hPFGo4rC.css} +1 -1
  114. package/payload/server/public/assets/jsx-runtime-hPFGo4rC.css.br +0 -0
  115. package/payload/server/public/assets/{jsx-runtime-bq0g4NlA.css.gz → jsx-runtime-hPFGo4rC.css.gz} +0 -0
  116. package/payload/server/public/assets/{operator-Dg-dwf7a.js → operator-1WFztEQA.js} +1 -1
  117. package/payload/server/public/assets/operator-1WFztEQA.js.br +0 -0
  118. package/payload/server/public/assets/operator-1WFztEQA.js.gz +0 -0
  119. package/payload/server/public/assets/page-B72XigcU.js +32 -0
  120. package/payload/server/public/assets/page-B72XigcU.js.br +0 -0
  121. package/payload/server/public/assets/page-B72XigcU.js.gz +0 -0
  122. package/payload/server/public/assets/page-nT8l_cX1.js +1 -0
  123. package/payload/server/public/assets/page-nT8l_cX1.js.br +0 -0
  124. package/payload/server/public/assets/page-nT8l_cX1.js.gz +0 -0
  125. package/payload/server/public/assets/play-CYqOCqm6.js +1 -0
  126. package/payload/server/public/assets/play-CYqOCqm6.js.br +0 -0
  127. package/payload/server/public/assets/play-CYqOCqm6.js.gz +0 -0
  128. package/payload/server/public/assets/public-DAea6w7w.js +1 -0
  129. package/payload/server/public/assets/public-DAea6w7w.js.br +0 -0
  130. package/payload/server/public/assets/public-DAea6w7w.js.gz +0 -0
  131. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js +1 -0
  132. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.br +0 -0
  133. package/payload/server/public/assets/rotate-ccw-CqlRlGsY.js.gz +0 -0
  134. package/payload/server/public/assets/{routines-6fxZNacQ.js → routines-vy5xsyqx.js} +2 -2
  135. package/payload/server/public/assets/routines-vy5xsyqx.js.br +0 -0
  136. package/payload/server/public/assets/routines-vy5xsyqx.js.gz +0 -0
  137. package/payload/server/public/assets/{skills-51mqx-s5.js → skills-X7FTilVy.js} +1 -1
  138. package/payload/server/public/assets/skills-X7FTilVy.js.br +0 -0
  139. package/payload/server/public/assets/skills-X7FTilVy.js.gz +0 -0
  140. package/payload/server/public/assets/tasks-BXx_jk_p.js +3 -0
  141. package/payload/server/public/assets/tasks-BXx_jk_p.js.br +0 -0
  142. package/payload/server/public/assets/tasks-BXx_jk_p.js.gz +0 -0
  143. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js +1 -0
  144. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.br +0 -0
  145. package/payload/server/public/assets/triangle-alert-CuWcGaRo.js.gz +0 -0
  146. package/payload/server/public/assets/{useCopyFeedback-BG7h37Rx.js → useCopyFeedback-DLSKs6r2.js} +1 -1
  147. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.br +0 -0
  148. package/payload/server/public/assets/useCopyFeedback-DLSKs6r2.js.gz +0 -0
  149. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js +6 -0
  150. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.br +0 -0
  151. package/payload/server/public/assets/useMediaQuery-C_1CV_iW.js.gz +0 -0
  152. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js +2 -0
  153. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.br +0 -0
  154. package/payload/server/public/assets/useVoiceRecorder-DBzM_HIx.js.gz +0 -0
  155. package/payload/server/public/assets/wrench-nDMQ6x-6.js +1 -0
  156. package/payload/server/public/assets/wrench-nDMQ6x-6.js.br +0 -0
  157. package/payload/server/public/assets/wrench-nDMQ6x-6.js.gz +0 -0
  158. package/payload/server/public/browser.html +5 -5
  159. package/payload/server/public/calendar.html +8 -8
  160. package/payload/server/public/chat.html +14 -14
  161. package/payload/server/public/data.html +12 -12
  162. package/payload/server/public/field.html +3 -3
  163. package/payload/server/public/graph.html +10 -10
  164. package/payload/server/public/index.html +14 -14
  165. package/payload/server/public/operator.html +16 -16
  166. package/payload/server/public/public.html +14 -14
  167. package/payload/server/public/routines.html +8 -8
  168. package/payload/server/public/skills.html +6 -6
  169. package/payload/server/public/tasks.html +7 -7
  170. package/payload/server/server.js +1 -1
  171. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js +0 -1
  172. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.br +0 -0
  173. package/payload/server/public/assets/AdminLoginScreens-DI5bNlX8.js.gz +0 -0
  174. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js +0 -3
  175. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.br +0 -0
  176. package/payload/server/public/assets/AdminShell-B-iX9ahJ.js.gz +0 -0
  177. package/payload/server/public/assets/activity-CqgFwANo.js +0 -1
  178. package/payload/server/public/assets/activity-CqgFwANo.js.br +0 -0
  179. package/payload/server/public/assets/activity-CqgFwANo.js.gz +0 -0
  180. package/payload/server/public/assets/admin-K63n6ybW.js +0 -1
  181. package/payload/server/public/assets/admin-K63n6ybW.js.br +0 -0
  182. package/payload/server/public/assets/admin-K63n6ybW.js.gz +0 -0
  183. package/payload/server/public/assets/agents-cue5Zf8X.js +0 -1
  184. package/payload/server/public/assets/agents-cue5Zf8X.js.br +0 -0
  185. package/payload/server/public/assets/agents-cue5Zf8X.js.gz +0 -0
  186. package/payload/server/public/assets/browser-CqDBcKwU.js.br +0 -0
  187. package/payload/server/public/assets/browser-CqDBcKwU.js.gz +0 -0
  188. package/payload/server/public/assets/calendar-BgzuW5Rt.js +0 -1
  189. package/payload/server/public/assets/calendar-BgzuW5Rt.js.br +0 -0
  190. package/payload/server/public/assets/calendar-BgzuW5Rt.js.gz +0 -0
  191. package/payload/server/public/assets/chat-HYZViu2u.js +0 -1
  192. package/payload/server/public/assets/chat-HYZViu2u.js.br +0 -3
  193. package/payload/server/public/assets/chat-HYZViu2u.js.gz +0 -0
  194. package/payload/server/public/assets/chevron-left-C5FXH9r_.js +0 -1
  195. package/payload/server/public/assets/chevron-left-C5FXH9r_.js.br +0 -0
  196. package/payload/server/public/assets/clock-DxPtyejC.js +0 -1
  197. package/payload/server/public/assets/clock-DxPtyejC.js.br +0 -0
  198. package/payload/server/public/assets/clock-DxPtyejC.js.gz +0 -0
  199. package/payload/server/public/assets/copy-BNhZ0flH.js +0 -1
  200. package/payload/server/public/assets/copy-BNhZ0flH.js.br +0 -0
  201. package/payload/server/public/assets/copy-BNhZ0flH.js.gz +0 -0
  202. package/payload/server/public/assets/data-kCBB8Z4Z.js +0 -1
  203. package/payload/server/public/assets/data-kCBB8Z4Z.js.br +0 -0
  204. package/payload/server/public/assets/data-kCBB8Z4Z.js.gz +0 -0
  205. package/payload/server/public/assets/field-X4OVA3vx.js.br +0 -0
  206. package/payload/server/public/assets/field-X4OVA3vx.js.gz +0 -0
  207. package/payload/server/public/assets/file-text-BddO_2EI.js +0 -1
  208. package/payload/server/public/assets/file-text-BddO_2EI.js.br +0 -0
  209. package/payload/server/public/assets/file-text-BddO_2EI.js.gz +0 -0
  210. package/payload/server/public/assets/graph-l7tzHqvm.js.br +0 -0
  211. package/payload/server/public/assets/graph-l7tzHqvm.js.gz +0 -0
  212. package/payload/server/public/assets/graph-labels-CtFFq5W2.js +0 -1
  213. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.br +0 -0
  214. package/payload/server/public/assets/graph-labels-CtFFq5W2.js.gz +0 -0
  215. package/payload/server/public/assets/jsx-runtime-bq0g4NlA.css.br +0 -0
  216. package/payload/server/public/assets/operator-Dg-dwf7a.js.br +0 -7
  217. package/payload/server/public/assets/operator-Dg-dwf7a.js.gz +0 -0
  218. package/payload/server/public/assets/page-Ci1bfIbs.js +0 -1
  219. package/payload/server/public/assets/page-Ci1bfIbs.js.br +0 -0
  220. package/payload/server/public/assets/page-Ci1bfIbs.js.gz +0 -0
  221. package/payload/server/public/assets/page-Y9JmFq0Q.js +0 -32
  222. package/payload/server/public/assets/page-Y9JmFq0Q.js.br +0 -0
  223. package/payload/server/public/assets/page-Y9JmFq0Q.js.gz +0 -0
  224. package/payload/server/public/assets/play-BSZVGpQT.js +0 -1
  225. package/payload/server/public/assets/play-BSZVGpQT.js.br +0 -0
  226. package/payload/server/public/assets/play-BSZVGpQT.js.gz +0 -0
  227. package/payload/server/public/assets/public-m66FAza_.js +0 -1
  228. package/payload/server/public/assets/public-m66FAza_.js.br +0 -0
  229. package/payload/server/public/assets/public-m66FAza_.js.gz +0 -0
  230. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js +0 -1
  231. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.br +0 -0
  232. package/payload/server/public/assets/rotate-ccw-DTOJmOxB.js.gz +0 -0
  233. package/payload/server/public/assets/routines-6fxZNacQ.js.br +0 -0
  234. package/payload/server/public/assets/routines-6fxZNacQ.js.gz +0 -0
  235. package/payload/server/public/assets/skills-51mqx-s5.js.br +0 -0
  236. package/payload/server/public/assets/skills-51mqx-s5.js.gz +0 -0
  237. package/payload/server/public/assets/tasks-D4y-Cwa3.js +0 -3
  238. package/payload/server/public/assets/tasks-D4y-Cwa3.js.br +0 -0
  239. package/payload/server/public/assets/tasks-D4y-Cwa3.js.gz +0 -0
  240. package/payload/server/public/assets/triangle-alert-BzCjDFts.js +0 -1
  241. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.br +0 -0
  242. package/payload/server/public/assets/triangle-alert-BzCjDFts.js.gz +0 -0
  243. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.br +0 -2
  244. package/payload/server/public/assets/useCopyFeedback-BG7h37Rx.js.gz +0 -0
  245. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js +0 -6
  246. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.br +0 -0
  247. package/payload/server/public/assets/useMediaQuery-CqwktQr_.js.gz +0 -0
  248. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js +0 -2
  249. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.br +0 -0
  250. package/payload/server/public/assets/useVoiceRecorder-CAZcUc1s.js.gz +0 -0
  251. package/payload/server/public/assets/wrench-B2WKLLQG.js +0 -1
  252. package/payload/server/public/assets/wrench-B2WKLLQG.js.br +0 -0
  253. package/payload/server/public/assets/wrench-B2WKLLQG.js.gz +0 -0
  254. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js → jsx-runtime-CvVXOszc.js} +0 -0
  255. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.br → jsx-runtime-CvVXOszc.js.br} +0 -0
  256. /package/payload/server/public/assets/{jsx-runtime-UK6hgBWO.js.gz → jsx-runtime-CvVXOszc.js.gz} +0 -0
@@ -19,12 +19,37 @@
19
19
  * on every surface, which is the Task 2313 class this must not recreate.
20
20
  */
21
21
  import { createHash } from "node:crypto";
22
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
23
- import { dirname, resolve } from "node:path";
22
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
23
+ import { dirname, join, resolve } from "node:path";
24
24
  import { int } from "neo4j-driver";
25
25
  import { spawnProviderCall } from "../lib/connector-freebusy.js";
26
26
  import { unflaggedLabel } from "../lib/gate-flag.js";
27
27
  import { getSession } from "../lib/neo4j.js";
28
+ /**
29
+ * Task 2689 — how long after its newest report the memory sweep counts as
30
+ * stale.
31
+ *
32
+ * The sweep runs hourly and `graph-health`'s own silence condition is "the
33
+ * sweep ran within the past day", so 26 hours gives it two missed runs of slack
34
+ * before the routine is woken.
35
+ */
36
+ export const SWEEP_STALE_AFTER_MS = 26 * 60 * 60 * 1000;
37
+ /**
38
+ * Task 2689 — one week of growth worth a person's attention on a Pi-class disk.
39
+ *
40
+ * The shipped `filesystem-health` template names its threshold explicitly; a
41
+ * gate written by hand that omits the field inherits this.
42
+ */
43
+ export const FS_DEFAULT_MIN_GROWTH_BYTES = 64 * 1024 * 1024;
44
+ /**
45
+ * Task 2689 — the most files one account-directory measurement walks.
46
+ *
47
+ * A bound, not a budget. A directory above it is exactly what
48
+ * `filesystem-health` exists to report, and the walk that stopped there has
49
+ * measured a floor rather than a total, so the check reports work and records
50
+ * no position.
51
+ */
52
+ export const FS_WALK_ENTRY_CAP = 200_000;
28
53
  /**
29
54
  * Task 2684 — the most node ids one `graph-unflagged` check hands over per fire.
30
55
  *
@@ -53,7 +78,19 @@ export const ACCEPTED_CHECK_TYPES = [
53
78
  "telegram",
54
79
  "graph",
55
80
  "graph-unflagged",
81
+ "connector",
82
+ "sweep",
83
+ "filesystem",
56
84
  ];
85
+ /**
86
+ * Task 2688 — the accepted `scope` values on a `connector` check, in one place.
87
+ *
88
+ * The same single-sourcing `ACCEPTED_MAIL_PROVIDERS` has: the probe asks the
89
+ * registry for queues of this kind, and `validateGate` refuses anything outside
90
+ * it at write time. A scope outside the set matches no queue, so the gate would
91
+ * answer quiet on every fire and the routine could never run.
92
+ */
93
+ export const ACCEPTED_CONNECTOR_SCOPES = ["operational", "accounting"];
57
94
  /**
58
95
  * Task 2657 — the accepted `provider` values on a `mail` check, in one place.
59
96
  *
@@ -88,6 +125,9 @@ export const ACCEPTED_CHECK_FIELDS = {
88
125
  telegram: ["type", "scope", "botId"],
89
126
  graph: ["type", "scope"],
90
127
  "graph-unflagged": ["type", "label"],
128
+ connector: ["type", "scope"],
129
+ sweep: ["type"],
130
+ filesystem: ["type", "minGrowthBytes"],
91
131
  };
92
132
  /**
93
133
  * Rooted at the INSTALL, not at the account.
@@ -241,6 +281,10 @@ export function slotKey(slot) {
241
281
  // commit over two bots names two entries rather than one repeated twice.
242
282
  if (slot.kind === "telegram" && slot.botId !== undefined)
243
283
  return `telegram:${slot.botId}`;
284
+ // Task 2688 — the connector's name, so a commit over two connectors names two
285
+ // entries rather than one repeated twice.
286
+ if (slot.kind === "connector")
287
+ return `connector:${slot.key}`;
244
288
  return slot.kind;
245
289
  }
246
290
  /**
@@ -273,6 +317,19 @@ export function slotPosition(slot, marks) {
273
317
  // The ISO `updatedAt` verbatim, as the Telegram position is: it IS the
274
318
  // value the comparison is made on.
275
319
  return marks.graph?.updatedAt ?? null;
320
+ case "connector":
321
+ // The ISO instant sent as the queue's changed-since argument, printed
322
+ // verbatim for the same reason.
323
+ return marks.connector?.[slot.key]?.changedSince ?? null;
324
+ case "sweep":
325
+ // Truncated for the reason the calendar digest is: the whole sha256 tells
326
+ // a reader nothing the first eight do not, and the file holds it all.
327
+ return marks.sweep?.digest?.slice(0, 8) ?? null;
328
+ case "filesystem":
329
+ // The optional chain, as every arm above has it: a file holding
330
+ // `"filesystem": {}` would otherwise print the string "undefined" as this
331
+ // routine's position in `op=gate-read` and in `positions=`.
332
+ return marks.filesystem?.bytes === undefined ? null : String(marks.filesystem.bytes);
276
333
  }
277
334
  }
278
335
  /**
@@ -299,6 +356,12 @@ export function slotsIn(marks) {
299
356
  slots.push({ kind: "calendar" });
300
357
  if (marks.graph?.updatedAt !== undefined)
301
358
  slots.push({ kind: "graph" });
359
+ for (const key of Object.keys(marks.connector ?? {}))
360
+ slots.push({ kind: "connector", key });
361
+ if (marks.sweep?.digest !== undefined)
362
+ slots.push({ kind: "sweep" });
363
+ if (marks.filesystem?.bytes !== undefined)
364
+ slots.push({ kind: "filesystem" });
302
365
  return slots;
303
366
  }
304
367
  /**
@@ -409,6 +472,107 @@ export function promoteSlot(slot, from, into) {
409
472
  into.graph = { ...(into.graph ?? {}), updatedAt };
410
473
  return true;
411
474
  }
475
+ case "connector": {
476
+ const entry = from.connector?.[slot.key];
477
+ if (entry === undefined)
478
+ return false;
479
+ into.connector = { ...(into.connector ?? {}), [slot.key]: entry };
480
+ return true;
481
+ }
482
+ case "sweep": {
483
+ const digest = from.sweep?.digest;
484
+ if (digest === undefined)
485
+ return false;
486
+ into.sweep = { ...(into.sweep ?? {}), digest };
487
+ return true;
488
+ }
489
+ case "filesystem": {
490
+ const bytes = from.filesystem?.bytes;
491
+ if (bytes === undefined)
492
+ return false;
493
+ into.filesystem = { ...(into.filesystem ?? {}), bytes };
494
+ return true;
495
+ }
496
+ }
497
+ }
498
+ /**
499
+ * Task 2689 — the memory sweep's state as one comparable value.
500
+ *
501
+ * The two review-queue counts and a staleness flag, never the newest report's
502
+ * identity. The sweep runs hourly and `graph-health` is proposed weekly, so a
503
+ * digest carrying the report id would differ on essentially every fire and the
504
+ * gate could never go quiet. A digest over the queue counts alone would go
505
+ * quiet correctly, but a sweep that STOPPED running moves no queue, so the
506
+ * routine could never report the one failure it exists to catch. The flag gives
507
+ * both: a constant value while the sweep is healthy, one change at the
508
+ * transition into staleness, and one more on the way out.
509
+ *
510
+ * `now` is a parameter rather than a `Date.now()` read inside, so the boundary
511
+ * is testable without moving the clock. A `newestReportAt` this cannot parse
512
+ * throws, through `instantOf`, and a throw counts as work.
513
+ */
514
+ export function digestSweepState(state, now) {
515
+ const stale = state.newestReportAt === null || now - instantOf(state.newestReportAt) > SWEEP_STALE_AFTER_MS;
516
+ return createHash("sha256").update(`${state.orphans}|${state.citations}|${stale}`).digest("hex");
517
+ }
518
+ /**
519
+ * Task 2689 — total bytes of the regular files under `root`.
520
+ *
521
+ * A symlink is never followed, so the walk cannot leave the account directory
522
+ * even when something inside points out of it — the rule the filesystem
523
+ * plugin's own walker holds to, and the reason this does not shell out to `du`.
524
+ *
525
+ * A vanished entry (`ENOENT` on a stat, or on a nested directory read) is
526
+ * SKIPPED: an account directory is written to continuously, and a file deleted
527
+ * between the read and the stat contributes nothing to a size. Every other
528
+ * error propagates. The ROOT is read outside that rule, so a missing account
529
+ * directory throws: a wrongly derived root that read zero would answer "nothing
530
+ * grew" for ever, which is the permanent mute this design forbids.
531
+ */
532
+ export function measureAccountBytes(root, cap) {
533
+ let bytes = 0;
534
+ // Every entry the walk touches, not only the files. Counting files alone
535
+ // would leave the directories unbounded, and the pending stack grows by one
536
+ // per directory: a tree that is wide in directories and thin in files would
537
+ // then walk all of it with the cap never reached.
538
+ let seen = 0;
539
+ const pending = [];
540
+ let dir = root;
541
+ let entries = readdirSync(root, { withFileTypes: true });
542
+ for (;;) {
543
+ for (const entry of entries) {
544
+ if (entry.isSymbolicLink())
545
+ continue;
546
+ if (seen >= cap)
547
+ return { bytes, capped: true };
548
+ seen++;
549
+ const full = join(dir, entry.name);
550
+ if (entry.isDirectory()) {
551
+ pending.push(full);
552
+ continue;
553
+ }
554
+ if (!entry.isFile())
555
+ continue;
556
+ try {
557
+ bytes += statSync(full).size;
558
+ }
559
+ catch (err) {
560
+ if (err.code !== "ENOENT")
561
+ throw err;
562
+ }
563
+ }
564
+ const next = pending.pop();
565
+ if (next === undefined)
566
+ return { bytes, capped: false };
567
+ dir = next;
568
+ try {
569
+ entries = readdirSync(dir, { withFileTypes: true });
570
+ }
571
+ catch (err) {
572
+ if (err.code !== "ENOENT")
573
+ throw err;
574
+ entries = [];
575
+ }
412
576
  }
413
577
  }
414
578
  // ---------------------------------------------------------------------------
@@ -623,6 +787,30 @@ const realMail = async (check, sinceUid, accountId) => {
623
787
  }
624
788
  }
625
789
  };
790
+ /**
791
+ * Task 2688 — one call to the connector plugin's own tools.
792
+ *
793
+ * A spawn rather than a loopback route or a direct read of the registry files,
794
+ * because the credential, the host binding and the SSRF guard all live inside
795
+ * that plugin, and reimplementing any of them here would put a second copy of a
796
+ * security control in a second package. It is the transport the Outlook mail
797
+ * probe already uses from this same heartbeat: same timeout, same ACCOUNT_ID
798
+ * scoping, same captured child stderr, same PLATFORM_PORT bridge.
799
+ *
800
+ * The import is dynamic because `lib/gate.ts` imports this module's types; a
801
+ * static value import would close that loop at run time.
802
+ */
803
+ async function callConnectorTool(accountId, tool, args) {
804
+ const { callPluginTool } = await import("../lib/gate.js");
805
+ const call = await callPluginTool("connector", tool, JSON.stringify(args), accountId);
806
+ const text = (Array.isArray(call.result.content) ? call.result.content : [])
807
+ .filter((c) => c.type === "text")
808
+ .map((c) => String(c.text))
809
+ .join("\n");
810
+ if (call.result.isError === true)
811
+ throw new Error(`${tool} returned an error: ${text}`);
812
+ return JSON.parse(text);
813
+ }
626
814
  export const REAL_SOURCES = {
627
815
  mail: realMail,
628
816
  // Task 2657 — the provider owns the decryption of its own tokens, so this is
@@ -796,6 +984,122 @@ export const REAL_SOURCES = {
796
984
  await session.close();
797
985
  }
798
986
  },
987
+ connectorQueues: async (accountId, kind) => {
988
+ // The connector plugin is optional. Its absence is an ANSWER (nothing is
989
+ // registered, so nothing registered can have moved), so it is detected
990
+ // before any spawn rather than by catching the transport error, which is
991
+ // indistinguishable from a child that crashed on boot.
992
+ const serverPath = resolve(process.env.PLATFORM_ROOT, "plugins", "connector", "mcp", "dist", "index.js");
993
+ if (!existsSync(serverPath))
994
+ return { pluginPresent: false, bindings: [] };
995
+ const payload = await callConnectorTool(accountId, "connector-list", {});
996
+ if (!Array.isArray(payload)) {
997
+ throw new Error(`connector-list returned ${typeof payload}, expected an array`);
998
+ }
999
+ const bindings = [];
1000
+ for (const entry of payload) {
1001
+ const e = entry;
1002
+ const q = e.queue;
1003
+ if (typeof e.name !== "string" || !q || q.kind !== kind)
1004
+ continue;
1005
+ // A descriptor missing a field it needs THROWS rather than being called
1006
+ // with `undefined` in the URL: that call would 404, the 404 would count as
1007
+ // work, and the routine would wake on every fire with nothing to show for
1008
+ // it. The throw names the connector once and counts as work once.
1009
+ if (typeof q.path !== "string" ||
1010
+ typeof q.changedSinceParam !== "string" ||
1011
+ typeof q.itemsField !== "string") {
1012
+ throw new Error(`connector ${e.name} carries an incomplete queue descriptor`);
1013
+ }
1014
+ bindings.push({ name: e.name, queue: q });
1015
+ }
1016
+ return { pluginPresent: true, bindings };
1017
+ },
1018
+ connectorQueueRead: async (accountId, binding, changedSince) => {
1019
+ const payload = await callConnectorTool(accountId, "connector-call", {
1020
+ name: binding.name,
1021
+ method: "GET",
1022
+ path: binding.queue.path,
1023
+ query: { [binding.queue.changedSinceParam]: changedSince },
1024
+ });
1025
+ const answer = payload;
1026
+ if (typeof answer.status !== "number") {
1027
+ throw new Error(`connector-call returned no status for ${binding.name}`);
1028
+ }
1029
+ // Every non-2xx is handed back with its status and no count. The probe
1030
+ // decides which of them is an answer (401 and 403) and which is a failure,
1031
+ // so that rule lives in one place rather than being split across the source
1032
+ // and its caller.
1033
+ if (answer.status < 200 || answer.status >= 300) {
1034
+ return { status: answer.status, items: 0, truncated: false };
1035
+ }
1036
+ // A truncated body cannot be counted — the array it carries is a prefix of
1037
+ // the real one, and the cut usually lands mid-token, so it will not even
1038
+ // parse. It is reported rather than thrown, because the cap is itself proof
1039
+ // the queue moved: the probe counts it as work and records the position, and
1040
+ // a throw would record none, leaving the same window to truncate again on
1041
+ // every fire for ever.
1042
+ if (answer.truncated === true)
1043
+ return { status: answer.status, items: 0, truncated: true };
1044
+ const parsed = typeof answer.body === "string" ? JSON.parse(answer.body) : answer.body;
1045
+ const items = parsed?.[binding.queue.itemsField];
1046
+ // A shape this cannot read throws, and a throw counts as work. Coercing it
1047
+ // to zero would answer "quiet" about a queue this never understood, which
1048
+ // is the one answer that loses a record.
1049
+ if (!Array.isArray(items)) {
1050
+ throw new Error(`connector ${binding.name} queue field ${binding.queue.itemsField} is not an array`);
1051
+ }
1052
+ return { status: answer.status, items: items.length, truncated: false };
1053
+ },
1054
+ sweepState: async (accountId) => {
1055
+ // Three reads on this install's own Neo4j, the way `calendarDigest` reads
1056
+ // `:ConnectorBusy`: the nodes are local and there is no cross-plugin call.
1057
+ // `dream-cycle` is the keyword `memory-dream-run` puts on every report it
1058
+ // writes, and it is the same keyword `graph-health`'s own prompt reads
1059
+ // through `memory-report-read-latest`.
1060
+ const session = getSession();
1061
+ try {
1062
+ const reportRes = await session.run(`MATCH (r:Report)
1063
+ WHERE r.accountId = $accountId AND 'dream-cycle' IN r.keywords
1064
+ RETURN toString(r.occurredAt) AS at
1065
+ ORDER BY r.occurredAt DESC LIMIT 1`, { accountId });
1066
+ const at = reportRes.records[0]?.get("at") ?? null;
1067
+ // An account whose sweep has never run holds no report, which reads as
1068
+ // stale. A value this cannot read THROWS rather than coercing to null:
1069
+ // a throw counts as work, while coercing would report a sweep age this
1070
+ // never established.
1071
+ if (at !== null && typeof at !== "string") {
1072
+ throw new Error(`sweep report occurredAt is type ${typeof at}, expected an ISO string`);
1073
+ }
1074
+ // The labels are literals in this file, never operator input, so
1075
+ // interpolating them cannot inject; the account is a parameter.
1076
+ const count = async (label) => {
1077
+ const res = await session.run(`MATCH (n:${label}) WHERE n.accountId = $accountId RETURN count(n) AS c`, { accountId });
1078
+ // neo4j returns a Cypher Integer. `Number` on a shape this cannot read
1079
+ // is NaN, which would digest as the string "NaN" and then never change
1080
+ // again — a gate that has gone quiet for the wrong reason.
1081
+ const n = Number(res.records[0]?.get("c") ?? 0);
1082
+ if (!Number.isFinite(n))
1083
+ throw new Error(`${label} count is not a number`);
1084
+ return n;
1085
+ };
1086
+ return {
1087
+ newestReportAt: at,
1088
+ orphans: await count("OrphanCandidate"),
1089
+ citations: await count("CitationProposal"),
1090
+ };
1091
+ }
1092
+ finally {
1093
+ await session.close();
1094
+ }
1095
+ },
1096
+ filesystemBytes: async (accountId) => {
1097
+ // `$PLATFORM_ROOT/../data/accounts` is the derivation `dispatcher-env.ts`
1098
+ // and `agent-dispatch.ts` already use. Re-deriving it differently here is
1099
+ // how the gate would come to measure a directory the routine never reads.
1100
+ const root = resolve(process.env.PLATFORM_ROOT, "..", "data/accounts", accountId);
1101
+ return measureAccountBytes(root, FS_WALK_ENTRY_CAP);
1102
+ },
799
1103
  calendarDigest: async (check, accountId) => {
800
1104
  const session = getSession();
801
1105
  try {
@@ -860,6 +1164,18 @@ export function checkSource(check) {
860
1164
  // going unreadable is not the other.
861
1165
  case "graph-unflagged":
862
1166
  return `graph:${check.label}`;
1167
+ // Task 2688 — the scope rather than a connector name: which connectors the
1168
+ // scope resolves to is a property of the account and can change between
1169
+ // fires, so a set built from a name would read as a new outage whenever the
1170
+ // operator registered another one.
1171
+ case "connector":
1172
+ return `connector:${check.scope}`;
1173
+ // Task 2689 — one sweep and one account directory per account, so each
1174
+ // names itself, the way `whatsapp` and `graph` do.
1175
+ case "sweep":
1176
+ return "sweep";
1177
+ case "filesystem":
1178
+ return "filesystem";
863
1179
  default:
864
1180
  return String(check.type);
865
1181
  }
@@ -1033,6 +1349,124 @@ flagKey) {
1033
1349
  const { ids } = await sources.graphUnflagged(accountId, label, flagKey, UNFLAGGED_ID_CAP);
1034
1350
  return { hasWork: ids.length > 0, mark: null, unflagged: { label, ids } };
1035
1351
  }
1352
+ case "connector": {
1353
+ // The instant this arm STARTED, recorded as the new position of every
1354
+ // connector that answers. Taken once, before the first call, so a slow
1355
+ // queue cannot advance its own mark past records that arrived while it
1356
+ // was answering.
1357
+ const startedAt = new Date().toISOString();
1358
+ const { pluginPresent, bindings } = await sources.connectorQueues(accountId, check.scope);
1359
+ // Two quiet answers, each with its own reason. Neither is an error: an
1360
+ // install with no connector plugin, and an account that has described no
1361
+ // queue of this kind, both mean nothing registered here can have moved.
1362
+ // The line is load-bearing — a routine that will never fire has to be
1363
+ // visible somewhere, and `op=checked` alone reads exactly like a healthy
1364
+ // quiet fire.
1365
+ if (!pluginPresent) {
1366
+ console.error(`[scheduling-gate] op=connector-quiet reason=no-plugin scope=${check.scope}`);
1367
+ return { hasWork: false, mark: null };
1368
+ }
1369
+ if (bindings.length === 0) {
1370
+ console.error(`[scheduling-gate] op=connector-quiet reason=no-queue scope=${check.scope}`);
1371
+ return { hasWork: false, mark: null };
1372
+ }
1373
+ let hasWork = false;
1374
+ const observed = [];
1375
+ for (const binding of bindings) {
1376
+ const record = () => observed.push({
1377
+ slot: { kind: "connector", key: binding.name },
1378
+ write: (m) => {
1379
+ m.connector = { ...(m.connector ?? {}), [binding.name]: { changedSince: startedAt } };
1380
+ },
1381
+ });
1382
+ const since = marks.connector?.[binding.name]?.changedSince;
1383
+ if (since === undefined) {
1384
+ // A first run: everything already queued counts as work, the same as
1385
+ // every other watermarked check. No call is made, because a read with
1386
+ // no changed-since filter is precisely the response `connector-call`
1387
+ // caps, and a capped body cannot be counted.
1388
+ console.error(`[scheduling-gate] op=connector-first-run connector=${binding.name}`);
1389
+ hasWork = true;
1390
+ record();
1391
+ continue;
1392
+ }
1393
+ const res = await sources.connectorQueueRead(accountId, binding, since);
1394
+ // An unauthorised connector is an ANSWER: it reports no work, records no
1395
+ // position, and says so on its own line. No position, because a
1396
+ // connector this gate could not read must not be advanced past — the
1397
+ // records that arrived while the credential was dead are still unseen.
1398
+ if (res.status === 401 || res.status === 403) {
1399
+ console.error(`[scheduling-gate] op=connector-quiet reason=unauthorised ` +
1400
+ `connector=${binding.name} status=${res.status}`);
1401
+ continue;
1402
+ }
1403
+ // Every other non-2xx throws, which counts as work and fires. A 500 or a
1404
+ // 404 is a source this gate could not read at all, and answering quiet
1405
+ // about it would mute the routine for the length of an outage.
1406
+ if (res.status < 200 || res.status >= 300) {
1407
+ throw new Error(`connector ${binding.name} queue answered ${res.status}`);
1408
+ }
1409
+ console.error(`[scheduling-gate] op=connector-probe connector=${binding.name} ` +
1410
+ `status=${res.status} items=${res.items} truncated=${res.truncated}`);
1411
+ // A capped body is proof of movement that cannot be counted, so it reads
1412
+ // as work and still records its position: recording none would leave the
1413
+ // same window in place, it would truncate again, and the routine would
1414
+ // wake the model on every fire with no path back to quiet.
1415
+ if (res.items > 0 || res.truncated)
1416
+ hasWork = true;
1417
+ record();
1418
+ }
1419
+ return { hasWork, mark: observed.length > 0 ? observed : null };
1420
+ }
1421
+ case "sweep": {
1422
+ const state = await sources.sweepState(accountId);
1423
+ const digest = digestSweepState(state, Date.now());
1424
+ const stored = marks.sweep?.digest;
1425
+ return {
1426
+ // An absent digest is a first run, which reports work exactly as every
1427
+ // other stored-position check does on its first fire.
1428
+ hasWork: stored !== digest,
1429
+ mark: {
1430
+ slot: { kind: "sweep" },
1431
+ write: (m) => {
1432
+ m.sweep = { ...(m.sweep ?? {}), digest };
1433
+ },
1434
+ },
1435
+ };
1436
+ }
1437
+ case "filesystem": {
1438
+ const minGrowth = check.minGrowthBytes ?? FS_DEFAULT_MIN_GROWTH_BYTES;
1439
+ // Refused HERE, before the source is reached, exactly as the mail arm
1440
+ // refuses an unknown provider: the throw lands inside
1441
+ // `runSchedulingGate`'s try, where it counts as work and names itself.
1442
+ // Zero would make every fire pass, because growth is never below zero,
1443
+ // and a gate that can never go quiet is the class this design removes.
1444
+ if (!Number.isInteger(minGrowth) || minGrowth <= 0) {
1445
+ throw new Error(`minGrowthBytes must be a positive integer, got ${JSON.stringify(check.minGrowthBytes)}`);
1446
+ }
1447
+ const { bytes, capped } = await sources.filesystemBytes(accountId);
1448
+ if (capped) {
1449
+ // A floor, not a total, so it records NO position: promoting a floor
1450
+ // would make the next fire measure growth from a number the directory
1451
+ // never held, and the error would be in the muting direction.
1452
+ console.error(`[scheduling-gate] op=fs-measure capped=true bytes=${bytes} treatedAs=has-work`);
1453
+ return { hasWork: true, mark: null };
1454
+ }
1455
+ const stored = marks.filesystem?.bytes;
1456
+ return {
1457
+ // An absent position is a first run: whatever is already there counts
1458
+ // as work, the way every other watermarked check reads its first fire.
1459
+ // A directory that SHRANK reports no work — growth is the question, and
1460
+ // the stored value stays put until an accepted wake moves it.
1461
+ hasWork: stored === undefined || bytes - stored >= minGrowth,
1462
+ mark: {
1463
+ slot: { kind: "filesystem" },
1464
+ write: (m) => {
1465
+ m.filesystem = { ...(m.filesystem ?? {}), bytes };
1466
+ },
1467
+ },
1468
+ };
1469
+ }
1036
1470
  // Task 2653 — a type outside the union. The switch was exhaustive over
1037
1471
  // `GateCheck` and had no default, so `probe` returned `undefined` and the
1038
1472
  // caller's `outcome.hasWork` read threw OUTSIDE its own try: the TypeError
@@ -1105,18 +1539,29 @@ export async function runSchedulingGate(params, accountId, platformRoot, sources
1105
1539
  }
1106
1540
  // A check that threw took the `continue` above, so it records no position:
1107
1541
  // its state is unknown, not absent.
1108
- if (outcome.mark) {
1542
+ //
1543
+ // Task 2688 — one check may hand back several positions (one per registered
1544
+ // connector), so the single mark is normalised to a list and each is logged
1545
+ // on its own line.
1546
+ const observedMarks = outcome.mark === null ? [] : Array.isArray(outcome.mark) ? outcome.mark : [outcome.mark];
1547
+ for (const mark of observedMarks) {
1109
1548
  // Every probe records what it saw, whatever the verdict: the position is a
1110
1549
  // fact about the source.
1111
- outcome.mark.write(fresh);
1550
+ mark.write(fresh);
1112
1551
  // Task 2663 — BOTH sides of the comparison. This line printed only
1113
1552
  // `position=`, which was `fresh` — this probe's own reading — so the value
1114
1553
  // the verdict was actually made against appeared in no log line anywhere,
1115
1554
  // and a gate stuck for ever on an empty slot read exactly like a healthy
1116
1555
  // one. Measured on 2026-08-10: 18 wakes whose `position=` never moved.
1117
- console.error(`[scheduling-gate] op=gate-read eventId=${id8} check=${check.type} ` +
1118
- `stored=${slotPosition(outcome.mark.slot, marks) ?? "null"} ` +
1119
- `observed=${slotPosition(outcome.mark.slot, fresh) ?? "null"}`);
1556
+ console.error(
1557
+ // Task 2688 — `slot=` is appended rather than inserted, because one
1558
+ // check can now print several of these lines and only the slot key
1559
+ // tells them apart. It goes last so the fields every existing reader
1560
+ // greps for keep their positions.
1561
+ `[scheduling-gate] op=gate-read eventId=${id8} check=${check.type} ` +
1562
+ `stored=${slotPosition(mark.slot, marks) ?? "null"} ` +
1563
+ `observed=${slotPosition(mark.slot, fresh) ?? "null"} ` +
1564
+ `slot=${slotKey(mark.slot)}`);
1120
1565
  }
1121
1566
  // Task 2684 — an empty set carries nothing to flag, so it contributes no
1122
1567
  // entry: the dispatcher's `op=gate-flag` then names only labels that