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