@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,98 @@
1
+ import type { PaddockConfig } from "./config.js";
2
+ import type { Project } from "./projects.js";
3
+ import { type PaddockTrigger } from "./trigger-config.js";
4
+ /**
5
+ * A project's keeper agent config. Inherits the fleet `defaults` (runtime,
6
+ * max_turns, permission_mode, allowed/denied tools) via addAgent's deep-merge;
7
+ * only project-specific fields are set here.
8
+ *
9
+ * Model resolution: `modelOverride` (a per-chat override) wins, else the
10
+ * project's persisted `model`, else the keeper default (Opus).
11
+ *
12
+ * System prompt: by default (`nativeSystemPrompt`, issue #176) we set NO
13
+ * `system_prompt`, so Claude Code's full default coding prompt applies — the SDK
14
+ * runtime falls back to the `claude_code` preset and the CLI runtime passes no
15
+ * `--system-prompt`, which amount to the same thing. It applies together with
16
+ * the project's CLAUDE.md hierarchy — the canonical instance-wide
17
+ * `<projectsRoot>/CLAUDE.md`
18
+ * (auto-loaded via the cwd walk-up, since a project dir is a child of the
19
+ * projects root) plus a per-project CLAUDE.md. This is its own decision (issue
20
+ * #176): an instance with no CLAUDE.md files can opt back into the terse replace
21
+ * prompt below with `PADDOCK_NATIVE_PROMPT=false`.
22
+ *
23
+ * This is the reading #512 settled on, and the reason it is `projectsRoot` and
24
+ * not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
25
+ * version-controlled. Since #516 the ROOT project reaches it the same way —
26
+ * its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
27
+ */
28
+ export declare function buildAgentConfig(cfg: PaddockConfig, project: Project, modelOverride?: string): Record<string, unknown> & {
29
+ name: string;
30
+ };
31
+ /**
32
+ * The sweeper's working directory: a dedicated per-project dir under the data
33
+ * root, deliberately NOT the project dir (issue #548).
34
+ *
35
+ * A CLI agent's working directory is what Claude Code encodes into its
36
+ * transcript path, so two agents sharing a cwd share ONE session directory. The
37
+ * sweeper's cwd used to be `project.dir`, which for a notebook project equals the
38
+ * keeper's `project.workingDir` — so keeper and sweeper wrote their transcripts
39
+ * into the same `.chats/`. herdctl identifies a freshly-spawned session by
40
+ * set-difference against a pre-spawn directory snapshot, which is immune to a
41
+ * co-located agent *appending* to its own session but NOT to one *creating* a new
42
+ * file: whichever brand-new `*.jsonl` appears first is claimed as "ours". A sweep
43
+ * is scheduled after every keeper turn, so the sweeper's spawn raced the next
44
+ * keeper turn and a user's chat could be bound to the sweeper's hidden curation
45
+ * transcript (wrong reply streamed, wrong transcript resumed, chat missing from
46
+ * the project's list).
47
+ *
48
+ * The sweeper is TOOL-LESS, so its cwd is inert — nothing about curation depends
49
+ * on it (SweepService reads the project's files itself and inlines them in the
50
+ * prompt, then writes the results itself). Giving it its own directory removes
51
+ * the shared directory, and with it the whole class of collisions, structurally.
52
+ *
53
+ * Kept OUTSIDE `projectsRoot` on purpose: core's discovery unions every
54
+ * `~/.claude/projects/*` bucket whose decoded path is a strict *descendant* of an
55
+ * agent's cwd, and the root workspace's cwd IS `projectsRoot`.
56
+ */
57
+ export declare function sweeperWorkingDir(cfg: PaddockConfig, slug: string): string;
58
+ /**
59
+ * A project's sweeper (curator) agent config. TOOL-LESS: the sweeper has NO
60
+ * tools (`allowed_tools: []`) — it never reads or writes files. Instead it
61
+ * RETURNS the curated content as plain assistant text in marked sections
62
+ * (OVERVIEW / CHANGELOG / optional CLAUDE, issue #177); SweepService parses
63
+ * that text and writes OVERVIEW.md / CHANGELOG.md / CLAUDE.md itself.
64
+ *
65
+ * This is cheaper and far more predictable than letting a Haiku agent drive
66
+ * file edits: no tool-loop turns, no partial writes, no permission_mode /
67
+ * denied_tools to reason about (all irrelevant with zero tools).
68
+ */
69
+ export declare function buildSweeperConfig(cfg: PaddockConfig, project: Project): Record<string, unknown> & {
70
+ name: string;
71
+ };
72
+ /**
73
+ * A trigger's scoped herdctl agent config (Epic T) — the unified successor to
74
+ * {@link hookAgentConfig}. A trigger that {@link triggerRunsOnOwnAgent} (every event
75
+ * trigger; a schedule trigger with a non-empty `run.tools` allow-list — T2) is
76
+ * registered as its OWN agent `trigger-<slug>-<name>` whose tool config
77
+ * (`allowed_tools`/`permission_mode`/`model`/`max_turns`, projected by
78
+ * {@link triggerToAgentToolConfig} from the trigger's `run`) IS its capability set.
79
+ * Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
80
+ * the keeper does). A tool-less trigger gets `allowed_tools: []` and can only return text.
81
+ */
82
+ export declare function buildTriggerConfig(cfg: PaddockConfig, project: Project, triggerName: string, trigger: PaddockTrigger): Record<string, unknown> & {
83
+ name: string;
84
+ };
85
+ /**
86
+ * Write the minimal herdctl.yaml the FleetManager boots from: a fleet block
87
+ * plus the fleet-wide `defaults` (deep-merged into agents added at runtime),
88
+ * and ZERO agents. All agents are registered programmatically via
89
+ * `fleet.addAgent(...)` in init() — paddock no longer generates per-agent
90
+ * yaml files or calls `reload()`.
91
+ *
92
+ * The `fleet` block is strict (name/description only). `defaults` are deep-
93
+ * merged into each agent by addAgent (mergeDefaults defaults to true), so agents
94
+ * inherit model/max_turns/permission_mode/allowed+denied_tools from here. NOT
95
+ * `runtime`: core's `DefaultsSchema` has no such key, so it is stripped at parse
96
+ * and every agent sets its own.
97
+ */
98
+ export declare function ensureConfigFile(cfg: PaddockConfig): Promise<void>;
@@ -0,0 +1,331 @@
1
+ /**
2
+ * herdctl-agent-config — the pure builders that project a Paddock `Project` (+
3
+ * config) into the herdctl agent-config objects `FleetManager.addAgent` accepts,
4
+ * plus {@link ensureConfigFile} which writes the minimal boot herdctl.yaml.
5
+ *
6
+ * Split out of {@link HerdctlService} (issue #403): every function here reads
7
+ * only its arguments (the `PaddockConfig` bag + the project/trigger) and the
8
+ * pure constants from {@link ./herdctl-agent-names.js} — no `fleet`/live-session
9
+ * state — so they are independently readable and testable. {@link HerdctlService}
10
+ * keeps thin private wrappers that delegate here (so `this.cfg` stays the single
11
+ * source of config and the existing private-method test seams are unchanged).
12
+ */
13
+ import { promises as fs } from "node:fs";
14
+ import path from "node:path";
15
+ import YAML from "yaml";
16
+ import { DEFAULT_MODEL, SWEEPER_DEFAULT_MODEL, DEFAULT_PERMISSION_MODE, DEFAULT_MAX_TURNS, } from "./models.js";
17
+ import { triggerToAgentToolConfig, triggersToHerdctlSchedules, curatorTriggerOf, } from "./trigger-config.js";
18
+ import { keeperAgentName, sweeperAgentName, triggerAgentName, browserMcpServers, BROWSER_MCP_TOOL, DENIED_TOOLS, KEEPER_MAX_CONCURRENT, KEEPER_SESSION_TIMEOUT, } from "./herdctl-agent-names.js";
19
+ /**
20
+ * A project's keeper agent config. Inherits the fleet `defaults` (runtime,
21
+ * max_turns, permission_mode, allowed/denied tools) via addAgent's deep-merge;
22
+ * only project-specific fields are set here.
23
+ *
24
+ * Model resolution: `modelOverride` (a per-chat override) wins, else the
25
+ * project's persisted `model`, else the keeper default (Opus).
26
+ *
27
+ * System prompt: by default (`nativeSystemPrompt`, issue #176) we set NO
28
+ * `system_prompt`, so Claude Code's full default coding prompt applies — the SDK
29
+ * runtime falls back to the `claude_code` preset and the CLI runtime passes no
30
+ * `--system-prompt`, which amount to the same thing. It applies together with
31
+ * the project's CLAUDE.md hierarchy — the canonical instance-wide
32
+ * `<projectsRoot>/CLAUDE.md`
33
+ * (auto-loaded via the cwd walk-up, since a project dir is a child of the
34
+ * projects root) plus a per-project CLAUDE.md. This is its own decision (issue
35
+ * #176): an instance with no CLAUDE.md files can opt back into the terse replace
36
+ * prompt below with `PADDOCK_NATIVE_PROMPT=false`.
37
+ *
38
+ * This is the reading #512 settled on, and the reason it is `projectsRoot` and
39
+ * not `<dataDir>`: `projectsRoot` IS the instance's backing repo, so the file is
40
+ * version-controlled. Since #516 the ROOT project reaches it the same way —
41
+ * its cwd IS `projectsRoot`, so the walk-up needs no special-casing.
42
+ */
43
+ export function buildAgentConfig(cfg, project, modelOverride) {
44
+ const config = {
45
+ name: keeperAgentName(project.slug),
46
+ description: project.summary || `Claude Code agent for project ${project.name}.`,
47
+ // Repo-backed projects (issue #187): the keeper runs INSIDE the cloned
48
+ // checkout (project.workingDir), so the repo's own CLAUDE.md + git tooling
49
+ // apply. For a notebook project workingDir === dir, so this is unchanged.
50
+ working_directory: project.workingDir,
51
+ // `runtime` governs the ONE-SHOT `trigger()` path only — it does NOT decide
52
+ // how chats run. `openChatSession` (session drive-mode, the default) hard-codes
53
+ // `new SDKRuntime()` and never reads this field, so a normal chat runs on the
54
+ // Claude Agent SDK regardless. This value bites only when a project is set to
55
+ // `driveMode: batch`, whose turns go through `RuntimeFactory.create`. Set here
56
+ // per-agent because core's `DefaultsSchema` has no `runtime` key: the fleet
57
+ // `defaults.runtime` we write below is stripped when the file is parsed, so
58
+ // there is nothing for addAgent's deep-merge to hand down. Same for the
59
+ // sweeper and trigger copies.
60
+ runtime: "cli",
61
+ model: modelOverride ?? project.model ?? DEFAULT_MODEL,
62
+ // Per-project keeper settings (issue #12). The project DTO always carries
63
+ // concrete values (fleet defaults resolved in projects.ts), so setting
64
+ // them here just overrides the inherited fleet `defaults` per project.
65
+ permission_mode: project.permissionMode ?? DEFAULT_PERMISSION_MODE,
66
+ max_turns: project.maxTurns ?? DEFAULT_MAX_TURNS,
67
+ // Allow parallel chats per project (forks, and just multiple open chats)
68
+ // instead of herdctl's serialize-by-default max_concurrent: 1.
69
+ instances: { max_concurrent: KEEPER_MAX_CONCURRENT },
70
+ // Session retention (Paddock#111): keep an agent-level session alive long
71
+ // enough that a scheduler-fired wake can still resume its transcript. Note
72
+ // Paddock always resumes by EXPLICIT session id, which bypasses this
73
+ // timeout — and the transcript itself is governed by Claude Code's
74
+ // `cleanupPeriodDays` (default 30d, adequate for realistic wake horizons;
75
+ // set out-of-band via .claude/settings.json if longer horizons are needed).
76
+ // So this is defense-in-depth for the fallback-resume path, sized to the
77
+ // reaper's 7-day recurring-wake expiry.
78
+ session: { timeout: KEEPER_SESSION_TIMEOUT },
79
+ default_prompt: "Summarize the current state of this project.",
80
+ };
81
+ // Docker isolation: only set it when the project opts in, so a project that
82
+ // leaves it off keeps inheriting the fleet default (no Docker) unchanged.
83
+ if (project.docker)
84
+ config.docker = { enabled: true };
85
+ // Native by default: omit the replace prompt so the default coding prompt +
86
+ // CLAUDE.md hierarchy apply (issue #176). Only a non-native instance
87
+ // (PADDOCK_NATIVE_PROMPT=false) gets the terse replace prompt.
88
+ if (!cfg.nativeSystemPrompt) {
89
+ config.system_prompt =
90
+ "You are a Claude Code agent for this project directory. " +
91
+ "Honor any CLAUDE.md present. Keep CHANGELOG.md current. " +
92
+ "Create branches for significant changes; never force-push.";
93
+ }
94
+ // Unified triggers (Epic T / T1): SCHEDULE-type triggers are forwarded into the
95
+ // keeper agent's `schedules` block, in herdctl's OWN `ScheduleSchema` shape,
96
+ // UNMOLESTED — herdctl's cron engine reads `agent.schedules` live every tick, so
97
+ // declaring them here arms them with no translation. The Paddock-only `promptFile`
98
+ // is stripped (the schedule-trigger handler resolves it at fire time). Event/webhook
99
+ // triggers are excluded by triggersToHerdctlSchedules. Only set the key when
100
+ // non-empty so a trigger-less project stays byte-identical to before.
101
+ const schedules = triggersToHerdctlSchedules(project.triggers);
102
+ if (schedules)
103
+ config.schedules = schedules;
104
+ // Browser MCP (headless Chromium) when enabled for this box; `mcp__playwright__*`
105
+ // is already on the inherited defaults.allowed_tools.
106
+ const browser = browserMcpServers(cfg.browserMcp);
107
+ if (browser)
108
+ config.mcp_servers = browser;
109
+ return config;
110
+ }
111
+ /**
112
+ * The sweeper's working directory: a dedicated per-project dir under the data
113
+ * root, deliberately NOT the project dir (issue #548).
114
+ *
115
+ * A CLI agent's working directory is what Claude Code encodes into its
116
+ * transcript path, so two agents sharing a cwd share ONE session directory. The
117
+ * sweeper's cwd used to be `project.dir`, which for a notebook project equals the
118
+ * keeper's `project.workingDir` — so keeper and sweeper wrote their transcripts
119
+ * into the same `.chats/`. herdctl identifies a freshly-spawned session by
120
+ * set-difference against a pre-spawn directory snapshot, which is immune to a
121
+ * co-located agent *appending* to its own session but NOT to one *creating* a new
122
+ * file: whichever brand-new `*.jsonl` appears first is claimed as "ours". A sweep
123
+ * is scheduled after every keeper turn, so the sweeper's spawn raced the next
124
+ * keeper turn and a user's chat could be bound to the sweeper's hidden curation
125
+ * transcript (wrong reply streamed, wrong transcript resumed, chat missing from
126
+ * the project's list).
127
+ *
128
+ * The sweeper is TOOL-LESS, so its cwd is inert — nothing about curation depends
129
+ * on it (SweepService reads the project's files itself and inlines them in the
130
+ * prompt, then writes the results itself). Giving it its own directory removes
131
+ * the shared directory, and with it the whole class of collisions, structurally.
132
+ *
133
+ * Kept OUTSIDE `projectsRoot` on purpose: core's discovery unions every
134
+ * `~/.claude/projects/*` bucket whose decoded path is a strict *descendant* of an
135
+ * agent's cwd, and the root workspace's cwd IS `projectsRoot`.
136
+ */
137
+ export function sweeperWorkingDir(cfg, slug) {
138
+ // The root workspace's key is "" (see project-paths.ts); reuse the same
139
+ // `_root` spelling the agent namespace uses so every sweeper dir is non-empty.
140
+ // Any other slug is SLUG_RE-validated upstream, so it holds no separators.
141
+ return path.join(cfg.dataDir, "sweepers", slug === "" ? "_root" : slug);
142
+ }
143
+ /**
144
+ * A project's sweeper (curator) agent config. TOOL-LESS: the sweeper has NO
145
+ * tools (`allowed_tools: []`) — it never reads or writes files. Instead it
146
+ * RETURNS the curated content as plain assistant text in marked sections
147
+ * (OVERVIEW / CHANGELOG / optional CLAUDE, issue #177); SweepService parses
148
+ * that text and writes OVERVIEW.md / CHANGELOG.md / CLAUDE.md itself.
149
+ *
150
+ * This is cheaper and far more predictable than letting a Haiku agent drive
151
+ * file edits: no tool-loop turns, no partial writes, no permission_mode /
152
+ * denied_tools to reason about (all irrelevant with zero tools).
153
+ */
154
+ export function buildSweeperConfig(cfg, project) {
155
+ // T5: the sweeper IS the default `curate-overview` (event/afterTurn) trigger. When a
156
+ // project declares that trigger with a `run.model`, honor it as the sweeper agent's
157
+ // model (design §2.1 #4). herdctl's per-fire trigger API has no model override, so
158
+ // the per-project `sweeper-<slug>` agent carries it — applied at (re-)registration
159
+ // (boot / `ensureProjectAgent`). Absent ⇒ the cheap curation default, unchanged.
160
+ const curatorModel = curatorTriggerOf(project.triggers)?.run.model;
161
+ return {
162
+ name: sweeperAgentName(project.slug),
163
+ description: `Overview/changelog curator (sweeper) for project ${project.name}.`,
164
+ // NOT project.dir — see sweeperWorkingDir (issue #548).
165
+ working_directory: sweeperWorkingDir(cfg, project.slug),
166
+ // The sweeper only ever runs via the one-shot `trigger()` path, so unlike the
167
+ // keeper's copy this one really does take effect on every sweep: a `claude -p`
168
+ // CLI subprocess. Set here per-agent because core's `DefaultsSchema` has no
169
+ // `runtime` key — the fleet `defaults.runtime` never survives parsing, so
170
+ // there is nothing to inherit (registering via addAgent is not the reason:
171
+ // addAgent does deep-merge the loaded defaults, they just never carry it).
172
+ runtime: "cli",
173
+ model: curatorModel ?? SWEEPER_DEFAULT_MODEL,
174
+ // Tool-less: a handful of turns is plenty since there are no tool loops.
175
+ max_turns: 4,
176
+ // NO tools. The sweeper returns text only; SweepService does the writing.
177
+ allowed_tools: [],
178
+ system_prompt: "You are a concise project curator. You DO NOT use any tools — you only " +
179
+ "return text. From the recent activity, the current OVERVIEW.md, the " +
180
+ "recent CHANGELOG.md, and the current CLAUDE.md provided in the user " +
181
+ "message, produce these three sections wrapped in these literal markers, " +
182
+ "and NOTHING else:\n" +
183
+ "\n" +
184
+ "<<<OVERVIEW>>>\n" +
185
+ "<the full markdown OVERVIEW.md, which REPLACES the current one wholesale: " +
186
+ "a synthesized snapshot of the project's CURRENT state for an LLM to read " +
187
+ "at the start of a new chat — what the project is, key decisions/facts, " +
188
+ "open questions, and next steps. No changelog or per-session history here. " +
189
+ "Output exactly NOCHANGE only if the current one is already accurate.>\n" +
190
+ "<<<CHANGELOG>>>\n" +
191
+ "<the full markdown CHANGELOG.md, which REPLACES the current one wholesale. " +
192
+ "PRESERVE the existing dated entries and add at most ONE new bullet under a " +
193
+ "`## YYYY-MM-DD` heading at the TOP (newest-first), reusing today's heading " +
194
+ "if present; coalesce near-duplicate recent bullets. What you return becomes " +
195
+ "the ENTIRE file, so never emit a bare sentence or a lone bullet here — that " +
196
+ "would destroy the file's history. If this activity is already captured by a " +
197
+ "recent entry, output exactly NOCHANGE and change nothing.>\n" +
198
+ "<<<CLAUDE>>>\n" +
199
+ "<the full curated-notes body for CLAUDE.md — everything that belongs under " +
200
+ "the `## Curated notes` heading — which REPLACES that managed section " +
201
+ "wholesale (human-authored content above it is preserved for you). ONLY " +
202
+ "long-lived identity/conventions: what the project fundamentally is, key " +
203
+ "decisions, how we work on it. You are shown the whole current file, so " +
204
+ "DEDUP — fold near-duplicate notes into one and drop stale ones. Never " +
205
+ "restate current state/tasks/history (those are OVERVIEW/CHANGELOG). If " +
206
+ "nothing durable changed, output exactly NOCHANGE.>\n" +
207
+ "<<<END>>>\n" +
208
+ "\n" +
209
+ "OVERVIEW.md describes the PROJECT, not the box it runs on: never record " +
210
+ "box/environment operational conventions (how to run/build/expose a dev " +
211
+ "server, ports, localhost vs. dev hostnames/URLs, where to clone, process " +
212
+ "managers) — those are owned by the box's own CLAUDE.md and must not be " +
213
+ "re-described or contradicted here.\n" +
214
+ "\n" +
215
+ "Be factual and terse. Do not invent details not present in the provided " +
216
+ "activity. Output ONLY the three sections between the markers — no preamble, " +
217
+ "no explanation, no tool use.",
218
+ default_prompt: "Curate OVERVIEW.md, CHANGELOG.md and CLAUDE.md from recent activity.",
219
+ };
220
+ }
221
+ /**
222
+ * A trigger's scoped herdctl agent config (Epic T) — the unified successor to
223
+ * {@link hookAgentConfig}. A trigger that {@link triggerRunsOnOwnAgent} (every event
224
+ * trigger; a schedule trigger with a non-empty `run.tools` allow-list — T2) is
225
+ * registered as its OWN agent `trigger-<slug>-<name>` whose tool config
226
+ * (`allowed_tools`/`permission_mode`/`model`/`max_turns`, projected by
227
+ * {@link triggerToAgentToolConfig} from the trigger's `run`) IS its capability set.
228
+ * Runs in the project's WORKING dir (so a trigger's Bash/Write act on the same tree
229
+ * the keeper does). A tool-less trigger gets `allowed_tools: []` and can only return text.
230
+ */
231
+ export function buildTriggerConfig(cfg, project, triggerName, trigger) {
232
+ const config = {
233
+ name: triggerAgentName(project.slug, triggerName),
234
+ description: `Trigger "${triggerName}" (${trigger.trigger.type}) for project ${project.name}.`,
235
+ working_directory: project.workingDir,
236
+ // Triggers fire through the one-shot `trigger()` path, so this takes effect on
237
+ // every fire: a `claude -p` CLI subprocess. Set here per-agent for the same
238
+ // reason as the keeper/sweeper copies: core's `DefaultsSchema` has no
239
+ // `runtime` key, so the fleet `defaults.runtime` is stripped at parse and
240
+ // never merged in.
241
+ runtime: "cli",
242
+ // Model defaults to the keeper default unless the run pins one;
243
+ // triggerToAgentToolConfig sets `model` only when the run specifies it, so
244
+ // provide the fallback here so a trigger never boots without a concrete model.
245
+ model: trigger.run.model ?? project.model ?? DEFAULT_MODEL,
246
+ // run → tool config (allowed tools, permission mode, model, max_turns).
247
+ ...triggerToAgentToolConfig(trigger.run),
248
+ };
249
+ if (project.docker)
250
+ config.docker = { enabled: true };
251
+ const browser = browserMcpServers(cfg.browserMcp);
252
+ if (browser)
253
+ config.mcp_servers = browser;
254
+ return config;
255
+ }
256
+ /**
257
+ * Write the minimal herdctl.yaml the FleetManager boots from: a fleet block
258
+ * plus the fleet-wide `defaults` (deep-merged into agents added at runtime),
259
+ * and ZERO agents. All agents are registered programmatically via
260
+ * `fleet.addAgent(...)` in init() — paddock no longer generates per-agent
261
+ * yaml files or calls `reload()`.
262
+ *
263
+ * The `fleet` block is strict (name/description only). `defaults` are deep-
264
+ * merged into each agent by addAgent (mergeDefaults defaults to true), so agents
265
+ * inherit model/max_turns/permission_mode/allowed+denied_tools from here. NOT
266
+ * `runtime`: core's `DefaultsSchema` has no such key, so it is stripped at parse
267
+ * and every agent sets its own.
268
+ */
269
+ export async function ensureConfigFile(cfg) {
270
+ const configDir = path.dirname(cfg.herdctlConfigPath);
271
+ await fs.mkdir(configDir, { recursive: true });
272
+ const doc = {
273
+ version: 1,
274
+ fleet: {
275
+ name: "paddock",
276
+ description: "Paddock agent fleet (agents registered at runtime).",
277
+ },
278
+ defaults: {
279
+ // Currently INERT: core's `DefaultsSchema` has no `runtime` key, so this is
280
+ // stripped when the file is parsed and never reaches an agent — which is
281
+ // exactly why the keeper/sweeper/trigger configs above each set `runtime`
282
+ // themselves. Left in place because it costs nothing and records the
283
+ // fleet-wide intent; if core ever accepts it, every agent still overrides
284
+ // it. And even honoured it would govern the one-shot `trigger()` path only:
285
+ // chats opened via `openChatSession` always run the SDK runtime.
286
+ runtime: "cli",
287
+ // The default model, so any agent that doesn't set its own runs on it;
288
+ // in practice every agent above sets `model` explicitly anyway.
289
+ model: DEFAULT_MODEL,
290
+ // ~200 turns: enough for real multi-step coding sessions while still
291
+ // bounding runaway agents (CLAUDE.md: always set max_turns). A project
292
+ // can override this per-project (issue #12); this is the inherited
293
+ // default (shared constant so the DTO resolution stays in sync).
294
+ max_turns: DEFAULT_MAX_TURNS,
295
+ // Keeper agents run native (no Docker) with acceptEdits + denied
296
+ // dangerous bash patterns by default; a project can opt into Docker
297
+ // isolation or a different permission mode per-project (issue #12).
298
+ permission_mode: DEFAULT_PERMISSION_MODE,
299
+ // `Skill` MUST be in the allowlist or every skill invocation is
300
+ // permission-denied — both runtimes pass this list through verbatim (CLI:
301
+ // `--allowedTools`; SDK: `options.allowedTools`) and auto-deny any tool not
302
+ // on it, with no prompt. Built-in skills (claude-api,
303
+ // code-review, deep-research, ...) ship inside the CLI binary and are
304
+ // registered/visible regardless of setting-sources, so the ONLY thing
305
+ // blocking them was this missing tool. Skills routinely fan out to
306
+ // sub-agents (`Task`), track progress (`TodoWrite`), and edit notebooks
307
+ // (`NotebookEdit`), each of which is likewise permission-checked against
308
+ // this same allowlist — so include them here to keep skills functional
309
+ // end-to-end (adds no capability the keeper's existing tools don't).
310
+ // BROWSER_MCP_TOOL (mcp__playwright__*) is listed unconditionally: it is a
311
+ // no-op unless the keeper agent actually attaches the playwright
312
+ // server (gated by PADDOCK_BROWSER_MCP), and having it on the allowlist
313
+ // means enabling the browser is a per-box env flip with no code change.
314
+ // Timer-class autonomy tools (Paddock#111): `ScheduleWakeup` + the
315
+ // session-only `Cron*` set + `Monitor` must be on the allowlist or the
316
+ // runtime auto-denies them, so a keeper couldn't schedule a wake even in
317
+ // session mode. `ToolSearch` is the harness's deferred-tool loader —
318
+ // several of these surface as deferred tools reached through it. These
319
+ // only actually DO anything in session drive-mode (the reaper re-fires
320
+ // them); in batch mode they're inert (documented in the box CLAUDE.md).
321
+ allowed_tools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "Task", "TodoWrite", "Skill", "NotebookEdit", "ToolSearch", "ScheduleWakeup", "Monitor", "CronCreate", "CronList", "CronDelete", BROWSER_MCP_TOOL],
322
+ // Best-effort denylist (#179): narrow, honest catastrophic-wipe patterns
323
+ // that do NOT block legitimate absolute-path cleanup. See DENIED_TOOLS.
324
+ denied_tools: [...DENIED_TOOLS],
325
+ },
326
+ };
327
+ const header = "# GENERATED by paddock-server. Do NOT hand-edit. Agents are NOT listed\n" +
328
+ "# here — they are registered at runtime via FleetManager.addAgent(). This\n" +
329
+ "# file only carries the fleet identity + the defaults merged into them.\n";
330
+ await fs.writeFile(cfg.herdctlConfigPath, header + YAML.stringify(doc), "utf8");
331
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * herdctl-agent-names — the pure, stateless name/visibility helpers and
3
+ * constants that map a project slug (+ hook/trigger name) to the deterministic
4
+ * herdctl agent names Paddock registers, plus the small tool/model constants the
5
+ * agent-config builders lean on.
6
+ *
7
+ * These are split out of {@link HerdctlService} (issue #403) because they only
8
+ * read their arguments — no `fleet`/`cfg`/live-session state — so they are
9
+ * independently readable and unit-testable. Everything here is re-exported from
10
+ * `./herdctl.js` so external importers keep resolving via that path unchanged.
11
+ */
12
+ import type { Project } from "./projects.js";
13
+ /**
14
+ * Maps a workspace key to its keeper agent name. Kept deterministic so the
15
+ * runtime registration and runtime lookups always agree.
16
+ *
17
+ * The key is encoded through {@link agentKeyFor}, because the root workspace's
18
+ * key is `""` and a bare `keeper-` is not a legal herdctl agent name. Every
19
+ * builder below goes through the same encoding, so none of them needs a root
20
+ * branch. See `project-paths.ts` for why the encoding lives at this boundary
21
+ * rather than in the identity.
22
+ */
23
+ export declare function keeperAgentName(slug: string): string;
24
+ /**
25
+ * Inverse of {@link keeperAgentName}: recover a workspace key from a keeper
26
+ * agent name (`keeper-<agentKey>` → `<key>`). Returns `null` for a non-keeper
27
+ * agent (e.g. a sweeper or a trigger agent), which is how a scheduler-fired wake
28
+ * tells "this is a chat to route back to a workspace" from "this is not a chat
29
+ * at all".
30
+ *
31
+ * **Callers must distinguish `null` from `""`.** The root workspace's key IS the
32
+ * empty string, so a falsy check (`if (!key) return;`) silently drops every root
33
+ * chat from wake/turn/job routing. Compare against `null` explicitly.
34
+ */
35
+ export declare function keeperSlugFromAgent(agentName: string): string | null;
36
+ /**
37
+ * The lightweight curator agent used by the post-turn sweep. Runs on a cheap
38
+ * model (Haiku 4.5) with only read/write tools, no Bash.
39
+ *
40
+ * NOTE: herdctl agents bind working_directory per agent. Because a single
41
+ * shared sweeper can only have one cwd, the SweepService registers ONE sweeper
42
+ * agent PER PROJECT (keeper-style) so each has the right cwd. The name is
43
+ * derived from the slug; the agent reads/writes that project's files.
44
+ */
45
+ export declare const SWEEPER_PREFIX = "sweeper-";
46
+ /** Maps a project slug to its sweeper agent name. */
47
+ export declare function sweeperAgentName(slug: string): string;
48
+ /**
49
+ * The agent-name prefix for event hooks (Epic G / G1). Each hook is its OWN herdctl
50
+ * agent `hook-<slug>-<name>` (GG-1) — registered alongside the keeper/sweeper — whose
51
+ * tool config IS the hook's capability set.
52
+ */
53
+ export declare const HOOK_AGENT_PREFIX = "hook-";
54
+ /**
55
+ * Maps a project slug + hook name to its agent name `hook-<slug>-<name>`. Kept
56
+ * deterministic so runtime registration and firing (`startAgentTurn`) always agree.
57
+ * (`slug` is a kebab id and `name` matches `[A-Za-z0-9._-]+`, so the composed name is
58
+ * a valid herdctl agent name; the reverse mapping for visibility (G3) is resolved
59
+ * from a project's declared hooks, not by parsing this string.)
60
+ */
61
+ export declare function hookAgentName(slug: string, hookName: string): string;
62
+ /**
63
+ * The agent-name prefix for unified triggers (Epic T / T1). An EVENT trigger is its
64
+ * OWN herdctl agent `trigger-<slug>-<name>` (tool config = capability), registered
65
+ * alongside the keeper/sweeper — exactly like a hook agent. Schedule triggers run on
66
+ * the keeper (T1) and webhook triggers are reserved, but every trigger carries this
67
+ * deterministic name in its DTO so the mapping is stable across T2–T5.
68
+ */
69
+ export declare const TRIGGER_AGENT_PREFIX = "trigger-";
70
+ /**
71
+ * Maps a project slug + trigger name to its agent name `trigger-<slug>-<name>`. Kept
72
+ * deterministic so runtime registration and firing always agree. The reverse mapping
73
+ * (for visibility) is resolved from a project's declared triggers, not by parsing this
74
+ * string (a slug may contain hyphens). Mirrors {@link hookAgentName}.
75
+ */
76
+ export declare function triggerAgentName(slug: string, triggerName: string): string;
77
+ /**
78
+ * The agents whose chats are VISIBLE in a project's chat list (Epic G / G3, GG-5):
79
+ * the keeper plus every event hook the project declares (`hook-<slug>-<name>`). This
80
+ * is the generalization of the old hard "keeper-only" listing — "all of a project's
81
+ * agents EXCEPT those marked hidden."
82
+ *
83
+ * The **sweeper** (`sweeper-<slug>`) is the one hidden agent: it is deliberately
84
+ * omitted here so its post-turn curation chats never surface (the `hideChats` case),
85
+ * exactly as before. Disabled hooks are still included — a hook chat that already ran should stay
86
+ * visible regardless of whether the hook is currently armed.
87
+ *
88
+ * Kept pure + exported so the listing filter is unit-testable in isolation (the
89
+ * sweeper-stays-hidden regression lives here), and so future callers have ONE place
90
+ * that answers "which of a project's agents' chats do we show?".
91
+ */
92
+ export declare function visibleProjectAgentNames(project: Project): string[];
93
+ /**
94
+ * The Claude Code tool pattern for the Playwright browser MCP. Must live on the
95
+ * agent allowlist (both runtimes auto-deny any tool not on the allow-list —
96
+ * `--allowedTools` on the CLI, `options.allowedTools` on the SDK — same reason
97
+ * `Skill` is listed) — so it is added to `defaults.allowed_tools`,
98
+ * which the keeper agents inherit and the tool-less sweeper overrides
99
+ * away. Harmless when the server isn't enabled: an allowed-but-absent tool is a
100
+ * no-op.
101
+ */
102
+ export declare const BROWSER_MCP_TOOL = "mcp__playwright__*";
103
+ /**
104
+ * The keeper's default `denied_tools` — a **best-effort, defence-in-depth**
105
+ * denylist, NOT a sandbox. Real isolation (per-agent filesystem confinement)
106
+ * is tracked in #7; these string patterns are trivially bypassable (a relative
107
+ * path, a `$VAR`, a `find -delete`) and are here only to make the obvious
108
+ * catastrophic footguns require deliberate rephrasing.
109
+ *
110
+ * Claude Code Bash patterns are prefix-with-`*` matches: `Bash(foo *)` denies
111
+ * any command starting with `foo `, and `Bash(foo)` denies EXACTLY `foo`.
112
+ *
113
+ * History (#179): this list used to carry `Bash(rm -rf /*)`, intended as "don't
114
+ * wipe root". But `/*` is a trailing wildcard, so it actually denied
115
+ * `rm -rf /<ANYTHING>` — i.e. every absolute-path delete, including the keeper
116
+ * cleaning up its OWN scratch/clone dirs (`rm -rf /tmp/foo`,
117
+ * `rm -rf /var/lib/.../clones/x`). That was both over-broad (blocked legitimate
118
+ * work, wasted turns, confusing "denied" cards) and false security (the agent
119
+ * just switched to a relative `rm -rf clones/x` and it went through).
120
+ *
121
+ * The honest replacement targets genuinely-catastrophic *bare* roots:
122
+ * - `rm -rf /` and `rm -rf / *` (root, incl. `--no-preserve-root` variants),
123
+ * - `rm -rf ~` / `rm -rf $HOME` (home, exact — a trailing wildcard would block
124
+ * `rm -rf ~/.cache/foo`, which is legitimate),
125
+ * - bare top-level system dirs matched EXACTLY (`Bash(rm -rf /etc)`, …) so that
126
+ * real subpath deletes under them (notably `/var/lib/paddock/...` and
127
+ * `/tmp/...`) are NOT denied.
128
+ *
129
+ * Note the deliberate gaps: a literal `rm -rf /*` glob command and
130
+ * `rm -rf /usr/local/...` are NOT caught, because the only pattern that would
131
+ * catch them (`Bash(rm -rf /*)`, `Bash(rm -rf /usr*)`) also re-blocks the
132
+ * legitimate absolute-path deletes we exist to allow. Narrow-and-honest beats
133
+ * broad-and-leaky; the sandbox (#7) is the real fix.
134
+ */
135
+ export declare const DENIED_TOOLS: readonly string[];
136
+ /**
137
+ * The Playwright browser MCP server given to the keeper agents so
138
+ * Claude Code can drive a headless Chromium (navigate / click / fill / snapshot
139
+ * / screenshot). Returns `undefined` when `enabled` is false (sourced from
140
+ * `cfg.browserMcp`, i.e. `PADDOCK_BROWSER_MCP=1` — issue #269), so a box WITHOUT
141
+ * the browser stack simply omits the server (no failed spawns) and enabling it
142
+ * is a per-box env flip — no code change.
143
+ *
144
+ * The browser is installed box-side by the homelab `paddock` Ansible role
145
+ * (`npm i -g @playwright/mcp` + `playwright install chromium`, exposing the
146
+ * `playwright-mcp` bin on PATH). The boxes are unprivileged LXCs, so Chromium
147
+ * must run headless + `--no-sandbox` (the container is the isolation boundary);
148
+ * `--isolated` keeps each session's profile in-memory (no persisted user-data
149
+ * dir). `--browser chromium` is REQUIRED: @playwright/mcp defaults to the
150
+ * `chrome` channel (branded Google Chrome), which isn't installed — without this
151
+ * flag the server tries to `playwright install chrome` at first use and stalls.
152
+ * The role installs the open-source `chromium` engine, so we select it here.
153
+ * The tool-less sweeper deliberately never receives this server.
154
+ */
155
+ export declare function browserMcpServers(enabled: boolean): Record<string, unknown> | undefined;
156
+ /**
157
+ * How many chat turns a project's keeper may run at once. herdctl defaults an
158
+ * agent to `max_concurrent: 1`, which would serialize a project's chats and make
159
+ * a second turn (e.g. the first message of a freshly *forked* chat sent while the
160
+ * parent is still streaming) fail with a ConcurrencyLimitError. Paddock is a
161
+ * single-user box that explicitly wants parallel chats per project — especially
162
+ * forks — so we lift the keeper's limit. (The shared-keeper model is still
163
+ * last-write-wins across concurrent chats of the same project; forks default to
164
+ * the parent's model, so that caveat rarely bites in practice.)
165
+ */
166
+ export declare const KEEPER_MAX_CONCURRENT = 10;
167
+ /**
168
+ * How long herdctl keeps a keeper's fallback session alive (Paddock#111). Sized
169
+ * to the reaper's 7-day recurring-wake expiry so a fallback resume still finds
170
+ * the session; explicit-id resume (Paddock's norm) bypasses this anyway.
171
+ */
172
+ export declare const KEEPER_SESSION_TIMEOUT = "168h";