@rubytech/create-maxy-code 0.1.551 → 0.1.553

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 (289) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/routine-templates/dist/index.d.ts +15 -2
  3. package/payload/platform/lib/routine-templates/dist/index.d.ts.map +1 -1
  4. package/payload/platform/lib/routine-templates/dist/index.js +25 -1
  5. package/payload/platform/lib/routine-templates/dist/index.js.map +1 -1
  6. package/payload/platform/lib/routine-templates/dist/roster.d.ts +76 -1
  7. package/payload/platform/lib/routine-templates/dist/roster.d.ts.map +1 -1
  8. package/payload/platform/lib/routine-templates/dist/roster.js +60 -3
  9. package/payload/platform/lib/routine-templates/dist/roster.js.map +1 -1
  10. package/payload/platform/lib/routine-templates/src/__tests__/roster.test.ts +93 -1
  11. package/payload/platform/lib/routine-templates/src/__tests__/seed.test.ts +74 -0
  12. package/payload/platform/lib/routine-templates/src/index.ts +25 -1
  13. package/payload/platform/lib/routine-templates/src/roster.ts +111 -2
  14. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +11 -0
  15. package/payload/platform/plugins/scheduling/PLUGIN.md +54 -4
  16. package/payload/platform/plugins/scheduling/mcp/dist/index.js +113 -6
  17. package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
  18. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.d.ts +2 -0
  19. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.js +216 -0
  21. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/connector-freebusy.test.js.map +1 -0
  22. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.d.ts +2 -0
  23. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.js +44 -0
  25. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/fires-per-day.test.js.map +1 -0
  26. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.d.ts +2 -0
  27. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.d.ts.map +1 -0
  28. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js +128 -0
  29. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-audits.test.js.map +1 -0
  30. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.d.ts +2 -0
  31. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.d.ts.map +1 -0
  32. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js +92 -0
  33. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate-classification.test.js.map +1 -0
  34. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.d.ts +2 -0
  35. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.d.ts.map +1 -0
  36. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js +135 -0
  37. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/gate.test.js.map +1 -0
  38. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js +210 -2
  39. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/schedule-audit.test.js.map +1 -1
  40. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js +18 -1
  41. package/payload/platform/plugins/scheduling/mcp/dist/lib/__tests__/update-summary.test.js.map +1 -1
  42. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.d.ts +86 -0
  43. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.d.ts.map +1 -0
  44. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.js +240 -0
  45. package/payload/platform/plugins/scheduling/mcp/dist/lib/connector-freebusy.js.map +1 -0
  46. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.d.ts +2 -0
  47. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.d.ts.map +1 -0
  48. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.js +47 -0
  49. package/payload/platform/plugins/scheduling/mcp/dist/lib/fires-per-day.js.map +1 -0
  50. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts +92 -0
  51. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.d.ts.map +1 -0
  52. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js +193 -0
  53. package/payload/platform/plugins/scheduling/mcp/dist/lib/gate.js.map +1 -0
  54. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts +8 -1
  55. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.d.ts.map +1 -1
  56. package/payload/platform/plugins/scheduling/mcp/dist/lib/routine-run.js.map +1 -1
  57. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts +152 -1
  58. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.d.ts.map +1 -1
  59. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js +472 -0
  60. package/payload/platform/plugins/scheduling/mcp/dist/lib/schedule-audit.js.map +1 -1
  61. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.d.ts.map +1 -1
  62. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js +11 -1
  63. package/payload/platform/plugins/scheduling/mcp/dist/lib/update-summary.js.map +1 -1
  64. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.d.ts +2 -0
  65. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.d.ts.map +1 -0
  66. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js +146 -0
  67. package/payload/platform/plugins/scheduling/mcp/dist/scripts/__tests__/gate-dispatch-wiring.test.js.map +1 -0
  68. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts +27 -0
  69. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.d.ts.map +1 -1
  70. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js +18 -0
  71. package/payload/platform/plugins/scheduling/mcp/dist/scripts/agent-turn-dispatch.js.map +1 -1
  72. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js +146 -64
  73. package/payload/platform/plugins/scheduling/mcp/dist/scripts/check-due-events.js.map +1 -1
  74. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-connector-busy-sync.test.js +84 -1
  75. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-connector-busy-sync.test.js.map +1 -1
  76. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.d.ts +2 -0
  77. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.d.ts.map +1 -0
  78. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js +200 -0
  79. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-waiver.test.js.map +1 -0
  80. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.d.ts +2 -0
  81. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.d.ts.map +1 -0
  82. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js +238 -0
  83. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-gate-write.test.js.map +1 -0
  84. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.d.ts +2 -0
  85. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.d.ts.map +1 -0
  86. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.js +208 -0
  87. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/schedule-update-recurrence-record.test.js.map +1 -0
  88. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.d.ts +2 -0
  89. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.d.ts.map +1 -0
  90. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js +291 -0
  91. package/payload/platform/plugins/scheduling/mcp/dist/tools/__tests__/scheduling-gate.test.js.map +1 -0
  92. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.d.ts +26 -2
  93. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.d.ts.map +1 -1
  94. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.js +51 -2
  95. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-connector-busy-sync.js.map +1 -1
  96. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +71 -0
  97. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
  98. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +109 -1
  99. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
  100. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts +3 -1
  101. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.d.ts.map +1 -1
  102. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js +124 -3
  103. package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-update.js.map +1 -1
  104. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts +148 -0
  105. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.d.ts.map +1 -0
  106. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js +410 -0
  107. package/payload/platform/plugins/scheduling/mcp/dist/tools/scheduling-gate.js.map +1 -0
  108. package/payload/platform/plugins/scheduling/mcp/package.json +1 -0
  109. package/payload/platform/plugins/workflows/skills/workflow-manager/SKILL.md +4 -0
  110. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  111. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +1 -0
  112. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  113. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts +51 -4
  114. package/payload/platform/services/claude-session-manager/dist/pty-census.d.ts.map +1 -1
  115. package/payload/platform/services/claude-session-manager/dist/pty-census.js +60 -10
  116. package/payload/platform/services/claude-session-manager/dist/pty-census.js.map +1 -1
  117. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts +64 -1
  118. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.d.ts.map +1 -1
  119. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js +189 -6
  120. package/payload/platform/services/claude-session-manager/dist/slice-memory-policy.js.map +1 -1
  121. package/payload/server/public/activity.html +5 -5
  122. package/payload/server/public/agents.html +4 -4
  123. package/payload/server/public/assets/{AdminLoginScreens-Bjcs9ta9.js → AdminLoginScreens-Ca2QhL18.js} +1 -1
  124. package/payload/server/public/assets/AdminLoginScreens-Ca2QhL18.js.br +0 -0
  125. package/payload/server/public/assets/AdminLoginScreens-Ca2QhL18.js.gz +0 -0
  126. package/payload/server/public/assets/{AdminShell-DVBAMUCy.js → AdminShell-Dc5du5xX.js} +1 -1
  127. package/payload/server/public/assets/AdminShell-Dc5du5xX.js.br +0 -0
  128. package/payload/server/public/assets/AdminShell-Dc5du5xX.js.gz +0 -0
  129. package/payload/server/public/assets/{activity-C8E8-EwQ.js → activity-TSDKmIYb.js} +1 -1
  130. package/payload/server/public/assets/activity-TSDKmIYb.js.br +0 -0
  131. package/payload/server/public/assets/activity-TSDKmIYb.js.gz +0 -0
  132. package/payload/server/public/assets/{admin-CeWAlpRS.js → admin-Ba729Eyg.js} +1 -1
  133. package/payload/server/public/assets/admin-Ba729Eyg.js.br +0 -0
  134. package/payload/server/public/assets/admin-Ba729Eyg.js.gz +0 -0
  135. package/payload/server/public/assets/{agents-DuN92iFb.js → agents-9hlQHOS5.js} +1 -1
  136. package/payload/server/public/assets/agents-9hlQHOS5.js.br +0 -0
  137. package/payload/server/public/assets/agents-9hlQHOS5.js.gz +0 -0
  138. package/payload/server/public/assets/{browser-BxebdKbh.js → browser-BrqoCsqh.js} +1 -1
  139. package/payload/server/public/assets/browser-BrqoCsqh.js.br +0 -0
  140. package/payload/server/public/assets/browser-BrqoCsqh.js.gz +0 -0
  141. package/payload/server/public/assets/{calendar-BtFZQ5Np.js → calendar-DOObQ_m8.js} +1 -1
  142. package/payload/server/public/assets/calendar-DOObQ_m8.js.br +0 -0
  143. package/payload/server/public/assets/calendar-DOObQ_m8.js.gz +0 -0
  144. package/payload/server/public/assets/chat-Xaul2lLF.js +1 -0
  145. package/payload/server/public/assets/chat-Xaul2lLF.js.br +3 -0
  146. package/payload/server/public/assets/chat-Xaul2lLF.js.gz +0 -0
  147. package/payload/server/public/assets/chevron-left-BYH5vH4S.js +1 -0
  148. package/payload/server/public/assets/chevron-left-BYH5vH4S.js.br +0 -0
  149. package/payload/server/public/assets/chevron-right-XxQPHfNH.js +1 -0
  150. package/payload/server/public/assets/chevron-right-XxQPHfNH.js.br +0 -0
  151. package/payload/server/public/assets/clock-BNp2JTKh.js +1 -0
  152. package/payload/server/public/assets/clock-BNp2JTKh.js.br +0 -0
  153. package/payload/server/public/assets/clock-BNp2JTKh.js.gz +0 -0
  154. package/payload/server/public/assets/{copy-DCDHXq3O.js → copy-BkbgmpJo.js} +1 -1
  155. package/payload/server/public/assets/copy-BkbgmpJo.js.br +0 -0
  156. package/payload/server/public/assets/copy-BkbgmpJo.js.gz +0 -0
  157. package/payload/server/public/assets/data-yuHj8g0C.js +1 -0
  158. package/payload/server/public/assets/data-yuHj8g0C.js.br +3 -0
  159. package/payload/server/public/assets/data-yuHj8g0C.js.gz +0 -0
  160. package/payload/server/public/assets/{file-text-zQgEQWCF.js → file-text-DDCrqbWB.js} +1 -1
  161. package/payload/server/public/assets/file-text-DDCrqbWB.js.br +0 -0
  162. package/payload/server/public/assets/file-text-DDCrqbWB.js.gz +0 -0
  163. package/payload/server/public/assets/{graph-CFCEh_YI.js → graph-BKPdL5ph.js} +1 -1
  164. package/payload/server/public/assets/graph-BKPdL5ph.js.br +0 -0
  165. package/payload/server/public/assets/graph-BKPdL5ph.js.gz +0 -0
  166. package/payload/server/public/assets/{graph-labels-CA1UcDuS.js → graph-labels-B2PrHK6O.js} +1 -1
  167. package/payload/server/public/assets/graph-labels-B2PrHK6O.js.br +0 -0
  168. package/payload/server/public/assets/graph-labels-B2PrHK6O.js.gz +0 -0
  169. package/payload/server/public/assets/{operator-hFXPDc2X.js → operator-BVULmN09.js} +1 -1
  170. package/payload/server/public/assets/operator-BVULmN09.js.br +0 -0
  171. package/payload/server/public/assets/operator-BVULmN09.js.gz +0 -0
  172. package/payload/server/public/assets/{page-ne7I_r9L.js → page-BbzdSxuU.js} +1 -1
  173. package/payload/server/public/assets/page-BbzdSxuU.js.br +0 -0
  174. package/payload/server/public/assets/page-BbzdSxuU.js.gz +0 -0
  175. package/payload/server/public/assets/{page-C8jyPH_3.js → page-baQ9FQIg.js} +1 -1
  176. package/payload/server/public/assets/page-baQ9FQIg.js.br +0 -0
  177. package/payload/server/public/assets/page-baQ9FQIg.js.gz +0 -0
  178. package/payload/server/public/assets/play-3hlqzxLI.js +1 -0
  179. package/payload/server/public/assets/play-3hlqzxLI.js.br +0 -0
  180. package/payload/server/public/assets/play-3hlqzxLI.js.gz +0 -0
  181. package/payload/server/public/assets/{public-BgnqjAal.js → public-B_SEBxWy.js} +1 -1
  182. package/payload/server/public/assets/public-B_SEBxWy.js.br +0 -0
  183. package/payload/server/public/assets/public-B_SEBxWy.js.gz +0 -0
  184. package/payload/server/public/assets/{rotate-ccw-DfJWjXAg.js → rotate-ccw-DzXTXh3g.js} +1 -1
  185. package/payload/server/public/assets/rotate-ccw-DzXTXh3g.js.br +0 -0
  186. package/payload/server/public/assets/rotate-ccw-DzXTXh3g.js.gz +0 -0
  187. package/payload/server/public/assets/routines-BQzE0xzC.js +2 -0
  188. package/payload/server/public/assets/routines-BQzE0xzC.js.br +0 -0
  189. package/payload/server/public/assets/routines-BQzE0xzC.js.gz +0 -0
  190. package/payload/server/public/assets/{skills-D-6EbgfP.js → skills-yCZhYVIs.js} +1 -1
  191. package/payload/server/public/assets/skills-yCZhYVIs.js.br +0 -0
  192. package/payload/server/public/assets/skills-yCZhYVIs.js.gz +0 -0
  193. package/payload/server/public/assets/{tasks-DnN5Fyds.js → tasks-DxNFLsHv.js} +1 -1
  194. package/payload/server/public/assets/tasks-DxNFLsHv.js.br +0 -0
  195. package/payload/server/public/assets/tasks-DxNFLsHv.js.gz +0 -0
  196. package/payload/server/public/assets/{triangle-alert-eh9UOKDT.js → triangle-alert-CYwHTcvU.js} +1 -1
  197. package/payload/server/public/assets/triangle-alert-CYwHTcvU.js.br +0 -0
  198. package/payload/server/public/assets/triangle-alert-CYwHTcvU.js.gz +0 -0
  199. package/payload/server/public/assets/{useCopyFeedback-Bz9OJHBb.js → useCopyFeedback-BCRjKc-H.js} +1 -1
  200. package/payload/server/public/assets/useCopyFeedback-BCRjKc-H.js.br +0 -0
  201. package/payload/server/public/assets/useCopyFeedback-BCRjKc-H.js.gz +0 -0
  202. package/payload/server/public/assets/{useMediaQuery-CNxsjKZp.css → useMediaQuery-Vxpamxqq.css} +1 -1
  203. package/payload/server/public/assets/useMediaQuery-Vxpamxqq.css.br +0 -0
  204. package/payload/server/public/assets/{useMediaQuery-CNxsjKZp.css.gz → useMediaQuery-Vxpamxqq.css.gz} +0 -0
  205. package/payload/server/public/assets/{useVoiceRecorder-BaDpMYxX.js → useVoiceRecorder-BaAtMu9T.js} +1 -1
  206. package/payload/server/public/assets/useVoiceRecorder-BaAtMu9T.js.br +0 -0
  207. package/payload/server/public/assets/useVoiceRecorder-BaAtMu9T.js.gz +0 -0
  208. package/payload/server/public/assets/{wrench-BO-l46rF.js → wrench-iI-vx-g2.js} +1 -1
  209. package/payload/server/public/assets/wrench-iI-vx-g2.js.br +0 -0
  210. package/payload/server/public/assets/wrench-iI-vx-g2.js.gz +0 -0
  211. package/payload/server/public/browser.html +4 -4
  212. package/payload/server/public/calendar.html +8 -8
  213. package/payload/server/public/chat.html +13 -13
  214. package/payload/server/public/data.html +11 -11
  215. package/payload/server/public/graph.html +9 -9
  216. package/payload/server/public/index.html +14 -14
  217. package/payload/server/public/operator.html +14 -14
  218. package/payload/server/public/public.html +13 -13
  219. package/payload/server/public/routines.html +6 -6
  220. package/payload/server/public/skills.html +5 -5
  221. package/payload/server/public/tasks.html +6 -6
  222. package/payload/server/server.js +36 -5
  223. package/payload/server/public/assets/AdminLoginScreens-Bjcs9ta9.js.br +0 -0
  224. package/payload/server/public/assets/AdminLoginScreens-Bjcs9ta9.js.gz +0 -0
  225. package/payload/server/public/assets/AdminShell-DVBAMUCy.js.br +0 -0
  226. package/payload/server/public/assets/AdminShell-DVBAMUCy.js.gz +0 -0
  227. package/payload/server/public/assets/activity-C8E8-EwQ.js.br +0 -0
  228. package/payload/server/public/assets/activity-C8E8-EwQ.js.gz +0 -0
  229. package/payload/server/public/assets/admin-CeWAlpRS.js.br +0 -0
  230. package/payload/server/public/assets/admin-CeWAlpRS.js.gz +0 -0
  231. package/payload/server/public/assets/agents-DuN92iFb.js.br +0 -0
  232. package/payload/server/public/assets/agents-DuN92iFb.js.gz +0 -0
  233. package/payload/server/public/assets/browser-BxebdKbh.js.br +0 -0
  234. package/payload/server/public/assets/browser-BxebdKbh.js.gz +0 -0
  235. package/payload/server/public/assets/calendar-BtFZQ5Np.js.br +0 -0
  236. package/payload/server/public/assets/calendar-BtFZQ5Np.js.gz +0 -0
  237. package/payload/server/public/assets/chat-CGIwynaH.js +0 -1
  238. package/payload/server/public/assets/chat-CGIwynaH.js.br +0 -3
  239. package/payload/server/public/assets/chat-CGIwynaH.js.gz +0 -0
  240. package/payload/server/public/assets/chevron-left-DaNP1zDb.js +0 -1
  241. package/payload/server/public/assets/chevron-left-DaNP1zDb.js.br +0 -0
  242. package/payload/server/public/assets/chevron-right-BbbuTUnt.js +0 -1
  243. package/payload/server/public/assets/chevron-right-BbbuTUnt.js.br +0 -0
  244. package/payload/server/public/assets/clock-CfDdatjp.js +0 -1
  245. package/payload/server/public/assets/clock-CfDdatjp.js.br +0 -0
  246. package/payload/server/public/assets/clock-CfDdatjp.js.gz +0 -0
  247. package/payload/server/public/assets/copy-DCDHXq3O.js.br +0 -0
  248. package/payload/server/public/assets/copy-DCDHXq3O.js.gz +0 -0
  249. package/payload/server/public/assets/data-BchSWOju.js +0 -1
  250. package/payload/server/public/assets/data-BchSWOju.js.br +0 -1
  251. package/payload/server/public/assets/data-BchSWOju.js.gz +0 -0
  252. package/payload/server/public/assets/file-text-zQgEQWCF.js.br +0 -0
  253. package/payload/server/public/assets/file-text-zQgEQWCF.js.gz +0 -0
  254. package/payload/server/public/assets/graph-CFCEh_YI.js.br +0 -0
  255. package/payload/server/public/assets/graph-CFCEh_YI.js.gz +0 -0
  256. package/payload/server/public/assets/graph-labels-CA1UcDuS.js.br +0 -0
  257. package/payload/server/public/assets/graph-labels-CA1UcDuS.js.gz +0 -0
  258. package/payload/server/public/assets/operator-hFXPDc2X.js.br +0 -0
  259. package/payload/server/public/assets/operator-hFXPDc2X.js.gz +0 -0
  260. package/payload/server/public/assets/page-C8jyPH_3.js.br +0 -0
  261. package/payload/server/public/assets/page-C8jyPH_3.js.gz +0 -0
  262. package/payload/server/public/assets/page-ne7I_r9L.js.br +0 -0
  263. package/payload/server/public/assets/page-ne7I_r9L.js.gz +0 -0
  264. package/payload/server/public/assets/play-Ep0SZTbr.js +0 -1
  265. package/payload/server/public/assets/play-Ep0SZTbr.js.br +0 -0
  266. package/payload/server/public/assets/play-Ep0SZTbr.js.gz +0 -0
  267. package/payload/server/public/assets/public-BgnqjAal.js.br +0 -0
  268. package/payload/server/public/assets/public-BgnqjAal.js.gz +0 -0
  269. package/payload/server/public/assets/rotate-ccw-DfJWjXAg.js.br +0 -0
  270. package/payload/server/public/assets/rotate-ccw-DfJWjXAg.js.gz +0 -0
  271. package/payload/server/public/assets/routines-OGFXwTFO.js +0 -2
  272. package/payload/server/public/assets/routines-OGFXwTFO.js.br +0 -0
  273. package/payload/server/public/assets/routines-OGFXwTFO.js.gz +0 -0
  274. package/payload/server/public/assets/skills-D-6EbgfP.js.br +0 -0
  275. package/payload/server/public/assets/skills-D-6EbgfP.js.gz +0 -0
  276. package/payload/server/public/assets/tasks-DnN5Fyds.js.br +0 -0
  277. package/payload/server/public/assets/tasks-DnN5Fyds.js.gz +0 -0
  278. package/payload/server/public/assets/triangle-alert-eh9UOKDT.js.br +0 -0
  279. package/payload/server/public/assets/triangle-alert-eh9UOKDT.js.gz +0 -0
  280. package/payload/server/public/assets/useCopyFeedback-Bz9OJHBb.js.br +0 -0
  281. package/payload/server/public/assets/useCopyFeedback-Bz9OJHBb.js.gz +0 -0
  282. package/payload/server/public/assets/useMediaQuery-CNxsjKZp.css.br +0 -0
  283. package/payload/server/public/assets/useVoiceRecorder-BaDpMYxX.js.br +0 -0
  284. package/payload/server/public/assets/useVoiceRecorder-BaDpMYxX.js.gz +0 -0
  285. package/payload/server/public/assets/wrench-BO-l46rF.js.br +0 -0
  286. package/payload/server/public/assets/wrench-BO-l46rF.js.gz +0 -0
  287. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js → useMediaQuery-Baf4Huvv.js} +0 -0
  288. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js.br → useMediaQuery-Baf4Huvv.js.br} +0 -0
  289. /package/payload/server/public/assets/{useMediaQuery-_mMln3Fu.js.gz → useMediaQuery-Baf4Huvv.js.gz} +0 -0
@@ -166,7 +166,20 @@ export async function resolveOwnerTimezone(
166
166
  * destroy the operator's own caption on the next version bump; and the
167
167
  * scheduling plugin's one-shot `converge-routine-description` has already given
168
168
  * every pre-existing row a description, so no row is left holding only a
169
- * title. */
169
+ * title.
170
+ *
171
+ * The gate (Task 2379) is written by a `SET` after the MERGE rather than inside
172
+ * either branch, because both branches need `e.eventId` and only one of them
173
+ * has just minted it. `writing` is the same condition the prompt follows, so a
174
+ * gate is written on create, refreshed on a version bump of an unedited row,
175
+ * and never touched on an operator-edited one.
176
+ *
177
+ * Two details are load-bearing. The null branch is symmetric on purpose: a
178
+ * template reclassified gateless must have its three properties cleared, or the
179
+ * row keeps a gate its template no longer declares and nothing reports it. And
180
+ * the commit token is substituted from the same `row.gateChecks` the gate is
181
+ * built from, so the precondition and the turn's own commit call cannot
182
+ * disagree about what this routine checks. */
170
183
  export const MERGE_CYPHER = `
171
184
  UNWIND $rows AS row
172
185
  OPTIONAL MATCH (pre:Event {accountId: $accountId, routineTemplate: row.key})
@@ -202,6 +215,13 @@ MERGE (e:Event {accountId: $accountId, routineTemplate: row.key})
202
215
  e.description = CASE WHEN refreshing THEN row.description ELSE e.description END,
203
216
  e.routineTemplateVersion = CASE WHEN refreshing THEN $version ELSE e.routineTemplateVersion END,
204
217
  e.updatedAt = CASE WHEN refreshing THEN $now ELSE e.updatedAt END
218
+ WITH row, e, existed, priorVersion, edited, (NOT existed OR refreshing) AS writing
219
+ SET e.gatePlugin = CASE WHEN writing THEN (CASE WHEN row.gateChecks IS NULL THEN null ELSE 'scheduling' END) ELSE e.gatePlugin END,
220
+ e.gateTool = CASE WHEN writing THEN (CASE WHEN row.gateChecks IS NULL THEN null ELSE 'scheduling-gate' END) ELSE e.gateTool END,
221
+ e.gateArgs = CASE WHEN writing THEN (CASE WHEN row.gateChecks IS NULL THEN null ELSE '{"eventId":"' + e.eventId + '","checks":' + row.gateChecks + '}' END) ELSE e.gateArgs END,
222
+ e.agentPrompt = CASE WHEN writing AND row.gateChecks IS NOT NULL
223
+ THEN replace(e.agentPrompt, '<<gateCommit>>', '{"eventId":"' + e.eventId + '","checks":' + row.gateChecks + ',"commit":true}')
224
+ ELSE e.agentPrompt END
205
225
  RETURN row.key AS key,
206
226
  e.eventId AS eventId,
207
227
  CASE WHEN NOT existed THEN 'create'
@@ -277,6 +297,10 @@ export async function seedRoutineRoster(args: SeedRoutineRosterArgs): Promise<Ro
277
297
  description: r.description,
278
298
  prompt: r.prompt,
279
299
  recurrence: r.recurrence,
300
+ // Task 2379 — the only place the checks are serialised. Null for a
301
+ // gateless template, which the MERGE reads as "clear the three gate
302
+ // properties on the write path".
303
+ gateChecks: r.gate ? JSON.stringify(r.gate.checks) : null,
280
304
  })),
281
305
  });
282
306
 
@@ -35,7 +35,76 @@
35
35
  * routine's prompt only when the stored `routineTemplateVersion` is BELOW this
36
36
  * and no operator has edited the row, so an unbumped edit ships to new installs
37
37
  * and never reaches existing ones — bump it in the same commit as the edit. */
38
- export const ROUTINE_TEMPLATE_VERSION = 2;
38
+ export const ROUTINE_TEMPLATE_VERSION = 3;
39
+
40
+ /**
41
+ * Task 2379 — the check shapes a shipped template may name.
42
+ *
43
+ * Declared here rather than imported from the scheduling plugin:
44
+ * `routine-templates` is a `platform/lib` package and importing from
45
+ * `platform/plugins` would invert the dependency direction and break the lib
46
+ * build, which has no plugin on its path. The roster test reads the tool's own
47
+ * source and asserts every type used here is one the tool declares, which is the
48
+ * same drift control this file already applies to tool names.
49
+ */
50
+ export interface RoutineMailCheck {
51
+ readonly type: "mail";
52
+ readonly mailbox: string;
53
+ readonly folder?: string;
54
+ readonly subjectPrefixes?: readonly string[];
55
+ }
56
+ export interface RoutineD1Check {
57
+ readonly type: "d1";
58
+ readonly database: string;
59
+ readonly sql: string;
60
+ }
61
+ export interface RoutineWhatsappCheck {
62
+ readonly type: "whatsapp";
63
+ readonly scope: "inbound" | "status";
64
+ }
65
+ export interface RoutineCalendarCheck {
66
+ readonly type: "calendar";
67
+ readonly connector: string;
68
+ }
69
+ export type RoutineGateCheck =
70
+ | RoutineMailCheck
71
+ | RoutineD1Check
72
+ | RoutineWhatsappCheck
73
+ | RoutineCalendarCheck;
74
+
75
+ /** The plugin and the tool are constants for every roster gate, so a template
76
+ * carries neither. `hasWork` is true when ANY check is true. */
77
+ export interface RoutineGate {
78
+ readonly checks: readonly RoutineGateCheck[];
79
+ }
80
+
81
+ /** Substituted by the seed with the full `scheduling-gate` argument object,
82
+ * including the row's own event id.
83
+ *
84
+ * A template cannot write that id: it is minted by the MERGE. Nor can the turn
85
+ * supply it, because the only place a scheduled turn sees its own event is the
86
+ * provenance preamble, which carries the first eight characters. The watermark
87
+ * file is named from the whole id, so a turn committing against the prefix
88
+ * would write a file the next gate never reads and the routine would report
89
+ * work for ever. */
90
+ export const GATE_COMMIT_TOKEN = "<<gateCommit>>";
91
+
92
+ /** True when the check compares against a stored position rather than reading a
93
+ * current one.
94
+ *
95
+ * A watermarked check reads its stored value or zero, so with no watermark file
96
+ * it reports work on every fire until something commits: such a gate is inert
97
+ * until its routine's prompt carries the commit line. Mirrors `probe`'s `mark`
98
+ * field in the scheduling plugin's `tools/scheduling-gate.ts`, which is a
99
+ * function for exactly these three and null for the other two. */
100
+ export function isWatermarkedCheck(check: RoutineGateCheck): boolean {
101
+ if (check.type === "whatsapp") return check.scope === "inbound";
102
+ return check.type === "mail" || check.type === "calendar";
103
+ }
104
+
105
+ export function gateNeedsCommit(gate: RoutineGate): boolean {
106
+ return gate.checks.some(isWatermarkedCheck);
107
+ }
39
108
 
40
109
  export interface RoutineTemplate {
41
110
  /** Stable identity. Half of the MERGE key `(accountId, routineTemplate)`, so
@@ -54,6 +123,31 @@ export interface RoutineTemplate {
54
123
  readonly recurrence: string;
55
124
  /** The injected turn. Refreshed on a version bump when the row is unedited. */
56
125
  readonly prompt: string;
126
+ /** Task 2379 — the precondition, present when this routine's work is
127
+ * conditional AND the check vocabulary can express its source without
128
+ * per-account data. Seeded onto `gatePlugin`/`gateTool`/`gateArgs` under the
129
+ * same rule the prompt follows: written on create, refreshed only on a
130
+ * version bump of an unedited row. */
131
+ readonly gate?: RoutineGate;
132
+ /** Why this template ships with no gate. Exactly one of `gate` and `gateless`
133
+ * is present on every template, and the roster test fails one carrying
134
+ * neither, so a template added later cannot skip the decision. A field rather
135
+ * than a comment because a comment cannot be asserted. */
136
+ readonly gateless?: string;
137
+ }
138
+
139
+ /** A template is classified when it carries exactly one of `gate` and a
140
+ * non-empty `gateless`.
141
+ *
142
+ * The rule lives here, in one function, rather than inline in the assertion
143
+ * that walks the roster. A predicate the test only ever feeds passing input
144
+ * reports clean while checking nothing, so the roster test asserts this both
145
+ * ways: true for all thirteen, and false for a template carrying neither, both
146
+ * and an empty reason. */
147
+ export function isClassified(template: RoutineTemplate): boolean {
148
+ const gated = template.gate != null;
149
+ const declaredGateless = typeof template.gateless === "string" && template.gateless.trim() !== "";
150
+ return gated !== declaredGateless;
57
151
  }
58
152
 
59
153
  const START_OF_DAY = `Prepare the operator's morning briefing and reply with it as a single message.
@@ -128,7 +222,9 @@ An appointment on the provider calendar with no counterpart in the graph is invi
128
222
 
129
223
  If the account has a busy-time sync configured, run schedule-connector-busy-sync so availability reflects what was just reconciled.
130
224
 
131
- Reply only when something was actually changed, naming what and why. A clean reconcile ends the turn without replying, every time. This routine runs often, so a message on every clean run trains the operator to ignore it.`;
225
+ Reply only when something was actually changed, naming what and why. A clean reconcile ends the turn without replying, every time. This routine runs often, so a message on every clean run trains the operator to ignore it.
226
+
227
+ When the run has finished and nothing failed, call scheduling-gate with <<gateCommit>> so the next run does not re-examine what you have just reconciled. Do not call it after a run that failed or that you stopped early: the work would be marked as handled without anyone having seen it.`;
132
228
 
133
229
  const CONTACT_RECONCILE = `Reconcile the people the business actually deals with against the people it has recorded.
134
230
 
@@ -231,6 +327,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
231
327
  "Sends one morning message covering what needs a decision, what is time-bound and what changed overnight; stays silent when there is nothing worth saying.",
232
328
  recurrence: "30 7 * * 1-5",
233
329
  prompt: START_OF_DAY,
330
+ gateless: "A morning briefing is judgement every fire, so a gate would either always pass or suppress the briefing.",
234
331
  },
235
332
  {
236
333
  key: "inbound-check",
@@ -239,6 +336,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
239
336
  "Triages mail that arrived since the last check and writes replies for you to approve; it never sends anything itself.",
240
337
  recurrence: "*/30 9-17 * * 1-5",
241
338
  prompt: INBOUND_CHECK,
339
+ gateless: "Conditional on new mail, but the mail check matches an exact mailbox address and no shipped template can name one (Task 2389).",
242
340
  },
243
341
  {
244
342
  key: "end-of-day",
@@ -247,6 +345,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
247
345
  "Closes the day with your unsent drafts, overdue work and tomorrow's diary; says nothing when all three are empty.",
248
346
  recurrence: "0 18 * * 1-5",
249
347
  prompt: END_OF_DAY,
348
+ gateless: "A wrap-up is judgement every fire, so no cheap check stands in for reading the three surfaces it reports.",
250
349
  },
251
350
  {
252
351
  key: "appointment-reminder",
@@ -255,6 +354,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
255
354
  "Reminds each attendee once before a booked appointment, on a route they already use; it never opens a conversation with a stranger.",
256
355
  recurrence: "0 * * * *",
257
356
  prompt: APPOINTMENT_REMINDER,
357
+ gateless: "Conditional on an appointment falling in the window, but no check type reads appointments held in the graph (Task 2389).",
258
358
  },
259
359
  {
260
360
  key: "commitment-chase",
@@ -263,6 +363,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
263
363
  "Surfaces promises the business made and did not keep, and promises made to it that nobody kept; it reports them rather than chasing them.",
264
364
  recurrence: "0 10 * * 2",
265
365
  prompt: COMMITMENT_CHASE,
366
+ gateless: "Whether a promise has been dropped is the judgement itself, so no check can answer it more cheaply.",
266
367
  },
267
368
  {
268
369
  key: "calendar-reconcile",
@@ -271,6 +372,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
271
372
  "Reconciles the provider calendar against the appointments held in the graph, writing what is missing and marking what was cancelled; a clean run says nothing.",
272
373
  recurrence: "*/15 * * * *",
273
374
  prompt: CALENDAR_RECONCILE,
375
+ gate: { checks: [{ type: "calendar", connector: "account" }] },
274
376
  },
275
377
  {
276
378
  key: "contact-reconcile",
@@ -279,6 +381,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
279
381
  "Reconciles the people you correspond with against the people on record, creating and updating contacts; it never merges two records that might be two people.",
280
382
  recurrence: "45 6 * * 1-5",
281
383
  prompt: CONTACT_RECONCILE,
384
+ gateless: "Conditional on new correspondence, but no check type reads correspondence (Task 2389).",
282
385
  },
283
386
  {
284
387
  key: "crm-reconcile",
@@ -287,6 +390,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
287
390
  "Brings across what changed in whichever operational system this business runs; it reads only and never writes back to that system.",
288
391
  recurrence: "0 3 * * *",
289
392
  prompt: CRM_RECONCILE,
393
+ gateless: "Conditional on connector movement, but no check type reads a connector queue (Task 2389).",
290
394
  },
291
395
  {
292
396
  key: "finance-reconcile",
@@ -295,6 +399,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
295
399
  "Reconciles the accounting system against the internal ledger and reports what does not agree; it never adjusts an entry to make the two sides match.",
296
400
  recurrence: "0 4 * * 1",
297
401
  prompt: FINANCE_RECONCILE,
402
+ gateless: "Conditional on accounting movement, but no check type reads an accounting ledger (Task 2389).",
298
403
  },
299
404
  {
300
405
  key: "weekly-digest",
@@ -303,6 +408,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
303
408
  "Writes the week in three paragraphs at most: what changed, what is stuck, and what next week turns on.",
304
409
  recurrence: "0 16 * * 5",
305
410
  prompt: WEEKLY_DIGEST,
411
+ gateless: "A digest is judgement every fire, and a gate would suppress the report it exists to send.",
306
412
  },
307
413
  {
308
414
  key: "channel-link-check",
@@ -311,6 +417,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
311
417
  "Reports a messaging link that has gone down and whether it can recover on its own; it never disconnects a link or starts a re-pair.",
312
418
  recurrence: "*/15 * * * *",
313
419
  prompt: CHANNEL_LINK_CHECK,
420
+ gate: { checks: [{ type: "whatsapp", scope: "status" }] },
314
421
  },
315
422
  {
316
423
  key: "graph-health",
@@ -319,6 +426,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
319
426
  "Reports what the hourly graph sweep last did and what is waiting for review; it reads only and never runs the sweep itself.",
320
427
  recurrence: "0 9 * * 1",
321
428
  prompt: GRAPH_HEALTH,
429
+ gateless: "Reports the sweep's state every fire by design, so there is nothing conditional to check.",
322
430
  },
323
431
  {
324
432
  key: "filesystem-health",
@@ -327,6 +435,7 @@ export const ROUTINE_ROSTER: readonly RoutineTemplate[] = [
327
435
  "Reports what has grown under the account directory and which paths are worth attention; it deletes nothing.",
328
436
  recurrence: "0 9 * * 1",
329
437
  prompt: FILESYSTEM_HEALTH,
438
+ gateless: "Reports what has grown every fire by design, so there is nothing conditional to check.",
330
439
  },
331
440
  ];
332
441
 
@@ -9,6 +9,17 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-08-03 (0.1.553)
13
+
14
+ - Each routine now shows how it runs, what its precondition checks, and how often it fired in the last day, so a routine that skipped can be told apart from one that ran quietly.
15
+ - Every built-in routine is now classified as gated or ungated, and a new recurring routine has to answer that question rather than defaulting to waking on the clock.
16
+ - Changing how often a routine runs is now recorded with its cost difference, and the rate across the install is counted on each check.
17
+
18
+ ## 2026-08-02 (0.1.552)
19
+
20
+ - A recurring routine can now check a condition first, so the clock alone no longer wakes the assistant when there is nothing to do. A one-time reminder is never gated, since skipping it would lose it.
21
+ - When the device runs short on memory, the largest chat is now stopped before the system starts swapping, rather than after everything has slowed down.
22
+
12
23
  ## 2026-08-02 (0.1.551)
13
24
 
14
25
  - You can now run as many Telegram bots as you like, each paired to its own agent, with its own access list, secret and conversations kept apart. Existing setups are converted automatically, the sidebar splits by bot, and a standing check reports each one.
@@ -39,6 +39,10 @@ tools:
39
39
  publicAllowlist: false
40
40
  adminAllowlist: false
41
41
  riskClass: write_local
42
+ - name: scheduling-gate
43
+ publicAllowlist: false
44
+ adminAllowlist: true
45
+ riskClass: read
42
46
  - name: schedule-connector-busy-sync
43
47
  publicAllowlist: false
44
48
  adminAllowlist: true
@@ -101,6 +105,24 @@ is planned; until it ships, the internal calendar plus `.ics` import is the clie
101
105
 
102
106
  Automatic connector-to-internal availability sync is separate future work.
103
107
 
108
+ ## Reflecting a connector calendar into the booking page
109
+
110
+ `schedule-connector-busy-sync` replaces this account's stored connector-busy set. Call it with **no
111
+ arguments**: it resolves the account's own authorised connector from its token store, reads the next
112
+ 15 days of free/busy from that connector, and syncs. Nothing about that is a judgement, so nothing
113
+ about it needs you — the tool spawns the google or outlook plugin's own free/busy tool to do the
114
+ reading, and it is safe to invoke from a scheduled `action` dispatch with `args: {}`.
115
+
116
+ An account with no authorised connector syncs `connector='none'` with an empty set. That is a
117
+ success, not a failure: it clears a deauthorised connector's stale blocks.
118
+
119
+ A failed read is an error and writes nothing. The stored busy set survives a transient provider
120
+ outage, because publishing the operator's booked time as free is worse than not syncing at all.
121
+
122
+ Passing `connector` and `busy` together is the explicit form and behaves exactly as before. Passing
123
+ `busy` without `connector` is refused, because the connector tag is what a later sync compares
124
+ against and it cannot be guessed from the intervals.
125
+
104
126
  ## Meetings vs events — what `schedule-event` writes
105
127
 
106
128
  `schedule-event` writes one of two graph nodes, decided from the parameters you pass — never from keywords in the title:
@@ -264,17 +286,45 @@ Users and plugins both create events with actions via the same `schedule-event`
264
286
 
265
287
  ## Agent-bearing dispatch
266
288
 
267
- An `action` invokes one pre-encoded MCP tool with no reasoning. When a due event needs an agent to decide what to do and say "when this document is signed, work out what to tell me and message me on WhatsApp" pass `agentDispatch` instead. When the event fires, the heartbeat hands the prompt to the destination's native channel: the channel spawns or resumes the deterministic admin session for that phone or chat, the agent runs the prompt with full admin tools, and its answer is delivered back over the account's paired socket. No operator session need be open.
289
+ Choosing a dispatch mode is **two questions, not one**: should this fire run *at all*, and what should it *say* once it does. Answer them separately. A routine that needs a model to compose its message very often needs no model at all to decide whether there is anything to compose, and treating those as one question is how a conditional sweep ends up spawning a full agent turn every fifteen minutes to discover there was nothing to do.
290
+
291
+ 1. **`action`** — nothing here needs a model. One pre-encoded MCP tool, no reasoning, no spawn.
292
+ 2. **`agentDispatch` with a `gate`** — a model is needed to compose or to judge, but only when there is something to act on. **This is the default for anything conditional.** The dispatcher calls the gate tool headlessly before spawning; `hasWork:false` skips the fire entirely.
293
+ 3. **`agentDispatch` with no gate** — judgement applies on *every* fire, whatever the state of the world: a daily briefing, a weekly digest. This is the exception, and it is stated as one. A recurring `agentDispatch` with no `gate` is refused unless it carries `gateWaived` naming why.
294
+
295
+ When an agent-bearing event fires, the heartbeat hands the prompt to the destination's native channel: the channel spawns or resumes the deterministic admin session for that phone or chat, the agent runs the prompt with full admin tools, and its answer is delivered back over the account's paired socket. No operator session need be open.
296
+
297
+ The worked example is the second shape. The signature check is a gate; the prompt keeps only the composing and the messaging:
268
298
 
269
299
  ```json
270
- { "channel": "whatsapp", "destination": "+447700900123", "prompt": "Check whether the tenancy agreement is signed yet and, if it is, message me a one-line confirmation." }
300
+ {
301
+ "agentDispatch": {
302
+ "channel": "whatsapp",
303
+ "destination": "+447700900123",
304
+ "prompt": "The tenancy agreement has just been signed. Write the one-line confirmation and send it."
305
+ },
306
+ "gate": {
307
+ "plugin": "scheduling",
308
+ "tool": "scheduling-gate",
309
+ "args": {
310
+ "eventId": "<this event's id>",
311
+ "checks": [{ "kind": "d1", "database": "<the account's database>", "sql": "SELECT count(*) FROM agreements WHERE signed_at > '<watermark>'" }]
312
+ }
313
+ }
314
+ }
271
315
  ```
272
316
 
317
+ Written as one model turn instead — "check whether it is signed yet and, if it is, message me" — the check runs inside the spawned agent, so the turn costs a full agent run on every fire whether or not anything was signed.
318
+
273
319
  - `channel` — `whatsapp` or `telegram`.
274
320
  - `destination` — the E.164 phone (WhatsApp) or chat id (Telegram) to run the turn for and reply to. It **must already be a registered admin or account-manager** on that channel; a destination that is not is rejected when the event is created. For WhatsApp the authoritative admin/manager lists are read from the socket-owning **house** account (never the event's own account), and the destination's registered account must match the account that created the schedule: an admin belongs to the house, a manager to its bound sub-account. The house/owner account may target any registered admin or manager; a sub-account is locked to its own manager(s). A refusal names the true reason — `not-registered` (in no house list) or `cross-account` (registered to a different account, naming both accounts).
275
- - `prompt` — the instruction the agent runs.
321
+ - `prompt` — the instruction the agent runs. Write it for the case where there **is** work: the gate has already decided that.
322
+ - `gate` — `{ plugin, tool, args }`. The precondition, evaluated headlessly between the `nextRun` advance and the spawn. It must return `structuredContent {hasWork:boolean}`; the dispatcher reads that boolean and nothing else. A gate that errors, times out or returns no boolean **dispatches anyway**, so a broken check never silently mutes a routine. Requires a complete `agentDispatch`, and is refused on an action-mode event.
323
+ - `gateWaived` — a string reason, and the only way to create a recurring `agentDispatch` with no gate. Give the reason this routine has judgement to apply on every fire ("the morning briefing composes from scratch each day"). It is stored on the event and counted by the standing gate-classification check, so a deliberate exception stays readable and the fleet's drift back to ungated is a number someone can look at. A conditional sweep is not an exception; gate it.
324
+
325
+ **Recurring only.** A one-time agent event is never gated — its row is already `due` by the time the dispatcher sees it, so a skip there would drop the routine for good — and it is therefore outside the refusal. `schedule-update` takes no waiver either: an existing routine keeps what it has until it is edited.
276
326
 
277
- An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account — unless that same phone is also on the house `adminPhones` list, in which case it scopes to the channel account, because the dispatch rides the house socket and so arrives from the house number. On Telegram the session runs on the house account.
327
+ An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`; the gate as `gatePlugin`, `gateTool`, `gateArgs`; the waiver as `gateWaived`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account — unless that same phone is also on the house `adminPhones` list, in which case it scopes to the channel account, because the dispatch rides the house socket and so arrives from the house number. On Telegram the session runs on the house account.
278
328
 
279
329
  A WhatsApp `agentDispatch` destination may therefore be an account manager, not only an admin phone — this is the supported way to send a manager a proactive or scheduled WhatsApp message. The reply is delivered over the house's paired line regardless of which sub-account the session is scoped to; socket ownership never restricts which number the dispatch can reach.
280
330
 
@@ -3,6 +3,7 @@ import { eagerTool } from "../../../../lib/mcp-eager/dist/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
5
  import { scheduleEvent } from "./tools/schedule-event.js";
6
+ import { runSchedulingGate } from "./tools/scheduling-gate.js";
6
7
  import { scheduleList } from "./tools/schedule-list.js";
7
8
  import { scheduleGet } from "./tools/schedule-get.js";
8
9
  import { scheduleRuns } from "./tools/schedule-runs.js";
@@ -12,9 +13,11 @@ import { scheduleCancel } from "./tools/schedule-cancel.js";
12
13
  import { scheduleExportIcs } from "./tools/schedule-export-ics.js";
13
14
  import { scheduleImportIcs } from "./tools/schedule-import-ics.js";
14
15
  import { scheduleArchiveIcs } from "./tools/schedule-archive-ics.js";
15
- import { syncConnectorBusy } from "./tools/schedule-connector-busy-sync.js";
16
+ import { resolveAndSyncConnectorBusy, syncConnectorBusy, } from "./tools/schedule-connector-busy-sync.js";
17
+ import { resolveConnector, spawnProviderCall } from "./lib/connector-freebusy.js";
16
18
  import { closeDriver, getUserTimezone, getSession } from "./lib/neo4j.js";
17
19
  import { formatCombined } from "./lib/time-format.js";
20
+ import { join, resolve } from "node:path";
18
21
  // Cast to `any` to avoid TS2589. TypeScript hits its 50-level instantiation
19
22
  // limit when accumulating typed tool signatures on a generic McpServer.
20
23
  // Individual tool handlers retain full type safety in their own files.
@@ -61,6 +64,17 @@ const eventStatusSchema = z
61
64
  .enum(EVENT_STATUSES)
62
65
  .optional()
63
66
  .describe("Event status. 'suspended' pauses a routine: it stops firing and stays paused until it is set back to 'scheduled', which resumes it at its next natural occurrence. 'cancelled' is not set here — use schedule-cancel, which ends the series permanently.");
67
+ // Task 2378 — the precondition, shared by both write tools so the create path
68
+ // and the edit path cannot describe it differently to the agent.
69
+ const gateShape = {
70
+ plugin: z.string().describe("Plugin exposing the gate tool (e.g. 'scheduling')"),
71
+ tool: z.string().describe("MCP tool to call before the routine fires. It must return structuredContent {hasWork:boolean}."),
72
+ args: z.record(z.unknown()).optional().describe("Arguments passed to the gate tool verbatim"),
73
+ };
74
+ const GATE_DESCRIPTION = "Precondition: before this routine spawns an agent turn, the dispatcher calls this tool headlessly and reads structuredContent.hasWork. False skips the turn entirely; true dispatches as normal. A gate that errors, times out or returns no boolean dispatches anyway, so a broken gate never silently mutes the routine. Requires a complete agentDispatch and is refused on an action-mode event.";
75
+ // Task 2383 — the declared exception, on the create path only. `schedule-update`
76
+ // takes no waiver: an existing routine keeps what it has until it is edited.
77
+ const GATE_WAIVED_DESCRIPTION = "The declared reason this recurring routine runs a model on every fire with no gate. A daily briefing or a weekly digest composes from scratch each time and has judgement to apply whether or not anything changed; a conditional sweep does not, and belongs behind a gate. Required when a recurring agentDispatch carries no gate — the write is refused without one. Stored on the event and counted by the standing gate-classification check, so every deliberate exception stays readable.";
64
78
  // ---------------------------------------------------------------------------
65
79
  // schedule-event
66
80
  // ---------------------------------------------------------------------------
@@ -88,6 +102,8 @@ eagerTool(server, "schedule-event", "Create a scheduled event or appointment. A
88
102
  destination: z.string().describe("E.164 phone (WhatsApp) or Telegram chat id — must already be a registered admin/account-manager on that channel"),
89
103
  prompt: z.string().describe("The instruction the agent runs when the event fires"),
90
104
  }).optional().describe("Agent-bearing dispatch: when the event fires, spawns or resumes the admin channel session for `destination` and runs `prompt` with full admin tools, delivering the answer back over the channel. Mutually exclusive with `action`. Always records an event, never a meeting."),
105
+ gate: z.object(gateShape).optional().describe(GATE_DESCRIPTION),
106
+ gateWaived: z.string().optional().describe(GATE_WAIVED_DESCRIPTION),
91
107
  }, async (params) => {
92
108
  if (!accountId)
93
109
  return refuseNoAccount("schedule-event");
@@ -126,6 +142,10 @@ eagerTool(server, "schedule-event", "Create a scheduled event or appointment. A
126
142
  parts.push(`action: ${params.action.plugin}/${params.action.tool}`);
127
143
  if (params.agentDispatch)
128
144
  parts.push(`agent dispatch: ${params.agentDispatch.channel} → ${params.agentDispatch.destination}`);
145
+ if (params.gate)
146
+ parts.push(`gate: ${params.gate.plugin}/${params.gate.tool}`);
147
+ if (params.gateWaived)
148
+ parts.push(`gate waived: ${params.gateWaived}`);
129
149
  return { content: [{ type: "text", text: parts.join(", ") }] };
130
150
  }
131
151
  catch (err) {
@@ -138,24 +158,61 @@ eagerTool(server, "schedule-event", "Create a scheduled event or appointment. A
138
158
  // ---------------------------------------------------------------------------
139
159
  // schedule-connector-busy-sync (Task 1654)
140
160
  // ---------------------------------------------------------------------------
141
- eagerTool(server, "schedule-connector-busy-sync", "Reflect your calendar connector's busy blocks into your booking page availability. Call this from the recurring connector-sync turn after reading your Google Calendar or Microsoft 365 free/busy: pass connector='google' or 'm365' and busy as the list of {start,end} ISO 8601 busy intervals. If no calendar connector is authorised, pass connector='none' and busy=[] to clear stale blocks. Wholesale-replaces the stored connector-busy set for this account.", {
142
- connector: z.enum(["google", "m365", "none"]).describe("Which calendar connector these busy blocks came from, or 'none' if no connector is authorised"),
161
+ eagerTool(server, "schedule-connector-busy-sync", "Reflect your calendar connector's busy blocks into your booking page availability. Call it with NO arguments and it resolves the account's own authorised connector, reads the next 15 days of free/busy itself, and syncs — that is the normal use and needs no reading step from you. Passing connector and busy together is the explicit form: connector='google' or 'm365' with the list of {start,end} ISO 8601 busy intervals, or connector='none' and busy=[] to clear stale blocks. Wholesale-replaces the stored connector-busy set for this account.", {
162
+ connector: z.enum(["google", "m365", "none"]).optional().describe("Which calendar connector these busy blocks came from, or 'none' if no connector is authorised. Omit together with busy to have the tool resolve the account's own connector."),
143
163
  busy: z.array(z.object({
144
164
  start: z.string().describe("ISO 8601 busy-block start"),
145
165
  end: z.string().describe("ISO 8601 busy-block end"),
146
- })).describe("Busy intervals read from the connector. Empty when connector='none'."),
166
+ })).optional().describe("Busy intervals read from the connector. Empty when connector='none'. Omit to have the tool read the next 15 days itself."),
147
167
  }, async (params) => {
148
168
  if (!accountId)
149
169
  return refuseNoAccount("schedule-connector-busy-sync");
170
+ // Both halves are refused rather than defaulted, and for the same reason:
171
+ // either one alone would be silently reinterpreted. A busy list with no
172
+ // connector would have to guess where the intervals came from, and that tag
173
+ // is what a later sync compares against. A connector with no busy list would
174
+ // be discarded in favour of whatever the account actually holds, so a caller
175
+ // naming the wrong connector would be told nothing and get a different one.
176
+ if (Boolean(params.busy) !== Boolean(params.connector)) {
177
+ const passed = params.busy ? "busy" : "connector";
178
+ const missing = params.busy ? "connector" : "busy";
179
+ return {
180
+ content: [{ type: "text", text: `schedule-connector-busy-sync: ${passed} was passed without ${missing}. Pass both, or neither to have the tool resolve and read the account's own connector.` }],
181
+ isError: true,
182
+ };
183
+ }
150
184
  const session = getSession();
151
185
  try {
152
- const r = await syncConnectorBusy(session, accountId, params);
186
+ let r;
187
+ if (params.busy && params.connector) {
188
+ r = await syncConnectorBusy(session, accountId, { connector: params.connector, busy: params.busy });
189
+ }
190
+ else {
191
+ const platformRoot = process.env.PLATFORM_ROOT ?? process.cwd();
192
+ const accountsDir = resolve(platformRoot, "..", "data/accounts");
193
+ // Resolved twice — here and inside resolveAndSyncConnectorBusy — because
194
+ // spawnProviderCall builds the provider environment eagerly and throws
195
+ // when the brand ships no vendor app. An account with no connector at all
196
+ // must reach the clean empty sync instead.
197
+ const { connector } = resolveConnector(join(accountsDir, accountId));
198
+ r = await resolveAndSyncConnectorBusy(session, accountId, {
199
+ accountsDir,
200
+ platformRoot,
201
+ now: new Date(),
202
+ call: connector === "none"
203
+ ? async () => { throw new Error("no calendar connector is authorised for this account"); }
204
+ : spawnProviderCall(platformRoot, accountId, connector),
205
+ });
206
+ }
153
207
  process.stderr.write(`[calendar-availability] op=cal-sync connector=${r.connector} busyBlocks=${r.busyBlocks} reflected=${r.reflected}\n`);
154
208
  return { content: [{ type: "text", text: `Synced ${r.reflected} connector busy block(s) from ${r.connector}.` }] };
155
209
  }
156
210
  catch (err) {
157
211
  const reason = err instanceof Error ? err.message : String(err);
158
- process.stderr.write(`[calendar-availability] op=cal-sync-fail connector=${params.connector} reason=${JSON.stringify(reason)}\n`);
212
+ // `resolved` rather than `undefined` when the caller passed no connector:
213
+ // the connector on this line is what the caller supplied, and the resolved
214
+ // value is already on the `[connector-busy-sync] op=` lines above.
215
+ process.stderr.write(`[calendar-availability] op=cal-sync-fail connector=${params.connector ?? "resolved"} reason=${JSON.stringify(reason)}\n`);
159
216
  return { content: [{ type: "text", text: `Connector busy sync failed: ${reason}` }], isError: true };
160
217
  }
161
218
  finally {
@@ -325,6 +382,7 @@ eagerTool(server, "schedule-update", "Update an event's properties or append a n
325
382
  destination: z.string().describe("E.164 phone (WhatsApp) or Telegram chat id — must already be a registered admin/account-manager on that channel"),
326
383
  prompt: z.string().describe("The instruction the agent runs when the event fires"),
327
384
  }).nullable().optional().describe("Set/replace the agent-bearing dispatch, or pass null to clear it. Mutually exclusive with action."),
385
+ gate: z.object(gateShape).nullable().optional().describe(`${GATE_DESCRIPTION} Pass null to clear it.`),
328
386
  }, async (params) => {
329
387
  if (!accountId)
330
388
  return refuseNoAccount("schedule-update");
@@ -346,6 +404,55 @@ eagerTool(server, "schedule-update", "Update an event's properties or append a n
346
404
  }
347
405
  });
348
406
  // ---------------------------------------------------------------------------
407
+ // scheduling-gate (Task 2378)
408
+ // ---------------------------------------------------------------------------
409
+ eagerTool(server, "scheduling-gate", "Precondition check for a scheduled routine: answers whether any of the named sources has new work, without running a turn. The dispatcher calls this headlessly before firing a gated routine and reads structuredContent.hasWork. Call it yourself with commit=true as the LAST step of a successful routine run, with the same eventId and checks, to advance the watermarks past what you just handled. Never call commit=true on a run that failed or that you did not finish — the work would be marked consumed and the next fire would skip it.", {
410
+ eventId: z.string().describe("The gated routine's event ID. Names the watermark file, so it must match the binding."),
411
+ checks: z.array(z.record(z.unknown())).describe("Checks to run. mail {type,mailbox,folder?,subjectPrefixes?} · d1 {type,database,sql} · whatsapp {type,scope:'inbound'|'status'} · calendar {type,connector}. hasWork is true when ANY check is true."),
412
+ commit: z.boolean().optional().describe("Advance every watermark named in checks to what the sources hold now. Only the routine's own turn sets this, at the end of a successful run."),
413
+ }, async (params) => {
414
+ if (!accountId)
415
+ return refuseNoAccount("scheduling-gate");
416
+ const platformRoot = process.env.PLATFORM_ROOT;
417
+ if (!platformRoot) {
418
+ // Refused rather than defaulted: a guessed root would put the watermark
419
+ // file somewhere the next call would not look, and the routine would
420
+ // report work for ever.
421
+ return {
422
+ content: [{ type: "text", text: "scheduling-gate cannot execute: PLATFORM_ROOT is not set." }],
423
+ isError: true,
424
+ };
425
+ }
426
+ try {
427
+ const result = await runSchedulingGate({ eventId: params.eventId, checks: params.checks, commit: params.commit }, accountId, platformRoot);
428
+ // structuredContent is what the dispatcher reads; the text is for a human
429
+ // or an agent reading the transcript and is never parsed for control flow.
430
+ // No outputSchema is declared: the MCP server validates structuredContent
431
+ // only when a tool has one, so this passes to the wire untouched, and
432
+ // declaring one would mean changing the shared eagerTool signature.
433
+ return {
434
+ content: [
435
+ {
436
+ type: "text",
437
+ text: result.advanced
438
+ ? `hasWork=${result.hasWork} reasons=[${result.reasons.join(", ")}] advanced=[${result.advanced.join(", ")}]`
439
+ : `hasWork=${result.hasWork} reasons=[${result.reasons.join(", ")}]`,
440
+ },
441
+ ],
442
+ structuredContent: result,
443
+ };
444
+ }
445
+ catch (err) {
446
+ // A throw here reaches the dispatcher as an unreadable gate, which fails
447
+ // OPEN and dispatches. That is the safe direction and is why this does not
448
+ // try to synthesise a hasWork value it did not measure.
449
+ return {
450
+ content: [{ type: "text", text: `Gate check failed: ${err instanceof Error ? err.message : String(err)}` }],
451
+ isError: true,
452
+ };
453
+ }
454
+ });
455
+ // ---------------------------------------------------------------------------
349
456
  // schedule-cancel
350
457
  // ---------------------------------------------------------------------------
351
458
  eagerTool(server, "schedule-cancel", "Cancel a scheduled event. For recurring events, this cancels the entire series. Idempotent — cancelling an already-cancelled event is a no-op. Passing a meeting uid deletes that meeting and its attendee links, the same as deleting it on the Calendar page.", {