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