@edspencer/paddock 0.0.1 → 0.56.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 (294) 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 +512 -0
  33. package/packages/server/dist/config.js +593 -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/event-bus.d.ts +87 -0
  37. package/packages/server/dist/event-bus.js +48 -0
  38. package/packages/server/dist/git.d.ts +128 -0
  39. package/packages/server/dist/git.js +446 -0
  40. package/packages/server/dist/github-auth.d.ts +44 -0
  41. package/packages/server/dist/github-auth.js +169 -0
  42. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  43. package/packages/server/dist/herdctl-agent-config.js +331 -0
  44. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  45. package/packages/server/dist/herdctl-agent-names.js +228 -0
  46. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  47. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  48. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  49. package/packages/server/dist/herdctl-jobs.js +225 -0
  50. package/packages/server/dist/herdctl.d.ts +572 -0
  51. package/packages/server/dist/herdctl.js +1218 -0
  52. package/packages/server/dist/hook-config.d.ts +217 -0
  53. package/packages/server/dist/hook-config.js +224 -0
  54. package/packages/server/dist/http-bytes.d.ts +24 -0
  55. package/packages/server/dist/http-bytes.js +52 -0
  56. package/packages/server/dist/index.d.ts +1 -0
  57. package/packages/server/dist/index.js +14 -0
  58. package/packages/server/dist/instance-config.d.ts +135 -0
  59. package/packages/server/dist/instance-config.js +345 -0
  60. package/packages/server/dist/localcommand.d.ts +23 -0
  61. package/packages/server/dist/localcommand.js +140 -0
  62. package/packages/server/dist/management-auth.d.ts +99 -0
  63. package/packages/server/dist/management-auth.js +165 -0
  64. package/packages/server/dist/management-config.d.ts +164 -0
  65. package/packages/server/dist/management-config.js +286 -0
  66. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  67. package/packages/server/dist/management-mcp-server.js +95 -0
  68. package/packages/server/dist/management-metadata.d.ts +67 -0
  69. package/packages/server/dist/management-metadata.js +56 -0
  70. package/packages/server/dist/management-ops.d.ts +110 -0
  71. package/packages/server/dist/management-ops.js +449 -0
  72. package/packages/server/dist/management-policy.d.ts +172 -0
  73. package/packages/server/dist/management-policy.js +259 -0
  74. package/packages/server/dist/message-provenance.d.ts +112 -0
  75. package/packages/server/dist/message-provenance.js +260 -0
  76. package/packages/server/dist/models.d.ts +164 -0
  77. package/packages/server/dist/models.js +208 -0
  78. package/packages/server/dist/openapi.d.ts +66 -0
  79. package/packages/server/dist/openapi.js +183 -0
  80. package/packages/server/dist/parent-detach.d.ts +30 -0
  81. package/packages/server/dist/parent-detach.js +109 -0
  82. package/packages/server/dist/preload.d.ts +43 -0
  83. package/packages/server/dist/preload.js +55 -0
  84. package/packages/server/dist/project-files.d.ts +76 -0
  85. package/packages/server/dist/project-files.js +191 -0
  86. package/packages/server/dist/project-mime.d.ts +35 -0
  87. package/packages/server/dist/project-mime.js +55 -0
  88. package/packages/server/dist/project-paths.d.ts +88 -0
  89. package/packages/server/dist/project-paths.js +126 -0
  90. package/packages/server/dist/project-types.d.ts +316 -0
  91. package/packages/server/dist/project-types.js +48 -0
  92. package/packages/server/dist/projects.d.ts +202 -0
  93. package/packages/server/dist/projects.js +894 -0
  94. package/packages/server/dist/queued-message.d.ts +35 -0
  95. package/packages/server/dist/queued-message.js +116 -0
  96. package/packages/server/dist/read-state.d.ts +46 -0
  97. package/packages/server/dist/read-state.js +139 -0
  98. package/packages/server/dist/recovery-config.d.ts +111 -0
  99. package/packages/server/dist/recovery-config.js +105 -0
  100. package/packages/server/dist/recovery.d.ts +262 -0
  101. package/packages/server/dist/recovery.js +490 -0
  102. package/packages/server/dist/resume-drain.d.ts +122 -0
  103. package/packages/server/dist/resume-drain.js +158 -0
  104. package/packages/server/dist/route-context.d.ts +168 -0
  105. package/packages/server/dist/route-context.js +39 -0
  106. package/packages/server/dist/route-errors.d.ts +8 -0
  107. package/packages/server/dist/route-errors.js +13 -0
  108. package/packages/server/dist/routes/chats.d.ts +12 -0
  109. package/packages/server/dist/routes/chats.js +1406 -0
  110. package/packages/server/dist/routes/git.d.ts +13 -0
  111. package/packages/server/dist/routes/git.js +236 -0
  112. package/packages/server/dist/routes/mcp.d.ts +83 -0
  113. package/packages/server/dist/routes/mcp.js +250 -0
  114. package/packages/server/dist/routes/meta.d.ts +15 -0
  115. package/packages/server/dist/routes/meta.js +447 -0
  116. package/packages/server/dist/routes/projects.d.ts +25 -0
  117. package/packages/server/dist/routes/projects.js +736 -0
  118. package/packages/server/dist/routes/triggers.d.ts +21 -0
  119. package/packages/server/dist/routes/triggers.js +324 -0
  120. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  121. package/packages/server/dist/routes/workspace-mount.js +32 -0
  122. package/packages/server/dist/routes.d.ts +26 -0
  123. package/packages/server/dist/routes.js +29 -0
  124. package/packages/server/dist/run-provenance.d.ts +109 -0
  125. package/packages/server/dist/run-provenance.js +206 -0
  126. package/packages/server/dist/runs.d.ts +92 -0
  127. package/packages/server/dist/runs.js +72 -0
  128. package/packages/server/dist/schedule-config.d.ts +68 -0
  129. package/packages/server/dist/schedule-config.js +75 -0
  130. package/packages/server/dist/schedule-session.d.ts +34 -0
  131. package/packages/server/dist/schedule-session.js +120 -0
  132. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  133. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  134. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  135. package/packages/server/dist/self-mcp-read.js +97 -0
  136. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  137. package/packages/server/dist/self-mcp-triggers.js +144 -0
  138. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  139. package/packages/server/dist/self-mcp-types.js +10 -0
  140. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  141. package/packages/server/dist/self-mcp-util.js +157 -0
  142. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  143. package/packages/server/dist/self-mcp-write.js +215 -0
  144. package/packages/server/dist/self-mcp.d.ts +95 -0
  145. package/packages/server/dist/self-mcp.js +448 -0
  146. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  147. package/packages/server/dist/send-file-mcp.js +281 -0
  148. package/packages/server/dist/session-hub.d.ts +186 -0
  149. package/packages/server/dist/session-hub.js +291 -0
  150. package/packages/server/dist/spawn-capability.d.ts +99 -0
  151. package/packages/server/dist/spawn-capability.js +106 -0
  152. package/packages/server/dist/star.d.ts +26 -0
  153. package/packages/server/dist/star.js +94 -0
  154. package/packages/server/dist/start.d.ts +1 -0
  155. package/packages/server/dist/start.js +21 -0
  156. package/packages/server/dist/subagents.d.ts +337 -0
  157. package/packages/server/dist/subagents.js +626 -0
  158. package/packages/server/dist/sweep.d.ts +122 -0
  159. package/packages/server/dist/sweep.js +696 -0
  160. package/packages/server/dist/tooldetails.d.ts +33 -0
  161. package/packages/server/dist/tooldetails.js +466 -0
  162. package/packages/server/dist/transcribe.d.ts +69 -0
  163. package/packages/server/dist/transcribe.js +119 -0
  164. package/packages/server/dist/transcripts.d.ts +42 -0
  165. package/packages/server/dist/transcripts.js +164 -0
  166. package/packages/server/dist/trigger-config.d.ts +278 -0
  167. package/packages/server/dist/trigger-config.js +417 -0
  168. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  169. package/packages/server/dist/trigger-runtime.js +82 -0
  170. package/packages/server/dist/trigger-session.d.ts +34 -0
  171. package/packages/server/dist/trigger-session.js +122 -0
  172. package/packages/server/dist/triggers.d.ts +83 -0
  173. package/packages/server/dist/triggers.js +124 -0
  174. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  175. package/packages/server/dist/trusted-proxy.js +157 -0
  176. package/packages/server/dist/turn-notice.d.ts +154 -0
  177. package/packages/server/dist/turn-notice.js +271 -0
  178. package/packages/server/dist/unread.d.ts +36 -0
  179. package/packages/server/dist/unread.js +123 -0
  180. package/packages/server/dist/usage.d.ts +66 -0
  181. package/packages/server/dist/usage.js +243 -0
  182. package/packages/server/dist/wake-injection.d.ts +156 -0
  183. package/packages/server/dist/wake-injection.js +136 -0
  184. package/packages/server/dist/whisper-local.d.ts +9 -0
  185. package/packages/server/dist/whisper-local.js +122 -0
  186. package/packages/server/dist/ws-context.d.ts +158 -0
  187. package/packages/server/dist/ws-context.js +1 -0
  188. package/packages/server/dist/ws-protocol.d.ts +423 -0
  189. package/packages/server/dist/ws-protocol.js +251 -0
  190. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  191. package/packages/server/dist/ws-self-mcp.js +28 -0
  192. package/packages/server/dist/ws-triggers.d.ts +29 -0
  193. package/packages/server/dist/ws-triggers.js +261 -0
  194. package/packages/server/dist/ws-turn.d.ts +86 -0
  195. package/packages/server/dist/ws-turn.js +751 -0
  196. package/packages/server/dist/ws.d.ts +62 -0
  197. package/packages/server/dist/ws.js +1024 -0
  198. package/packages/server/package.json +6 -0
  199. package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
  200. package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
  201. package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
  202. package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
  203. package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
  204. package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
  205. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
  206. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  207. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
  208. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  209. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
  210. package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
  211. package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
  212. package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
  213. package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
  214. package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
  215. package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
  216. package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
  217. package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
  218. package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
  219. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
  220. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
  221. package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
  222. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
  223. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  224. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  225. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  226. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  227. package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
  228. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  229. package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
  230. package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
  231. package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
  232. package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
  233. package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
  234. package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
  235. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
  236. package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
  237. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
  238. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
  239. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  240. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  241. package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
  242. package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
  243. package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
  244. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  245. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  246. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
  247. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  248. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  249. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
  250. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  251. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
  252. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  253. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  254. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  255. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  256. package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
  257. package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
  258. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
  259. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  260. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  261. package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
  262. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  263. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
  264. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  265. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
  266. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  267. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  268. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
  269. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  270. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
  271. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  272. package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
  273. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
  274. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  275. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
  276. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  277. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
  278. package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
  279. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
  280. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  281. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
  282. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  283. package/packages/web/dist/favicon.ico +0 -0
  284. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  285. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  286. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  287. package/packages/web/dist/icons/favicon-16.png +0 -0
  288. package/packages/web/dist/icons/favicon-32.png +0 -0
  289. package/packages/web/dist/icons/icon-192.png +0 -0
  290. package/packages/web/dist/icons/icon-512.png +0 -0
  291. package/packages/web/dist/icons/maskable-512.png +0 -0
  292. package/packages/web/dist/index.html +56 -0
  293. package/packages/web/dist/manifest.webmanifest +32 -0
  294. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,251 @@
1
+ function num(v) {
2
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
3
+ }
4
+ /**
5
+ * Defensively extract per-turn usage + model from a raw SDK message. The SDK's
6
+ * `message` is typed `unknown` in core, and the CLI/SDK runtimes shape usage
7
+ * slightly differently, so every field access is guarded.
8
+ *
9
+ * - assistant message: `m.message.usage` (Anthropic usage block) + `m.message.model`.
10
+ * - result message: top-level `m.usage` (same field names). Flagged `fromResult`
11
+ * because that block is a cumulative per-turn total, not a snapshot (#398).
12
+ *
13
+ * Returns `{ usage: null, model: null, fromResult: false }` when neither is present.
14
+ */
15
+ export function extractUsage(m) {
16
+ const raw = m;
17
+ const fromResult = raw.type === "result";
18
+ // Locate the usage block + model from whichever shape this message carries.
19
+ let usageBlock;
20
+ let model = null;
21
+ const inner = raw.message && typeof raw.message === "object"
22
+ ? raw.message
23
+ : undefined;
24
+ if (inner) {
25
+ if (inner.usage !== undefined)
26
+ usageBlock = inner.usage;
27
+ if (typeof inner.model === "string")
28
+ model = inner.model;
29
+ }
30
+ // Result messages (and some shapes) carry usage at the top level.
31
+ if (usageBlock === undefined && raw.usage !== undefined)
32
+ usageBlock = raw.usage;
33
+ if (usageBlock === undefined || usageBlock === null || typeof usageBlock !== "object") {
34
+ return { usage: null, model, fromResult };
35
+ }
36
+ const u = usageBlock;
37
+ const usage = {
38
+ inputTokens: num(u.input_tokens),
39
+ outputTokens: num(u.output_tokens),
40
+ cacheCreationTokens: num(u.cache_creation_input_tokens),
41
+ cacheReadTokens: num(u.cache_read_input_tokens),
42
+ };
43
+ // A usage block of all-zeros (e.g. a stub) is not informative; treat as none.
44
+ const anyTokens = usage.inputTokens || usage.outputTokens || usage.cacheCreationTokens || usage.cacheReadTokens;
45
+ return { usage: anyTokens ? usage : null, model, fromResult };
46
+ }
47
+ /** The `<synthetic>` model marker CC stamps on placeholder assistant turns. */
48
+ const SYNTHETIC_MODEL = "<synthetic>";
49
+ /**
50
+ * Bare synthetic placeholders that carry no output worth surfacing — e.g. the
51
+ * "No response requested." turn CC injects after a `/compact` continuation. Only a
52
+ * synthetic message with *substantive* text is a genuine local-command result.
53
+ */
54
+ const SYNTHETIC_PLACEHOLDERS = new Set(["No response requested."]);
55
+ /**
56
+ * The rendered output of a client-local slash command (`/context`, `/usage`, …) to
57
+ * surface as an assistant note, or null when this SDK message isn't one (issue #158).
58
+ *
59
+ * The interactive TUI renders these locally; a non-interactive SDK session (Paddock's
60
+ * path) instead surfaces the output as EITHER a `type:"system"` / `local_command`
61
+ * entry (its `content` wraps the output in `<local-command-stdout>…`) or a
62
+ * `model:"<synthetic>"` assistant placeholder carrying the text. @herdctl/chat's
63
+ * translator drops both (synthetic messages are skipped; system entries aren't text),
64
+ * so the live command turn would otherwise read as a silent no-op. We recover the text
65
+ * and emit it as an assistant note (mirroring the `compact_boundary` special-case),
66
+ * consistent with the history-path recovery in `localcommand.ts`. Bare placeholders
67
+ * (e.g. `/compact`'s "No response requested.") yield null so they stay quiet. Paddock's
68
+ * own context ring + cost meter remain the primary usage view; this just stops the
69
+ * output vanishing.
70
+ */
71
+ export function extractLocalCommandOutput(m) {
72
+ const raw = m;
73
+ // Disk/canonical form: a `system` / `local_command` entry wrapping the stdout.
74
+ if (raw.type === "system" && raw.subtype === "local_command") {
75
+ const content = typeof raw.content === "string" ? raw.content : "";
76
+ const inner = /<local-command-stdout>([\s\S]*)<\/local-command-stdout>/.exec(content)?.[1];
77
+ const text = (inner ?? "").trim();
78
+ return text.length > 0 ? text : null;
79
+ }
80
+ // Live/stream form: a synthetic placeholder assistant message carrying the text.
81
+ if (raw.type !== "assistant")
82
+ return null;
83
+ const inner = raw.message && typeof raw.message === "object"
84
+ ? raw.message
85
+ : undefined;
86
+ if (!inner || inner.model !== SYNTHETIC_MODEL)
87
+ return null;
88
+ let text = "";
89
+ if (typeof inner.content === "string")
90
+ text = inner.content;
91
+ else if (Array.isArray(inner.content)) {
92
+ for (const block of inner.content) {
93
+ const b = block;
94
+ if (b?.type === "text" && typeof b.text === "string")
95
+ text += b.text;
96
+ }
97
+ }
98
+ text = text.trim();
99
+ if (text.length === 0 || SYNTHETIC_PLACEHOLDERS.has(text))
100
+ return null;
101
+ return text;
102
+ }
103
+ /**
104
+ * The context snapshot a usage block implies: the tokens resident in the model's
105
+ * context window for this turn (fresh input + cache reads + cache creation).
106
+ */
107
+ function contextTokensOf(u) {
108
+ return u.inputTokens + u.cacheReadTokens + u.cacheCreationTokens;
109
+ }
110
+ /**
111
+ * Merge two context-snapshot usage blocks, keeping whichever best reflects the
112
+ * context window (issue #165). Only assistant / partial-assistant blocks are ever
113
+ * passed here — the terminal `result` block is folded separately (see
114
+ * {@link foldTurnUsage}), because its usage is cumulative, not a snapshot (#398).
115
+ *
116
+ * Assistant usage grows monotonically through a turn (each round re-reads the
117
+ * cached prefix), so "last assistant" == "max assistant". We still keep the MAX
118
+ * `contextTokens` (input + cacheRead + cacheCreation) defensively: an odd
119
+ * cache-less block can never LOWER the snapshot. `outputTokens` keeps the max
120
+ * seen.
121
+ */
122
+ export function pickTurnUsage(prev, next) {
123
+ if (!prev)
124
+ return next;
125
+ const chosen = contextTokensOf(next) >= contextTokensOf(prev) ? next : prev;
126
+ return {
127
+ ...chosen,
128
+ outputTokens: Math.max(prev.outputTokens, next.outputTokens),
129
+ };
130
+ }
131
+ /** A fresh, empty per-turn usage accumulator. */
132
+ export function initTurnUsage() {
133
+ return { snapshot: null, cumulative: null, model: null };
134
+ }
135
+ /**
136
+ * Fold one raw SDK message into the running usage state. Mirrors exactly what the
137
+ * ws.ts stream loop does, so tests can drive it the same way. Assistant usage
138
+ * accumulates into the context snapshot; the terminal `result` block is stashed
139
+ * separately as the turn's cumulative total (#398).
140
+ */
141
+ export function foldTurnUsage(state, m) {
142
+ const ex = extractUsage(m);
143
+ if (ex.usage) {
144
+ if (ex.fromResult)
145
+ state.cumulative = ex.usage;
146
+ else
147
+ state.snapshot = pickTurnUsage(state.snapshot, ex.usage);
148
+ }
149
+ if (ex.model)
150
+ state.model = ex.model;
151
+ return state;
152
+ }
153
+ /**
154
+ * Resolve the final per-turn usage emitted on `chat:complete`. `contextTokens`
155
+ * always derives from the snapshot (the true window). The cumulative result block
156
+ * is used only to lift `outputTokens` to the turn's final total, and as a
157
+ * last-resort fallback for context if NO assistant usage was seen this turn (some
158
+ * runtimes emit usage only on the result — there cumulative == the single call).
159
+ */
160
+ export function resolveTurnUsage(state) {
161
+ const base = state.snapshot ?? state.cumulative;
162
+ if (!base)
163
+ return null;
164
+ return {
165
+ ...base,
166
+ outputTokens: Math.max(base.outputTokens, state.cumulative?.outputTokens ?? 0),
167
+ };
168
+ }
169
+ export function isClientMessage(data) {
170
+ if (typeof data !== "object" || data === null)
171
+ return false;
172
+ const m = data;
173
+ if (m.type === "ping")
174
+ return true;
175
+ if (m.type === "chat:cancel") {
176
+ const p = m.payload;
177
+ return !!p && typeof p.jobId === "string";
178
+ }
179
+ if (m.type === "chat:send") {
180
+ const p = m.payload;
181
+ if (!p || typeof p.message !== "string")
182
+ return false;
183
+ const slug = p.projectSlug;
184
+ if (typeof slug !== "string")
185
+ return false;
186
+ if (p.preloadContext !== undefined && typeof p.preloadContext !== "boolean")
187
+ return false;
188
+ if (p.model !== undefined && typeof p.model !== "string")
189
+ return false;
190
+ if (p.attachments !== undefined) {
191
+ if (!Array.isArray(p.attachments))
192
+ return false;
193
+ for (const a of p.attachments) {
194
+ if (!a || typeof a !== "object")
195
+ return false;
196
+ const ao = a;
197
+ if (typeof ao.id !== "string" || typeof ao.filename !== "string")
198
+ return false;
199
+ if (ao.kind !== undefined && typeof ao.kind !== "string")
200
+ return false;
201
+ }
202
+ }
203
+ return p.sessionId === undefined || p.sessionId === null || typeof p.sessionId === "string";
204
+ }
205
+ if (m.type === "chat:command") {
206
+ const p = m.payload;
207
+ if (!p || typeof p.command !== "string" || p.command.length === 0)
208
+ return false;
209
+ const slug = p.projectSlug;
210
+ if (typeof slug !== "string")
211
+ return false;
212
+ return p.sessionId === undefined || p.sessionId === null || typeof p.sessionId === "string";
213
+ }
214
+ if (m.type === "chat:subscribe") {
215
+ const p = m.payload;
216
+ if (!p || typeof p.sessionId !== "string" || p.sessionId.length === 0)
217
+ return false;
218
+ if (p.wantReplay !== undefined && typeof p.wantReplay !== "boolean")
219
+ return false;
220
+ if (p.lastSeq !== undefined && typeof p.lastSeq !== "number")
221
+ return false;
222
+ return true;
223
+ }
224
+ if (m.type === "chat:continue") {
225
+ const p = m.payload;
226
+ if (!p || typeof p.sessionId !== "string" || p.sessionId.length === 0)
227
+ return false;
228
+ const slug = p.projectSlug;
229
+ return typeof slug === "string";
230
+ }
231
+ if (m.type === "chat:set_queue") {
232
+ // NOTE: this case was missing until #245 — every chat:set_queue was rejected
233
+ // as "Unknown message", so the server-side queue (#197) never persisted a
234
+ // thing. That's the deeper reason a queued message could strand: the backstop
235
+ // was never armed. Validate leniently (all payload fields optional bar slug).
236
+ const p = m.payload;
237
+ if (!p)
238
+ return false;
239
+ const slug = p.projectSlug;
240
+ if (typeof slug !== "string")
241
+ return false;
242
+ if (p.sessionId !== undefined && p.sessionId !== null && typeof p.sessionId !== "string")
243
+ return false;
244
+ if (p.text !== undefined && p.text !== null && typeof p.text !== "string")
245
+ return false;
246
+ if (p.ts !== undefined && p.ts !== null && typeof p.ts !== "number")
247
+ return false;
248
+ return true;
249
+ }
250
+ return false;
251
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Self-management MCP server-def assembly (extracted from ws.ts in #403; the ops
3
+ * construction moved out to management-ops.ts in #312 M1).
4
+ *
5
+ * What remains here is exactly the MCP-TRANSPORT concern: take a caller's built
6
+ * {@link ManagementOps} and turn them into an {@link InjectedMcpServerDef}. The
7
+ * operations themselves — and the policy that guards them — now live in
8
+ * management-ops.ts, so a non-MCP caller (the `/mcp` transport now, REST parity
9
+ * in #465) reaches the same ops without going through this module.
10
+ *
11
+ * `forkKickoffPrompt` is re-exported for its existing importers (ws.ts re-exports
12
+ * it in turn, and its unit test resolves it via ws.js).
13
+ */
14
+ import type { InjectedMcpServerDef } from "@herdctl/core";
15
+ import type { ChatHandlerContext } from "./ws-context.js";
16
+ import type { RunProvenance } from "./run-provenance.js";
17
+ import { type ManagementOps } from "./management-ops.js";
18
+ import { type ManagementPrincipal } from "./management-policy.js";
19
+ export { forkKickoffPrompt } from "./management-ops.js";
20
+ /**
21
+ * Assemble an MCP server def from already-built, already-policed ops. This is
22
+ * the seam the `/mcp` transport uses: authenticate → build ops for the principal
23
+ * → hand them here.
24
+ */
25
+ export declare function selfMcpServerDefForOps(ops: ManagementOps, principal: ManagementPrincipal): InjectedMcpServerDef;
26
+ /**
27
+ * Build the self-management MCP server def (issue #214) bound to one turn's
28
+ * context — the IN-PROCESS keeper path.
29
+ *
30
+ * Runs under {@link INTERNAL_PRINCIPAL}: a keeper is bounded by depth
31
+ * (`maxSpawnDepth`, #278), not by auth+scope, so policy short-circuits to allow
32
+ * and this path behaves exactly as it did before #312.
33
+ *
34
+ * `parentProvenance` is the provenance of the chat these tools run IN — so any
35
+ * child they spawn is `childOf(parentProvenance)` (origin `spawned`, depth+1).
36
+ */
37
+ export declare function buildSelfMcpServerDef(ctx: ChatHandlerContext, params: {
38
+ currentProjectSlug: string;
39
+ currentSessionId: () => string | null;
40
+ parentProvenance: RunProvenance;
41
+ includeWrite: boolean;
42
+ /**
43
+ * Whether to additionally append the Epic T / T3 unified trigger-management
44
+ * tools. Resolved per-project by the caller from the REUSED hooks-MCP gate.
45
+ */
46
+ includeTriggers: boolean;
47
+ /**
48
+ * Whether to expose `create_project` (#467) — its own instance-level gate,
49
+ * separate from the other write tools.
50
+ */
51
+ includeProjects: boolean;
52
+ }): InjectedMcpServerDef;
@@ -0,0 +1,28 @@
1
+ import { selfMcpServerDef } from "./self-mcp.js";
2
+ import { buildManagementOps, enforceManagementPolicy, managementToolFilter, } from "./management-ops.js";
3
+ import { INTERNAL_PRINCIPAL } from "./management-policy.js";
4
+ export { forkKickoffPrompt } from "./management-ops.js";
5
+ /**
6
+ * Assemble an MCP server def from already-built, already-policed ops. This is
7
+ * the seam the `/mcp` transport uses: authenticate → build ops for the principal
8
+ * → hand them here.
9
+ */
10
+ export function selfMcpServerDefForOps(ops, principal) {
11
+ return selfMcpServerDef(ops.read, ops.write, { toolFilter: managementToolFilter(principal) });
12
+ }
13
+ /**
14
+ * Build the self-management MCP server def (issue #214) bound to one turn's
15
+ * context — the IN-PROCESS keeper path.
16
+ *
17
+ * Runs under {@link INTERNAL_PRINCIPAL}: a keeper is bounded by depth
18
+ * (`maxSpawnDepth`, #278), not by auth+scope, so policy short-circuits to allow
19
+ * and this path behaves exactly as it did before #312.
20
+ *
21
+ * `parentProvenance` is the provenance of the chat these tools run IN — so any
22
+ * child they spawn is `childOf(parentProvenance)` (origin `spawned`, depth+1).
23
+ */
24
+ export function buildSelfMcpServerDef(ctx, params) {
25
+ const ops = buildManagementOps(ctx, params);
26
+ const policed = enforceManagementPolicy(ops, INTERNAL_PRINCIPAL);
27
+ return selfMcpServerDefForOps(policed, INTERNAL_PRINCIPAL);
28
+ }
@@ -0,0 +1,29 @@
1
+ import { type TriggerDto, type TriggerEvent } from "./trigger-config.js";
2
+ import type { ChatHandlerDeps, StartAgentTurn } from "./ws-context.js";
3
+ /**
4
+ * The context a fired lifecycle event carries into an EVENT trigger's prompt (Epic T /
5
+ * T1). v1 (`onArchive`) supplies the archived chat's session id so the trigger knows
6
+ * what to act on.
7
+ */
8
+ export interface TriggerEventContext {
9
+ /** The session id of the chat whose lifecycle event fired the trigger. */
10
+ sessionId: string;
11
+ }
12
+ /** The shared trigger-firing surface `makeChatHandler` consumes + returns. */
13
+ export interface TriggerCluster {
14
+ /** Post-turn curation signal (T5): emit `afterTurn` so the sweeper runs once. */
15
+ emitAfterTurn(slug: string, sessionId: string | null): void;
16
+ /** Prepend the OVERVIEW+CHANGELOG preload block for a new chat (issues #1/#188). */
17
+ composePreloadedPrompt(projectSlug: string, baseMessage: string): Promise<string>;
18
+ /** Fire a named trigger on demand (Run-now / run_trigger); resolves its chat id or null. */
19
+ fireTrigger(slug: string, triggerName: string): Promise<string | null>;
20
+ /** Run one fire of a resolved trigger record as a first-class chat on the hub. */
21
+ fireTriggerForProject(project: Awaited<ReturnType<ChatHandlerDeps["projects"]["get"]>>, trigger: TriggerDto, ctx?: TriggerEventContext): Promise<string | null>;
22
+ /** Fire every enabled EVENT trigger matching `event` for a project (after-commit). */
23
+ dispatchEventTriggers(slug: string, event: TriggerEvent, ctx: TriggerEventContext): Promise<void>;
24
+ }
25
+ /**
26
+ * Build the trigger-firing cluster bound to the handler's deps + the shared
27
+ * `startAgentTurn` engine, and wire its schedule/event listeners.
28
+ */
29
+ export declare function makeTriggerCluster(deps: ChatHandlerDeps, startAgentTurn: StartAgentTurn): TriggerCluster;
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Trigger / schedule / event firing cluster, extracted from ws.ts (#403).
3
+ *
4
+ * `makeTriggerCluster(deps, startAgentTurn)` returns the shared firing surface and
5
+ * wires the herdctl schedule handler + the onArchive / afterTurn event listeners.
6
+ * Every trigger type funnels through ONE `startAgentTurn` call (the shared engine,
7
+ * passed in) so a cron / event / manual "Run now" fire is indistinguishable.
8
+ *
9
+ * Also holds two helpers historically co-located with the firing code: the
10
+ * post-turn `emitAfterTurn` curation signal (T5 sweeper) and `composePreloadedPrompt`
11
+ * (the OVERVIEW+CHANGELOG new-chat preload, shared by the human path + self-MCP).
12
+ */
13
+ import { promises as fs } from "node:fs";
14
+ import { isKnownDriveMode } from "./models.js";
15
+ import { keeperAgentName, keeperSlugFromAgent, triggerAgentName, } from "./herdctl.js";
16
+ import { resolveMaxSpawnDepth } from "./spawn-capability.js";
17
+ import { wrapPreload, composePreloadContext } from "./preload.js";
18
+ import { triggerPromptFileAbsPath, triggerRunsOnOwnAgent, isCuratorTrigger, } from "./trigger-config.js";
19
+ /**
20
+ * Build the trigger-firing cluster bound to the handler's deps + the shared
21
+ * `startAgentTurn` engine, and wire its schedule/event listeners.
22
+ */
23
+ export function makeTriggerCluster(deps, startAgentTurn) {
24
+ /** Resolve a project's effective keeper drive mode (override else instance default). */
25
+ function resolveDriveMode(project) {
26
+ return project.driveMode && isKnownDriveMode(project.driveMode)
27
+ ? project.driveMode
28
+ : deps.cfg.driveMode;
29
+ }
30
+ // Drive scheduler-fired chats onto the hub (issue #265 / DD-1, DD-2). herdctl's
31
+ // cron engine fires a project keeper's declared schedule and routes it HERE
32
+ // (setScheduleTriggerHandler) instead of running it headless.
33
+ deps.herdctl.onScheduleTrigger(async (info) => {
34
+ const slug = keeperSlugFromAgent(info.agent.name);
35
+ // Only keeper agents carry Paddock schedules; a non-keeper trigger (there are
36
+ // none today) has nowhere sensible to route, so ignore it rather than guess.
37
+ // `null` means "not a keeper"; `""` is the ROOT workspace's own key, so this
38
+ // must not be a falsy check.
39
+ if (slug === null)
40
+ return;
41
+ const project = await deps.projects.get(slug).catch(() => null);
42
+ if (!project)
43
+ return;
44
+ // Every armed keeper schedule belongs to a SCHEDULE-type trigger (forwarded into
45
+ // the keeper `schedules` block under its trigger name). Resolve + fire it via the
46
+ // single trigger fire path.
47
+ const trig = project.triggers?.[info.scheduleName];
48
+ if (trig && trig.trigger.type === "schedule" && trig.enabled === true) {
49
+ await fireTriggerForProject(project, {
50
+ name: info.scheduleName,
51
+ agentName: triggerAgentName(slug, info.scheduleName),
52
+ ...trig,
53
+ });
54
+ }
55
+ // A fired keeper schedule with no matching enabled SCHEDULE trigger is ignored:
56
+ // triggers are the only thing forwarded into the keeper's `schedules` block.
57
+ });
58
+ // --- unified triggers (Epic T / T1) ------------------------------------
59
+ /**
60
+ * Resolve the prompt a fired trigger should run. A trigger's `promptFile`
61
+ * (Paddock-only, `.paddock/triggers/*.md`, git-tracked + keeper-editable) is read
62
+ * FRESH here at fire time — so an edit takes effect on the very next fire with no
63
+ * agent re-register — and falls back to the inline `run.prompt` when there's no file
64
+ * or it can't be read. For an EVENT trigger, a short machine preamble naming the
65
+ * event + archived chat is prepended (so the trigger knows WHAT to act on); a
66
+ * schedule trigger gets no preamble. Mirrors {@link resolveSchedulePrompt} /
67
+ * {@link resolveHookPrompt}.
68
+ */
69
+ async function resolveTriggerPrompt(project, trigger, ctx) {
70
+ let body = typeof trigger.run.prompt === "string" ? trigger.run.prompt : "";
71
+ if (trigger.run.promptFile) {
72
+ const abs = triggerPromptFileAbsPath(project.workingDir, trigger.run.promptFile);
73
+ if (abs) {
74
+ const content = await fs.readFile(abs, "utf8").catch(() => null);
75
+ if (content !== null)
76
+ body = content;
77
+ }
78
+ }
79
+ if (trigger.trigger.type === "event" && ctx) {
80
+ const preamble = `A \`${trigger.trigger.on}\` event trigger fired for project \`${project.slug}\`: ` +
81
+ `chat \`${ctx.sessionId}\` was just archived.\n\n`;
82
+ return preamble + body;
83
+ }
84
+ return body;
85
+ }
86
+ /**
87
+ * Run one fire of a trigger as a first-class chat on the hub — the ONE fire path for
88
+ * every trigger type (Epic T), replacing the separate schedule + hook fire paths with
89
+ * a single `startAgentTurn` call. Whether the fired turn runs on the trigger's OWN
90
+ * scoped `trigger-<slug>-<name>` agent (tool config = `run.tools`) or on the keeper is
91
+ * decided by {@link triggerRunsOnOwnAgent}: an EVENT trigger always runs scoped; a
92
+ * SCHEDULE trigger runs scoped ONLY when it declares a non-empty `run.tools` allow-list
93
+ * (T2 — #307), otherwise it runs as the keeper with the project-agent default toolset
94
+ * (pre-T2 behaviour, unchanged). `run.maxSpawnDepth` gates this fire's self-MCP spawn
95
+ * capability regardless of which agent runs it.
96
+ *
97
+ * `run.session` drives new-vs-accrete: `"new"` → a FRESH chat every fire
98
+ * (`resume: null`); `"resume"` → resume the trigger's ONE owned session (recorded on
99
+ * first fire in the {@link TriggerSessionStore}, rebound after a restart) so a
100
+ * "manager" accretes a single transcript. A stale owned id (its transcript deleted)
101
+ * is forgotten so the next fire re-creates one. FIRE-AND-FORGET: a rejection (the
102
+ * turn never produced a session id — its own failure frame already emitted) is
103
+ * swallowed so a transient failure never wedges the trigger. Resolves the
104
+ * created/resumed session id, or `null`.
105
+ */
106
+ async function fireTriggerForProject(project, trigger, ctx) {
107
+ const slug = project.slug;
108
+ const isSchedule = trigger.trigger.type === "schedule";
109
+ // T2: a scoped trigger (every event; a schedule with a `run.tools` allow-list) runs
110
+ // on its OWN `trigger-<slug>-<name>` agent so herdctl enforces its capability; an
111
+ // unscoped schedule runs as the keeper (project-agent default toolset, unchanged).
112
+ const onOwnAgent = triggerRunsOnOwnAgent(trigger);
113
+ const agentName = onOwnAgent ? triggerAgentName(slug, trigger.name) : keeperAgentName(slug);
114
+ const prompt = await resolveTriggerPrompt(project, trigger, ctx);
115
+ // run.session: "resume" accretes into an owned session; "new" starts fresh.
116
+ let resume = null;
117
+ if (trigger.run.session === "resume" && deps.triggerSessions) {
118
+ const owned = await deps.triggerSessions.get(slug, trigger.name).catch(() => undefined);
119
+ if (owned && (await deps.herdctl.sessionExists(project, owned).catch(() => false))) {
120
+ resume = owned;
121
+ }
122
+ else if (owned) {
123
+ await deps.triggerSessions.clear(slug, trigger.name).catch(() => undefined);
124
+ }
125
+ }
126
+ try {
127
+ const sessionId = await startAgentTurn({
128
+ projectSlug: slug,
129
+ agentName,
130
+ workingDir: project.workingDir,
131
+ resume,
132
+ prompt,
133
+ driveMode: resolveDriveMode(project),
134
+ fallbackModel: trigger.run.model ?? project.model,
135
+ // Provenance (A1/#261): a schedule fire is a root `scheduled` trigger; an event
136
+ // trigger reuses the `hook` origin (its E1 badge surface) — both depth 0.
137
+ origin: isSchedule ? "scheduled" : "hook",
138
+ depth: 0,
139
+ // A per-trigger `run.maxSpawnDepth` (design §2.3, T2) gates this fire's self-MCP
140
+ // spawn capability; it wins over the project override, which wins over the
141
+ // instance default (reuses B1's resolver).
142
+ maxSpawnDepth: resolveMaxSpawnDepth(trigger.run.maxSpawnDepth ?? project.maxSpawnDepth, deps.cfg.maxSpawnDepth),
143
+ // Attribute the injected kickoff turn to the trigger that fired it (#290).
144
+ sender: {
145
+ kind: isSchedule ? "schedule" : "hook",
146
+ name: trigger.name,
147
+ project: slug,
148
+ },
149
+ });
150
+ // First fire of an accreting trigger: remember the chat it created so the next
151
+ // fire resumes THIS transcript (a resume already had an id).
152
+ if (trigger.run.session === "resume" && !resume && deps.triggerSessions) {
153
+ await deps.triggerSessions.set(slug, trigger.name, sessionId).catch(() => undefined);
154
+ }
155
+ return sessionId;
156
+ }
157
+ catch {
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Resolve a project's ENABLED event triggers for `event` and fire each (after-commit,
163
+ * non-blocking — after-commit, non-blocking). Concurrent +
164
+ * independent; one trigger's failure never affects another. No-op when the trigger
165
+ * system isn't wired ({@link makeChatHandler} dep `triggers` absent) or the project
166
+ * has no matching enabled event trigger.
167
+ */
168
+ async function dispatchEventTriggers(slug, event, ctx) {
169
+ if (!deps.triggers)
170
+ return;
171
+ const project = await deps.projects.get(slug).catch(() => null);
172
+ if (!project)
173
+ return;
174
+ const matching = await deps.triggers.enabledForEvent(slug, event).catch(() => []);
175
+ await Promise.all(matching.map((trigger) => fireTriggerForProject(project, trigger, ctx)));
176
+ }
177
+ // Dispatch enabled EVENT triggers on the SAME lifecycle events hooks fire on — the
178
+ // event-bus supports multiple listeners, so this rides alongside the hook dispatcher
179
+ // (they read disjoint config blocks). onArchive is the wired event; afterTurn is
180
+ // reserved for the sweeper fold-in (T5) and not emitted yet.
181
+ deps.events?.on("onArchive", (payload) => {
182
+ void dispatchEventTriggers(payload.slug, "onArchive", { sessionId: payload.sessionId });
183
+ });
184
+ /**
185
+ * Signal a completed turn's post-turn CURATION (Epic T / T5) — the sweeper, folded in
186
+ * as the default `curate-overview` (event/afterTurn) trigger. Emits the `afterTurn`
187
+ * lifecycle event so the curator dispatches EXACTLY ONCE per turn (its enabled gate +
188
+ * per-project prompt extension resolved inside SweepService). Fires for every
189
+ * workspace, the root included.
190
+ * Falls back to a direct `sweep.enqueue` when the event bus isn't wired (older
191
+ * callers / tests), so behaviour is identical with or without the bus. Called from
192
+ * every post-turn commit site (a human chat turn, a session-mode wake, and every
193
+ * server-initiated `startAgentTurn`) — the ONE place the sweeper is now triggered.
194
+ */
195
+ function emitAfterTurn(slug, sessionId) {
196
+ if (deps.events)
197
+ deps.events.emit("afterTurn", { slug, sessionId });
198
+ else
199
+ deps.sweep?.enqueue(slug);
200
+ }
201
+ // The folded-in sweeper (T5): `afterTurn` drives the default post-turn curator. Unlike
202
+ // `onArchive`, afterTurn is NOT fanned out to generic `trigger-<slug>-<name>` agents —
203
+ // the curator is tool-less and executed by SweepService (returns marked text, Paddock
204
+ // writes OVERVIEW.md/CHANGELOG.md). So this is the SOLE afterTurn consumer, which is
205
+ // what guarantees the sweeper runs exactly once per turn (no double-curation).
206
+ deps.events?.on("afterTurn", (payload) => {
207
+ deps.sweep?.enqueue(payload.slug);
208
+ });
209
+ /**
210
+ * Fire a TRIGGER now (Epic T / T1), reused by the "Run now" REST route + `run_trigger`
211
+ * self-MCP verb (#327) and shared with the cron path below. Resolves the live project +
212
+ * its trigger record and fires via {@link fireTriggerForProject} — through the SAME hub
213
+ * path a cron/event fire uses, so a manual run is indistinguishable from an automatic
214
+ * one. Fires ANY trigger type on demand (a schedule, an event trigger, or a reserved
215
+ * webhook trigger you want to smoke-test before its ingress lands) regardless of its
216
+ * `enabled` flag — a manual run is a deliberate act (mirrors the schedule DD-1 rule).
217
+ * Returns the started chat's session id, or `null` if the project/trigger is gone or
218
+ * the turn never produced a session.
219
+ */
220
+ async function fireTrigger(slug, triggerName) {
221
+ const project = await deps.projects.get(slug).catch(() => null);
222
+ if (!project)
223
+ return null;
224
+ const rec = project.triggers?.[triggerName];
225
+ if (!rec)
226
+ return null;
227
+ // The post-turn CURATOR (any `event`/`afterTurn` trigger — the folded-in sweeper, T5)
228
+ // is NOT fireable on the generic path: it registers no scoped `trigger-<slug>-<name>`
229
+ // agent and runs via SweepService on `afterTurn`. Refuse rather than firing a turn on
230
+ // a non-existent agent (defence-in-depth — the REST route + run_trigger MCP reject it
231
+ // up front with a clear message; this guards any other caller).
232
+ if (isCuratorTrigger(rec))
233
+ return null;
234
+ return fireTriggerForProject(project, { name: triggerName, agentName: triggerAgentName(slug, triggerName), ...rec });
235
+ }
236
+ /**
237
+ * Compose the OVERVIEW.md + CHANGELOG.md preload block onto `baseMessage` for
238
+ * a NEW chat (issues #1/#188), shared (C2 / #264) by the human New-Chat path
239
+ * and the self-MCP `create_chat` spawn path so both inject the SAME context.
240
+ * Injects only when the project has an OVERVIEW.md (the signal that a sweep
241
+ * has curated real state); when it fires it prepends BOTH the overview
242
+ * (current state) AND the CHANGELOG.md (cross-session history), matching the
243
+ * UI checkbox. Returns `baseMessage` unchanged when there's no overview yet.
244
+ */
245
+ async function composePreloadedPrompt(projectSlug, baseMessage) {
246
+ const overview = await deps.projects.readOverview(projectSlug).catch(() => "");
247
+ if (overview.trim().length === 0)
248
+ return baseMessage;
249
+ const changelog = await deps.projects.readChangelog(projectSlug).catch(() => "");
250
+ // Single-sourced wrapper (see preload.ts) so the chat-list can strip it back
251
+ // off for display (issue #62).
252
+ return wrapPreload(composePreloadContext(overview, changelog), baseMessage);
253
+ }
254
+ return {
255
+ emitAfterTurn,
256
+ composePreloadedPrompt,
257
+ fireTrigger,
258
+ fireTriggerForProject,
259
+ dispatchEventTriggers,
260
+ };
261
+ }