@edspencer/paddock 0.0.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (296) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +538 -0
  33. package/packages/server/dist/config.js +625 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,417 @@
1
+ /**
2
+ * Unified trigger config helpers (Epic T "Unify Triggers", ticket T1 — foundation).
3
+ *
4
+ * A **trigger** collapses what were two separate config blocks — event **hooks**
5
+ * (Epic G) and cron **schedules** (Epic D) — into ONE declarative shape over the
6
+ * existing `startAgentTurn` execution core (design doc `DESIGN-unified-triggers.md`
7
+ * §2). A trigger is **when** ({@link TriggerWhen}, a discriminated union on
8
+ * `trigger.type`) + **what** ({@link TriggerRun}, a shared agent-run definition) +
9
+ * top-level {@link PaddockTrigger.enabled}:
10
+ *
11
+ * - `schedule` → `cron` **xor** `interval` (fired by herdctl's cron engine)
12
+ * - `event` → a known `on` value (`onArchive` / `afterTurn`, fired by the bus)
13
+ * - `webhook` → `path` — **shape reserved only**; NO ingress is wired in T1
14
+ * (that is the deferred T6/#311). It validates + persists so the config surface
15
+ * is frozen, but nothing fires it.
16
+ *
17
+ * This module is the small, PURE surface around that model — deliberately the same
18
+ * shape as the shipped `hook-config.ts` / `schedule-config.ts` it subsumes, so the
19
+ * `TriggerService` (foundation) and T2–T5 (schedule tool-scoping, verb collapse,
20
+ * Triggers tab, sweeper fold-in) build against ONE frozen contract:
21
+ *
22
+ * - {@link PaddockTriggerSchema} — the Zod **discriminated union** validating one
23
+ * trigger (`trigger.type` discriminant + the cron-xor-interval / prompt-xor-
24
+ * promptFile refinements). Zod (not a hand-rolled sanitizer like its hook/schedule
25
+ * ancestors) per the ticket, because the discriminant + xor rules read cleanly as
26
+ * one schema and give T3's REST layer structured errors — while the MAP-level
27
+ * {@link sanitizeTriggers} keeps the house "drop one malformed entry, don't brick
28
+ * the project" semantics its predecessors have.
29
+ * - {@link sanitizeTrigger} / {@link sanitizeTriggers} — validate an untrusted
30
+ * record / a hand-edited `project.yaml` `triggers` map.
31
+ * - {@link triggerToAgentToolConfig} — project a trigger's {@link TriggerRun} onto
32
+ * the herdctl agent tool-config fields (for an event/webhook trigger that runs as
33
+ * its OWN `trigger-<slug>-<name>` agent), so the grant is enforced by construction.
34
+ * - {@link triggersToHerdctlSchedules} — project schedule-type triggers onto the
35
+ * herdctl `ScheduleSchema` shape forwarded into the keeper agent's `schedules`
36
+ * block (so herdctl's cron engine arms them, exactly as legacy schedules are).
37
+ * - {@link triggerPromptFileAbsPath} — resolve a `promptFile` under the project's
38
+ * `.paddock/triggers/` dir, rejecting traversal / non-`.md`.
39
+ */
40
+ import path from "node:path";
41
+ import { z } from "zod";
42
+ /** The trigger kinds. `webhook` is shape-reserved (no ingress in T1 — deferred T6). */
43
+ export const TRIGGER_TYPES = ["schedule", "event", "webhook"];
44
+ /**
45
+ * The lifecycle events an `event`-type trigger may fire on. `onArchive` is wired
46
+ * (fired after a chat-archive commits — the Epic G motivating cleanup). `afterTurn`
47
+ * is a KNOWN value (validation accepts it) reserved for the sweeper fold-in (T5) —
48
+ * it validates + persists now but isn't emitted until then, exactly as `webhook` is
49
+ * shape-reserved. Adding a sibling event stays a ~3-line change (new value here + a
50
+ * payload + one `emit()` at the commit site).
51
+ */
52
+ export const TRIGGER_EVENTS = ["onArchive", "afterTurn"];
53
+ const PERMISSION_MODES = ["default", "acceptEdits", "bypassPermissions", "plan"];
54
+ /** The dir (relative to a project's working dir) holding keeper-editable prompts. */
55
+ export const TRIGGER_PROMPT_DIR = path.join(".paddock", "triggers");
56
+ /** Default max agent turns for a trigger when its run doesn't set one. */
57
+ export const TRIGGER_DEFAULT_MAX_TURNS = 30;
58
+ /**
59
+ * The canonical name of the built-in post-turn CURATOR trigger — the sweeper folded
60
+ * in as the default `event`/`afterTurn` trigger (T5, design §2.1 #4). A project need
61
+ * NOT declare it: absent, the curator runs with default behaviour (exactly the shipped
62
+ * sweeper). Declaring a `curate-overview` trigger only CUSTOMISES that default (extend
63
+ * its prompt, override its model, or `enabled: false` to switch curation off). Resolved
64
+ * by NAME as a convenience, but recognised structurally by {@link isCuratorTrigger}.
65
+ */
66
+ export const CURATE_TRIGGER_NAME = "curate-overview";
67
+ // --- Zod schema (the frozen discriminated union) -------------------------------
68
+ /**
69
+ * The `schedule` variant — `cron` xor `interval`. The xor can't live inside the
70
+ * discriminatedUnion member (Zod requires plain object members), so the cron-xor-
71
+ * interval rule is enforced by {@link PaddockTriggerSchema}'s `superRefine`; here we
72
+ * only shape/trim the two optional timer fields.
73
+ */
74
+ const scheduleWhenSchema = z.object({
75
+ type: z.literal("schedule"),
76
+ cron: z.string().trim().min(1).optional(),
77
+ interval: z.string().trim().min(1).optional(),
78
+ });
79
+ /** The `event` variant — a KNOWN `on` value (an unknown one fails validation). */
80
+ const eventWhenSchema = z.object({
81
+ type: z.literal("event"),
82
+ on: z.enum(TRIGGER_EVENTS),
83
+ });
84
+ /** The `webhook` variant — shape reserved (a `path`); no ingress is wired in T1. */
85
+ const webhookWhenSchema = z.object({
86
+ type: z.literal("webhook"),
87
+ path: z.string().trim().min(1),
88
+ });
89
+ /** The discriminated union on `trigger.type` — the heart of the unified model. */
90
+ const triggerWhenSchema = z.discriminatedUnion("type", [
91
+ scheduleWhenSchema,
92
+ eventWhenSchema,
93
+ webhookWhenSchema,
94
+ ]);
95
+ /**
96
+ * The shared agent-run definition (design §2.3) — WHAT a fired trigger does,
97
+ * identical across every trigger type. `tools` is a deny-by-default allow-list (the
98
+ * trigger's capability; `[]` = a tool-less curator); `session` selects new-vs-accrete;
99
+ * the rest mirror the per-agent overrides hooks already carry.
100
+ */
101
+ const runSchema = z.object({
102
+ /** Inline prompt (mutually exclusive with {@link promptFile} — enforced below). */
103
+ prompt: z.string().optional(),
104
+ /** Git-tracked `.paddock/triggers/*.md` prompt file, read fresh at fire time. */
105
+ promptFile: z.string().trim().min(1).optional(),
106
+ /** `new` = a fresh chat each fire; `resume` = one owned accreting session. */
107
+ session: z.enum(["new", "resume"]).default("new"),
108
+ /** Optional per-trigger model override (else the keeper default applies). */
109
+ model: z.string().trim().min(1).optional(),
110
+ /** Deny-by-default allow-list = the trigger's capability. `[]` = tool-less. */
111
+ tools: z.array(z.string().trim().min(1)).default([]),
112
+ /** Recursion bound for internal spawning (reuses B1); 0 = may not spawn. */
113
+ maxSpawnDepth: z.number().int().nonnegative().optional(),
114
+ /** Optional permission mode (else inherits the project-agent default). */
115
+ permissionMode: z.enum(PERMISSION_MODES).optional(),
116
+ /** Optional max-turns cap (else {@link TRIGGER_DEFAULT_MAX_TURNS}). */
117
+ maxTurns: z.number().int().positive().optional(),
118
+ });
119
+ /**
120
+ * The frozen trigger schema: `trigger` (discriminated union) + `run` + `enabled`.
121
+ * Two cross-field refinements the discriminatedUnion can't express inline:
122
+ * - a `schedule` trigger requires EXACTLY ONE of `cron` / `interval`;
123
+ * - `run` requires EXACTLY ONE of `prompt` / `promptFile`.
124
+ *
125
+ * `enabled` **defaults `false`** — a trigger created programmatically (or via a
126
+ * hand-edit that omits it) is inert until deliberately switched on (design §2.3,
127
+ * mirroring the Epic G safe-create default).
128
+ */
129
+ export const PaddockTriggerSchema = z
130
+ .object({
131
+ trigger: triggerWhenSchema,
132
+ run: runSchema,
133
+ enabled: z.boolean().default(false),
134
+ })
135
+ .superRefine((val, ctx) => {
136
+ if (val.trigger.type === "schedule") {
137
+ const hasCron = val.trigger.cron !== undefined;
138
+ const hasInterval = val.trigger.interval !== undefined;
139
+ // XOR: both or neither is invalid.
140
+ if (hasCron === hasInterval) {
141
+ ctx.addIssue({
142
+ code: z.ZodIssueCode.custom,
143
+ path: ["trigger"],
144
+ message: "a schedule trigger requires exactly one of `cron` or `interval`",
145
+ });
146
+ }
147
+ }
148
+ const hasPrompt = val.run.prompt !== undefined;
149
+ const hasFile = val.run.promptFile !== undefined;
150
+ // XOR: exactly one of an inline prompt or a prompt file.
151
+ if (hasPrompt === hasFile) {
152
+ ctx.addIssue({
153
+ code: z.ZodIssueCode.custom,
154
+ path: ["run"],
155
+ message: "`run` requires exactly one of `prompt` or `promptFile`",
156
+ });
157
+ }
158
+ });
159
+ // --- validation ---------------------------------------------------------------
160
+ /** A trigger name we're willing to key on (also a safe herdctl agent-name segment). */
161
+ export function isValidTriggerName(name) {
162
+ return typeof name === "string" && /^[A-Za-z0-9._-]+$/.test(name) && name.length <= 64;
163
+ }
164
+ /**
165
+ * Validate + normalise one untrusted trigger record into a {@link PaddockTrigger},
166
+ * or `null` if it's malformed (bad discriminant, both/neither cron+interval, unknown
167
+ * `on`, both/neither prompt+promptFile, …). Defaults are applied (`enabled: false`,
168
+ * `session: "new"`, `tools: []`). Unknown fields are stripped (Zod default).
169
+ */
170
+ export function sanitizeTrigger(raw) {
171
+ const result = PaddockTriggerSchema.safeParse(raw);
172
+ return result.success ? result.data : null;
173
+ }
174
+ /**
175
+ * Sanitise a whole `triggers` map, DROPPING malformed entries and entries with an
176
+ * unsafe name — so one bad hand-edit can't brick the project's agent registration
177
+ * (an invalid trigger agent config would throw in `addAgent`). Returns `undefined`
178
+ * when nothing survives (so it stays absent on disk / off the project record),
179
+ * exactly like its `sanitizeHooks` / `sanitizeSchedules` ancestors.
180
+ */
181
+ export function sanitizeTriggers(raw) {
182
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
183
+ return undefined;
184
+ const out = {};
185
+ for (const [name, val] of Object.entries(raw)) {
186
+ if (!isValidTriggerName(name))
187
+ continue;
188
+ const t = sanitizeTrigger(val);
189
+ if (t)
190
+ out[name] = t;
191
+ }
192
+ return Object.keys(out).length > 0 ? out : undefined;
193
+ }
194
+ /**
195
+ * Overlay a PARTIAL trigger update onto an existing trigger, producing an untrusted
196
+ * record for {@link sanitizeTrigger} to validate — the unified successor to
197
+ * {@link import("./hook-config.js").mergeHookUpdate}, used by the self-MCP `set_trigger`
198
+ * tool so an edit that supplies only some fields (the classic being "enable/disable",
199
+ * an `enabled`-only set — GG-3) preserves everything else rather than wiping it.
200
+ *
201
+ * The three parts merge independently:
202
+ * - `trigger` (the WHEN discriminant): supplied wholesale or inherited. Because the
203
+ * variants carry different fields (`cron`/`interval` vs `on` vs `path`), a partial
204
+ * trigger can't be merged field-by-field — so `incoming.trigger` is taken WHOLE
205
+ * when the caller supplies one (i.e. re-specifies `type` + its fields), else the
206
+ * existing `trigger` is inherited unchanged.
207
+ * - `run` (the WHAT): merged field-by-field over the existing run, so an edit that
208
+ * touches only `enabled`/`model`/`tools` keeps the rest. `prompt` and `promptFile`
209
+ * are MUTUALLY EXCLUSIVE, so supplying one clears the inherited counterpart —
210
+ * otherwise switching a file-backed trigger to an inline prompt would leave the
211
+ * stale `promptFile` and the merged record would carry BOTH (rejected by the
212
+ * schema's prompt-xor-promptFile refinement). Supplying neither leaves the prompt
213
+ * source untouched.
214
+ * - `enabled`: the supplied value, else the existing flag, else `false` (safe-create).
215
+ *
216
+ * A brand-new trigger (`existing` null) starts from the incoming record verbatim
217
+ * (its `trigger`/`run` must be complete — the schema rejects it otherwise).
218
+ */
219
+ export function mergeTriggerUpdate(existing, incoming) {
220
+ // WHEN: a supplied `trigger` (with a `type`) replaces the whole discriminant; else inherit.
221
+ const trigger = incoming.trigger && typeof incoming.trigger.type === "string"
222
+ ? incoming.trigger
223
+ : existing?.trigger;
224
+ // WHAT: overlay the supplied run fields on the existing run (or {} for a new trigger).
225
+ const run = { ...(existing?.run ?? {}), ...(incoming.run ?? {}) };
226
+ const inRun = incoming.run ?? {};
227
+ // Switching prompt source: one supplied side clears the inherited other, so the
228
+ // merged run never carries both (which the prompt-xor-promptFile refinement rejects).
229
+ if (inRun.prompt !== undefined && inRun.promptFile === undefined)
230
+ delete run.promptFile;
231
+ if (inRun.promptFile !== undefined && inRun.prompt === undefined)
232
+ delete run.prompt;
233
+ const enabled = incoming.enabled ?? existing?.enabled ?? false;
234
+ return { trigger, run, enabled };
235
+ }
236
+ // --- projection helpers -------------------------------------------------------
237
+ /**
238
+ * Whether a trigger runs on its OWN scoped `trigger-<slug>-<name>` agent (so
239
+ * {@link triggerToAgentToolConfig} governs its capability) rather than as the keeper.
240
+ *
241
+ * - **event** → ALWAYS its own agent (Epic G / T1). Its `tools` allow-list IS the
242
+ * capability; an empty `[]` means a deliberately tool-less curator.
243
+ * - **schedule** → its own scoped agent ONLY when it declares a NON-EMPTY `run.tools`
244
+ * allow-list (T2 — the per-trigger tool-scoping this ticket adds). A schedule with NO
245
+ * tools inherits the project agent's default toolset by running as the keeper,
246
+ * preserving the pre-T2 behaviour (ticket #307: "a schedule with no tools inherits
247
+ * the project-agent default"). This is the one asymmetry with events — a schedule's
248
+ * empty `tools` reads as "unscoped, run as keeper", NOT "tool-less curator", because
249
+ * schedules historically ran with the keeper's full tools and there are no users to
250
+ * migrate off that default.
251
+ * - **webhook** → reserved (never fired in T1), so it registers no agent.
252
+ *
253
+ * The ONE place the keeper-vs-own-agent routing decision lives, shared by the herdctl
254
+ * agent registration ({@link import("./herdctl.js").HerdctlService.registerTriggerAgents})
255
+ * and the fire path (`fireTriggerForProject` in `ws.ts`) so arming and firing always agree.
256
+ */
257
+ export function triggerRunsOnOwnAgent(trigger) {
258
+ if (trigger.trigger.type === "event")
259
+ return true;
260
+ if (trigger.trigger.type === "schedule")
261
+ return (trigger.run.tools?.length ?? 0) > 0;
262
+ return false; // webhook: shape reserved, nothing fires it.
263
+ }
264
+ /**
265
+ * Whether a trigger is the post-turn CURATOR (the folded-in sweeper, T5): an
266
+ * `event`/`afterTurn` trigger. The curator is SPECIAL — it does NOT run as a generic
267
+ * `trigger-<slug>-<name>` agent turn. It is tool-less by nature (returns marked text,
268
+ * Paddock's `SweepService` parses it and writes OVERVIEW.md/CHANGELOG.md), so it is
269
+ * executed by the SweepService via the project's `sweeper-<slug>` agent, NOT by the
270
+ * event dispatcher and NOT by its own herdctl agent. Its `run` therefore only tunes the
271
+ * sweep (prompt extension, model, `enabled`), which is why (unlike every OTHER event
272
+ * trigger — see {@link triggerRunsOnOwnAgent}) a curator trigger registers NO scoped
273
+ * agent and is skipped by the generic fire path. This keeps the sweeper's single
274
+ * dispatch (no double-curation): the `afterTurn` event → SweepService, once.
275
+ */
276
+ export function isCuratorTrigger(trigger) {
277
+ return trigger.trigger.type === "event" && trigger.trigger.on === "afterTurn";
278
+ }
279
+ /**
280
+ * Resolve a project's post-turn CURATOR trigger from its `triggers` map (T5) — the
281
+ * canonical {@link CURATE_TRIGGER_NAME} if it IS a curator, else the first structural
282
+ * curator ({@link isCuratorTrigger}) declared. `null` when the project declares none
283
+ * (the IMPLICIT built-in default — an un-customised project sweeps exactly as the
284
+ * shipped sweeper). Pure, so both `SweepService` (enabled gate + prompt extension) and
285
+ * `HerdctlService` (sweeper model override) resolve the ONE curator the same way.
286
+ */
287
+ export function curatorTriggerOf(triggers) {
288
+ if (!triggers)
289
+ return null;
290
+ const named = triggers[CURATE_TRIGGER_NAME];
291
+ if (named && isCuratorTrigger(named))
292
+ return named;
293
+ for (const t of Object.values(triggers))
294
+ if (isCuratorTrigger(t))
295
+ return t;
296
+ return null;
297
+ }
298
+ /**
299
+ * Project a trigger's {@link TriggerRun} onto the exact herdctl agent tool-config
300
+ * fields (snake_case), so an event/webhook trigger's OWN `trigger-<slug>-<name>`
301
+ * agent enforces the capability BY CONSTRUCTION. Always sets `allowed_tools` +
302
+ * `max_turns` so a trigger agent never silently inherits the keeper's broad default
303
+ * toolset; a tool-less trigger yields `allowed_tools: []` (the CLI runtime then
304
+ * denies every tool). The ONE place run→agent-config translation lives. (The exact
305
+ * analogue of `hookToAgentToolConfig`.)
306
+ */
307
+ export function triggerToAgentToolConfig(run) {
308
+ const out = {
309
+ allowed_tools: run.tools ?? [],
310
+ max_turns: run.maxTurns ?? TRIGGER_DEFAULT_MAX_TURNS,
311
+ };
312
+ if (run.permissionMode)
313
+ out.permission_mode = run.permissionMode;
314
+ if (run.model)
315
+ out.model = run.model;
316
+ return out;
317
+ }
318
+ /**
319
+ * Project ONE schedule-type trigger onto the herdctl `ScheduleSchema` fields the
320
+ * cron engine reads (`type` `cron`|`interval`, the timer field, `resume_session`,
321
+ * `enabled`), for forwarding into the keeper agent's `schedules` block. The
322
+ * Paddock-only prompt (inline/`promptFile`) is resolved at fire time by the trigger
323
+ * handler, so herdctl only ever sees a plain `prompt` (or none) — kept pure. The
324
+ * `resume_session` flag is derived from `run.session` so herdctl's own new-vs-accrete
325
+ * stays in agreement with Paddock's (the handler still owns the OWNED-session sidecar).
326
+ */
327
+ export function scheduleTriggerToHerdctl(when, run, enabled) {
328
+ const isCron = when.cron !== undefined;
329
+ const out = { type: isCron ? "cron" : "interval" };
330
+ if (isCron)
331
+ out.cron = when.cron;
332
+ else
333
+ out.interval = when.interval;
334
+ // Forward the inline prompt only when a promptFile isn't driving the trigger (the
335
+ // handler reads the file fresh at fire time and supplies the resolved text).
336
+ if (typeof run.prompt === "string" && run.promptFile === undefined)
337
+ out.prompt = run.prompt;
338
+ out.resume_session = run.session === "resume";
339
+ // Forward the Paddock-level enabled flag so herdctl's engine doesn't even fire a
340
+ // disabled schedule trigger (the handler double-checks too — belt and braces).
341
+ out.enabled = enabled;
342
+ return out;
343
+ }
344
+ /**
345
+ * Build the `schedules` block to forward into the keeper agent config from a
346
+ * project's SCHEDULE-type triggers, or `undefined` when it declares none. Non-schedule
347
+ * triggers (event/webhook) are skipped — they don't arm the cron engine. Keyed by
348
+ * trigger name (the same key the fire handler resolves back to a trigger).
349
+ */
350
+ export function triggersToHerdctlSchedules(map) {
351
+ if (!map)
352
+ return undefined;
353
+ const out = {};
354
+ for (const [name, t] of Object.entries(map)) {
355
+ if (t.trigger.type !== "schedule")
356
+ continue;
357
+ out[name] = scheduleTriggerToHerdctl(t.trigger, t.run, t.enabled === true);
358
+ }
359
+ return Object.keys(out).length > 0 ? out : undefined;
360
+ }
361
+ /**
362
+ * Project a persisted trigger ({@link TriggerDto} fields) onto the web-facing
363
+ * {@link ChatTriggerInfo}, resolving the SAME capability defaults
364
+ * {@link triggerToAgentToolConfig} uses so the banner mirrors the enforced grant
365
+ * exactly: an absent/empty allow-list surfaces as a tool-less `[]`, and an unset
366
+ * `maxTurns` surfaces the {@link TRIGGER_DEFAULT_MAX_TURNS} bound herdctl applies.
367
+ * The WHEN fields are read off the discriminated `trigger` so the banner can state
368
+ * the trigger type and the exact firing condition.
369
+ */
370
+ export function toChatTriggerInfo(dto) {
371
+ const when = dto.trigger;
372
+ const run = dto.run;
373
+ const info = {
374
+ name: dto.name,
375
+ type: when.type,
376
+ agentName: dto.agentName,
377
+ enabled: dto.enabled === true,
378
+ allowedTools: run.tools ?? [],
379
+ maxTurns: run.maxTurns ?? TRIGGER_DEFAULT_MAX_TURNS,
380
+ };
381
+ if (when.type === "event")
382
+ info.event = when.on;
383
+ else if (when.type === "schedule") {
384
+ if (when.cron !== undefined)
385
+ info.cron = when.cron;
386
+ if (when.interval !== undefined)
387
+ info.interval = when.interval;
388
+ }
389
+ else if (when.type === "webhook")
390
+ info.path = when.path;
391
+ if (run.permissionMode)
392
+ info.permissionMode = run.permissionMode;
393
+ if (run.model)
394
+ info.model = run.model;
395
+ return info;
396
+ }
397
+ /**
398
+ * Resolve a `promptFile` to an absolute path under the project's `.paddock/triggers/`
399
+ * dir, or `null` if it escapes that dir, is absolute, or isn't a `.md` file. The name
400
+ * is treated as relative to the triggers dir (so a bare `"daily.md"` resolves to
401
+ * `<workingDir>/.paddock/triggers/daily.md`). Byte-for-byte the hook/schedule twin's guard.
402
+ */
403
+ export function triggerPromptFileAbsPath(workingDir, promptFile) {
404
+ if (typeof promptFile !== "string" || promptFile.trim() === "")
405
+ return null;
406
+ const rel = promptFile.trim();
407
+ if (path.isAbsolute(rel))
408
+ return null;
409
+ const base = path.resolve(workingDir, TRIGGER_PROMPT_DIR);
410
+ const target = path.resolve(base, rel);
411
+ const within = target === base || target.startsWith(base + path.sep);
412
+ if (!within)
413
+ return null;
414
+ if (!target.toLowerCase().endsWith(".md"))
415
+ return null;
416
+ return target;
417
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Per-trigger RUNTIME state builder (Epic T follow-up / #327) — the live "last-run /
3
+ * next-run / status" view the Triggers tab renders alongside each trigger's config.
4
+ *
5
+ * When Epic T (#305 / T4) folded the Settings→Schedules section into the unified
6
+ * Triggers tab, two capabilities were dropped because {@link TriggerDto} carries
7
+ * trigger *config* only, not herdctl *runtime* state: the old section could show a
8
+ * schedule's last/next run + status, and could fire it on demand. This module restores
9
+ * the STATUS half — it JOINS the config DTOs with two herdctl runtime sources:
10
+ *
11
+ * - **`ScheduleInfo`** (the cron scheduler's live state, keyed by trigger name — the
12
+ * same key the forwarded `schedules` block uses): `nextRunAt` (the only source of a
13
+ * future fire time), `lastRunAt`, `status` (idle/running/disabled), and `lastError`.
14
+ * Meaningful for SCHEDULE-type triggers only.
15
+ * - **job records** (`JobMetadata`, per the E3/#268 run-history pattern): the trigger's
16
+ * most-recent RUN — its session id, terminal status, exit reason, timing, and the
17
+ * agent's one-line summary. Attributable ONLY for a trigger that runs on its OWN
18
+ * scoped `trigger-<slug>-<name>` agent ({@link triggerRunsOnOwnAgent}): every event
19
+ * trigger, and a schedule trigger with a `run.tools` allow-list (T2). An UNSCOPED
20
+ * schedule trigger runs as the shared keeper, whose job records can't be attributed
21
+ * back to one trigger — so its last-run falls back to `ScheduleInfo.lastRunAt`.
22
+ *
23
+ * The build step is deliberately PURE (no I/O): the route resolves the DTOs, the flat
24
+ * job list ({@link HerdctlService.listRunsForAgents}) and the schedule state
25
+ * ({@link HerdctlService.listAgentSchedules}), then hands them here so the join stays
26
+ * unit-testable — the same split {@link import("./runs.js").buildProjectRuns} uses.
27
+ */
28
+ import type { JobMetadata } from "@herdctl/core";
29
+ import { type TriggerDto } from "./trigger-config.js";
30
+ /**
31
+ * The slice of herdctl's `ScheduleInfo` this builder consumes — a local structural
32
+ * type (mirrors the one in routes.ts / ws.ts) so the module stays off
33
+ * `@herdctl/core`'s import surface. Keyed by the trigger's name.
34
+ */
35
+ export interface ScheduleRuntimeInfo {
36
+ status?: "idle" | "running" | "disabled";
37
+ lastRunAt?: string | null;
38
+ nextRunAt?: string | null;
39
+ lastError?: string | null;
40
+ }
41
+ /** The trigger's most-recent RUN, projected from a herdctl job record (or a schedule fire). */
42
+ export interface TriggerLastRun {
43
+ /** herdctl job id, or null when the last-run is only known from schedule state. */
44
+ jobId: string | null;
45
+ /** The chat the run belongs to (link target), or null. */
46
+ sessionId: string | null;
47
+ /** Terminal/live status: completed | failed | cancelled | running | pending. */
48
+ status: string;
49
+ /** Why it exited (success | error | max_turns | timeout | cancelled), if recorded. */
50
+ exitReason: string | null;
51
+ /** ISO timestamp the run started. */
52
+ startedAt: string;
53
+ /** ISO timestamp the run finished, or null while running / unrecorded. */
54
+ finishedAt: string | null;
55
+ /** Wall-clock seconds, or null while running / unrecorded. */
56
+ durationSeconds: number | null;
57
+ /** The agent's own one-line summary of the run, when it wrote one. */
58
+ summary: string | null;
59
+ }
60
+ /** One trigger's live runtime state — the "status" half the Triggers tab renders. */
61
+ export interface TriggerRuntime {
62
+ /** The trigger's name (its project.yaml map key). */
63
+ name: string;
64
+ /** WHEN discriminant, echoed so the client styles per type without re-parsing. */
65
+ type: "schedule" | "event" | "webhook";
66
+ /** True when a run is in flight (a live job, or the cron scheduler reports running). */
67
+ running: boolean;
68
+ /** ISO timestamp of the next scheduled fire (SCHEDULE triggers only), else null. */
69
+ nextRunAt: string | null;
70
+ /**
71
+ * The cron scheduler's status for a SCHEDULE trigger (idle/running/disabled), else
72
+ * null. Falls back to disabled/idle from the config `enabled` when herdctl hasn't
73
+ * armed it yet (or the keeper isn't running).
74
+ */
75
+ scheduleStatus: string | null;
76
+ /** The last fire's error message (SCHEDULE triggers), or null. */
77
+ lastError: string | null;
78
+ /** The most-recent run, or null when the trigger has never fired. */
79
+ lastRun: TriggerLastRun | null;
80
+ }
81
+ /**
82
+ * Join trigger config DTOs with herdctl runtime state into the per-trigger status view.
83
+ *
84
+ * @param triggers The project's trigger DTOs (config only).
85
+ * @param runs Job records for the project's keeper + trigger agents, most-recent
86
+ * first (as {@link HerdctlService.listRunsForAgents} returns). Grouped
87
+ * by `agent` here; a scoped trigger's newest own-agent job is its run.
88
+ * @param schedules The cron scheduler's live `ScheduleInfo` for the keeper's forwarded
89
+ * schedules (keyed by trigger name).
90
+ * @param slug The project slug — to resolve each trigger's scoped agent name.
91
+ */
92
+ export declare function buildTriggerRuntime(triggers: TriggerDto[], runs: JobMetadata[], schedules: Array<ScheduleRuntimeInfo & {
93
+ name: string;
94
+ }>, slug: string): TriggerRuntime[];
@@ -0,0 +1,82 @@
1
+ import { triggerAgentName } from "./herdctl.js";
2
+ import { triggerRunsOnOwnAgent } from "./trigger-config.js";
3
+ /** Project a job record onto the {@link TriggerLastRun} shape. */
4
+ function lastRunFromJob(job) {
5
+ return {
6
+ jobId: job.id,
7
+ sessionId: job.session_id ?? null,
8
+ status: job.status,
9
+ exitReason: job.exit_reason ?? null,
10
+ startedAt: job.started_at,
11
+ finishedAt: job.finished_at ?? null,
12
+ durationSeconds: job.duration_seconds ?? null,
13
+ summary: job.summary ?? null,
14
+ };
15
+ }
16
+ /**
17
+ * Synthesize a minimal {@link TriggerLastRun} from the cron scheduler's `lastRunAt`
18
+ * for an UNSCOPED schedule trigger (no attributable job record). herdctl tracks only a
19
+ * timestamp + a `lastError`, so status is inferred: `failed` when the last fire erred,
20
+ * else `completed` (a still-running fire is surfaced via {@link TriggerRuntime.running}
21
+ * / `scheduleStatus`, not here).
22
+ */
23
+ function lastRunFromSchedule(info) {
24
+ if (!info.lastRunAt)
25
+ return null;
26
+ return {
27
+ jobId: null,
28
+ sessionId: null,
29
+ status: info.lastError ? "failed" : "completed",
30
+ exitReason: info.lastError ? "error" : null,
31
+ startedAt: info.lastRunAt,
32
+ finishedAt: info.lastRunAt,
33
+ durationSeconds: null,
34
+ summary: null,
35
+ };
36
+ }
37
+ /**
38
+ * Join trigger config DTOs with herdctl runtime state into the per-trigger status view.
39
+ *
40
+ * @param triggers The project's trigger DTOs (config only).
41
+ * @param runs Job records for the project's keeper + trigger agents, most-recent
42
+ * first (as {@link HerdctlService.listRunsForAgents} returns). Grouped
43
+ * by `agent` here; a scoped trigger's newest own-agent job is its run.
44
+ * @param schedules The cron scheduler's live `ScheduleInfo` for the keeper's forwarded
45
+ * schedules (keyed by trigger name).
46
+ * @param slug The project slug — to resolve each trigger's scoped agent name.
47
+ */
48
+ export function buildTriggerRuntime(triggers, runs, schedules, slug) {
49
+ // Newest job per agent — `runs` is already most-recent-first, so the FIRST match wins.
50
+ const newestByAgent = new Map();
51
+ for (const job of runs) {
52
+ if (!newestByAgent.has(job.agent))
53
+ newestByAgent.set(job.agent, job);
54
+ }
55
+ const scheduleByName = new Map(schedules.map((s) => [s.name, s]));
56
+ return triggers.map((dto) => {
57
+ const type = dto.trigger.type;
58
+ const isSchedule = type === "schedule";
59
+ const info = isSchedule ? scheduleByName.get(dto.name) : undefined;
60
+ // A scoped trigger (every event; a schedule with a `run.tools` allow-list) writes
61
+ // job records under its OWN agent — attribute its newest one. An unscoped schedule
62
+ // runs as the shared keeper (no per-trigger attribution) — fall back to the cron
63
+ // scheduler's timestamp.
64
+ const job = triggerRunsOnOwnAgent(dto)
65
+ ? newestByAgent.get(triggerAgentName(slug, dto.name))
66
+ : undefined;
67
+ const lastRun = job ? lastRunFromJob(job) : info ? lastRunFromSchedule(info) : null;
68
+ const running = job?.status === "running" || info?.status === "running" || false;
69
+ const scheduleStatus = isSchedule
70
+ ? info?.status ?? (dto.enabled === false ? "disabled" : "idle")
71
+ : null;
72
+ return {
73
+ name: dto.name,
74
+ type,
75
+ running,
76
+ nextRunAt: isSchedule ? info?.nextRunAt ?? null : null,
77
+ scheduleStatus,
78
+ lastError: isSchedule ? info?.lastError ?? null : null,
79
+ lastRun,
80
+ };
81
+ });
82
+ }
@@ -0,0 +1,34 @@
1
+ export declare class TriggerSessionStore {
2
+ private readonly stateFile;
3
+ /** In-memory map of `<slug>\0<triggerName>` -> owned sessionId. */
4
+ private state;
5
+ /**
6
+ * The in-flight load, cached so concurrent first-callers share ONE read (the
7
+ * RunProvenanceStore fix): caching only the resolved `state` would let several
8
+ * fires that begin before the first `fs.readFile` resolves each build their own
9
+ * map and race on the assignment — a lost update.
10
+ */
11
+ private loadPromise;
12
+ /** Serialises concurrent writes so the file never interleaves. */
13
+ private writing;
14
+ constructor(dataDir: string);
15
+ private static key;
16
+ /** Load the persisted map (lazily, deduped; tolerant of a missing/corrupt file). */
17
+ private ensureLoaded;
18
+ /** The session id this trigger owns, or undefined if it hasn't fired yet. Non-throwing. */
19
+ get(slug: string, triggerName: string): Promise<string | undefined>;
20
+ /**
21
+ * Record the session a trigger owns (its first-fire chat). Idempotent for the same
22
+ * id; an unsafe/blank id is ignored. Overwrites if called again (used only on a
23
+ * fresh first fire, or to re-point after a stale id was cleared).
24
+ */
25
+ set(slug: string, triggerName: string, sessionId: string): Promise<void>;
26
+ /**
27
+ * Forget a trigger's owned session — used when the recorded transcript has vanished
28
+ * (so the next fire re-creates one) or the trigger was removed. No-op if there was
29
+ * nothing recorded.
30
+ */
31
+ clear(slug: string, triggerName: string): Promise<void>;
32
+ /** Write-through, serialised so overlapping writes can't corrupt the file. */
33
+ private persist;
34
+ }