tide-commander 1.94.0 → 1.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/assets/{BossLogsModal-b4O2O00j.js → BossLogsModal-BFy6Rxdn.js} +1 -1
  2. package/dist/assets/{BossSpawnModal--Cn40tMQ.js → BossSpawnModal-KQyJYj_B.js} +1 -1
  3. package/dist/assets/{ControlsModal-SNJ95Qn9.js → ControlsModal-W_ghuG_Q.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-DOkOsLaK.js → DockerLogsModal-valTcuRy.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-BKLJKK3B.js → EmbeddedEditor-CIsFWo6A.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-B8ao6401.js → GmailOAuthSetup-AcW1zAlk.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-By-gup6S.js → GoogleOAuthSetup-C9uZ46Oe.js} +1 -1
  8. package/dist/assets/{IframeModal-KzrFyAG0.js → IframeModal-CmWv7GXl.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CXMEldir.js → IntegrationsPanel-CNvzosgx.js} +2 -2
  10. package/dist/assets/{LogViewerModal-C70SfK7U.js → LogViewerModal-imTE-619.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CbZtFYh9.js → MonitoringModal-DLxBY738.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-C8mo6Tqu.js → PM2LogsModal-S4n0D11V.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BUB-mruP.js → RestoreArchivedAreaModal-B32sqCxN.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-zinDgDAK.js → Scene2DCanvas-bEu0UPUm.js} +1 -1
  15. package/dist/assets/{SceneManager-CAxi1UHT.js → SceneManager-Ce3X0awy.js} +1 -1
  16. package/dist/assets/{SkillsPanel-Bh_J9suc.js → SkillsPanel-Dj22nDU8.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-DZSpM5ew.js → SlackMultiInstanceSetup-J9NdRY0U.js} +1 -1
  18. package/dist/assets/{SpawnModal-BrcKWjDs.js → SpawnModal-F9nspdWd.js} +1 -1
  19. package/dist/assets/{SubordinateAssignmentModal-CBqmszjc.js → SubordinateAssignmentModal-DT31CkPr.js} +1 -1
  20. package/dist/assets/TriggerManagerPanel-CVJ9ozHz.js +9 -0
  21. package/dist/assets/{WorkflowEditorPanel-DE9448gZ.js → WorkflowEditorPanel-BYRi0cU9.js} +1 -1
  22. package/dist/assets/{index-DuDcnBNX.js → index-B9G4hrFR.js} +1 -1
  23. package/dist/assets/{index-DPvgEcBT.js → index-BPy8NE5B.js} +3 -3
  24. package/dist/assets/{index-Cbk6B5BY.js → index-BmjrPptb.js} +1 -1
  25. package/dist/assets/{index-DelPL0HI.js → index-ByfAbkqv.js} +1 -1
  26. package/dist/assets/{index-B4VnKOHB.js → index-C5wC_Xis.js} +1 -1
  27. package/dist/assets/{index-Dk2THz0j.js → index-CLr8DpLX.js} +2 -2
  28. package/dist/assets/{index-BkZ422b_.js → index-DJm7oMXk.js} +1 -1
  29. package/dist/assets/{index-CpLZo6IM.js → index-DkjWoHhy.js} +1 -1
  30. package/dist/assets/{index-C7EieXis.js → index-DqZhHROd.js} +1 -1
  31. package/dist/assets/{main-CteALl_G.js → main-9uTEp9Lr.js} +4 -4
  32. package/dist/assets/{web-CGYGq80f.js → web-CLuvtqCm.js} +1 -1
  33. package/dist/assets/{web-BZa7rL0p.js → web-D-a3phls.js} +1 -1
  34. package/dist/assets/{web-DtevCxzm.js → web-DM2bFWBN.js} +1 -1
  35. package/dist/index.html +1 -1
  36. package/dist/src/packages/server/app.js +14 -0
  37. package/dist/src/packages/server/data/builtin-skills/bitbucket-pr-review.js +431 -0
  38. package/dist/src/packages/server/data/builtin-skills/bitbucket-reviewer.js +191 -0
  39. package/dist/src/packages/server/data/builtin-skills/create-building.js +521 -484
  40. package/dist/src/packages/server/data/builtin-skills/index.js +4 -0
  41. package/dist/src/packages/server/data/builtin-skills/trigger-designer.js +246 -71
  42. package/dist/src/packages/server/routes/bitbucket-author-loop.js +54 -0
  43. package/dist/src/packages/server/routes/buildings.js +298 -0
  44. package/dist/src/packages/server/routes/index.js +3 -1
  45. package/dist/src/packages/server/routes/trigger-routes.js +68 -15
  46. package/dist/src/packages/server/routes/webhook-dedupe.js +58 -0
  47. package/dist/src/packages/server/routes/webhook-signatures.js +67 -0
  48. package/dist/src/packages/server/services/building-service.js +400 -85
  49. package/dist/src/packages/server/services/trigger-service.js +5 -3
  50. package/dist/src/packages/server/websocket/handler.js +2 -4
  51. package/package.json +1 -1
  52. package/dist/assets/TriggerManagerPanel-BD7VnHYc.js +0 -9
@@ -11,6 +11,8 @@ import { gitCaptain } from './git-captain.js';
11
11
  import { serverLogs } from './server-logs.js';
12
12
  import { streamingExec } from './streaming-exec.js';
13
13
  import { bitbucketPR } from './bitbucket-pr.js';
14
+ import { bitbucketPRReview } from './bitbucket-pr-review.js';
15
+ import { bitbucketReviewer } from './bitbucket-reviewer.js';
14
16
  import { pm2Logs } from './pm2-logs.js';
15
17
  import { createBuilding } from './create-building.js';
16
18
  import { exploreDatabase } from './explore-database.js';
@@ -34,6 +36,8 @@ export const BUILTIN_SKILLS = [
34
36
  serverLogs,
35
37
  streamingExec,
36
38
  bitbucketPR,
39
+ bitbucketPRReview,
40
+ bitbucketReviewer,
37
41
  pm2Logs,
38
42
  createBuilding,
39
43
  exploreDatabase,
@@ -17,22 +17,23 @@ Triggers are event-driven rules that call agents with predefined templates.
17
17
 
18
18
  A trigger has:
19
19
  - **Name & Description**: What the trigger does
20
- - **Trigger Type**: How the event is detected (webhook, cron, slack, email, jira, manual)
21
- - **Matching Mode**: How to match events (structural, LLM, hybrid)
20
+ - **Trigger Type**: How the event is detected (webhook, cron, slack, email, jira, whatsapp, bitbucket)
21
+ - **Match Mode**: How to evaluate if an event matches (\`structural\`, \`llm\`, \`hybrid\`)
22
+ - **Extraction Mode** (optional): How to pull template variables out of the event (\`structural\` or \`llm\`)
22
23
  - **Agent Assignment**: Which agent to fire
23
- - **Template**: Prompt template to send to the agent
24
- - **Rate Limiting**: Max fires per minute (default 10)
24
+ - **Template**: Prompt template sent to the agent, supports \`{{variable}}\` interpolation
25
25
 
26
26
  ## Trigger Types
27
27
 
28
28
  | Type | When It Fires | Use Cases |
29
29
  |---|---|---|
30
- | \`webhook\` | When webhook URL receives POST request | External systems trigger workflows |
31
- | \`cron\` | On a schedule (cron expression) | Periodic tasks, reports, maintenance |
32
- | \`slack\` | When Slack message matches pattern | Team notifications, approvals |
33
- | \`email\` | When email arrives matching pattern | Customer requests, alerts |
34
- | \`jira\` | When Jira ticket created/updated | Issue escalation, automation |
35
- | \`manual\` | Triggered manually by user | Testing, ad-hoc execution |
30
+ | \`webhook\` | When \`POST /api/triggers/webhook/:triggerId\` receives a request | External systems push events; HMAC-validated; recommended path for GitHub/Bitbucket Cloud webhooks (see Bitbucket section below) |
31
+ | \`cron\` | On a schedule (recurring expression or one-shot \`runAt\`) | Periodic tasks, reports, maintenance, scheduled reminders |
32
+ | \`slack\` | When a Slack message matches the configured filters | Team notifications, approvals, command parsing |
33
+ | \`email\` | When a Gmail message matches the configured filters | Customer requests, alerts, approval workflows |
34
+ | \`jira\` | When a Jira webhook delivery matches | Issue escalation, automation, comment routing |
35
+ | \`whatsapp\` | When a WhatsApp message arrives matching the configured filters | Inbound customer messages, group monitoring |
36
+ | \`bitbucket\` | (Type exists in the type system but the webhook receiver currently only accepts \`type: 'webhook'\` — for Bitbucket Cloud, use a webhook trigger as shown below) | n/a — see Bitbucket section |
36
37
 
37
38
  ## List All Triggers
38
39
 
@@ -53,20 +54,26 @@ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
53
54
  "type": "webhook",
54
55
  "agentId": "AGENT_ID",
55
56
  "matchMode": "structural",
56
- "extractionMode": "none",
57
- "config": { "method": "POST" },
58
- "promptTemplate": "Process external event: {{eventType}} - {{eventData}}",
59
- "enabled": true,
60
- "rateLimit": 10
57
+ "config": {
58
+ "method": "POST",
59
+ "secret": "OPTIONAL_HMAC_SECRET",
60
+ "extractFields": ["eventType", "data.user.id"]
61
+ },
62
+ "promptTemplate": "Process external event: {{eventType}} for user {{data.user.id}}\\n\\nFull payload: {{payload}}",
63
+ "enabled": true
61
64
  }
62
65
  EOF
63
66
  \`\`\`
64
67
 
65
- The webhook endpoint will be: \`http://localhost:{{PORT}}/api/webhook/trigger/TRIGGER_ID\`
68
+ The webhook endpoint will be: \`http://localhost:{{PORT}}/api/triggers/webhook/TRIGGER_ID\`
69
+
70
+ POST JSON to this endpoint to fire the trigger. If \`config.secret\` is set, the receiver requires either:
71
+ - \`X-Hub-Signature-256\` (GitHub-style HMAC-SHA256) or \`X-Hub-Signature\` (Bitbucket-style), with the signature computed over the raw request body, OR
72
+ - \`X-Webhook-Secret\` header set to the literal secret value (plain comparison).
66
73
 
67
- Post JSON to this endpoint to fire the trigger.
74
+ Each path listed in \`config.extractFields\` becomes a template variable named after the dotted path (e.g. \`{{data.user.id}}\`). The full payload is also available as \`{{payload}}\` (JSON-stringified).
68
75
 
69
- ## Create a Cron Trigger
76
+ ## Create a Cron Trigger (recurring)
70
77
 
71
78
  \`\`\`bash
72
79
  curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
@@ -79,13 +86,19 @@ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
79
86
  "type": "cron",
80
87
  "agentId": "AGENT_ID",
81
88
  "matchMode": "structural",
82
- "config": { "expression": "0 9 * * 1-5", "timezone": "UTC" },
83
- "promptTemplate": "Generate daily status report",
89
+ "config": {
90
+ "expression": "0 9 * * 1-5",
91
+ "timezone": "America/Mexico_City",
92
+ "payload": { "report": "daily-status" }
93
+ },
94
+ "promptTemplate": "Generate {{report}} report for {{cron.scheduledAt}}",
84
95
  "enabled": true
85
96
  }
86
97
  EOF
87
98
  \`\`\`
88
99
 
100
+ Keys in \`config.payload\` are merged into the template variables on every fire, in addition to the built-in \`{{cron.expression}}\` and \`{{cron.scheduledAt}}\`.
101
+
89
102
  ### Common Cron Expressions
90
103
 
91
104
  | Expression | When It Runs |
@@ -96,6 +109,48 @@ EOF
96
109
  | \`0 0 1 * *\` | First day of every month |
97
110
  | \`*/15 * * * *\` | Every 15 minutes |
98
111
 
112
+ ## Create a One-Shot Cron Trigger
113
+
114
+ For a single fire at a specific moment, omit \`expression\` and set \`runOnce: true\` + \`runAt\` (ISO 8601, treat as UTC):
115
+
116
+ \`\`\`bash
117
+ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
118
+ -H "Content-Type: application/json" \\
119
+ -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
120
+ -d @- <<'EOF'
121
+ {
122
+ "name": "One-Shot Reminder",
123
+ "type": "cron",
124
+ "agentId": "AGENT_ID",
125
+ "matchMode": "structural",
126
+ "config": {
127
+ "expression": "",
128
+ "timezone": "UTC",
129
+ "runOnce": true,
130
+ "runAt": "2026-05-15T17:00:00Z",
131
+ "payload": { "topic": "Q2 review prep" }
132
+ },
133
+ "promptTemplate": "Remind me about {{topic}} (scheduled at {{cron.runAt}})",
134
+ "enabled": true
135
+ }
136
+ EOF
137
+ \`\`\`
138
+
139
+ After firing, the trigger is auto-disabled and \`config.completedAt\` is set. If the server was down at \`runAt\` past the grace window, the trigger is marked errored with \`config.missedAt\`.
140
+
141
+ ## Validate a Cron Expression
142
+
143
+ Before saving, sanity-check the expression and preview the next fire times:
144
+
145
+ \`\`\`bash
146
+ curl -s -X POST "http://localhost:{{PORT}}/api/triggers/validate-cron" \\
147
+ -H "Content-Type: application/json" \\
148
+ -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
149
+ -d '{ "expression": "0 9 * * 1-5", "timezone": "America/Mexico_City" }'
150
+ \`\`\`
151
+
152
+ Response: \`{ "valid": true, "nextFires": ["2026-05-11T15:00:00.000Z", ...] }\` or \`{ "valid": false, "error": "..." }\`.
153
+
99
154
  ## Create a Slack Trigger
100
155
 
101
156
  \`\`\`bash
@@ -109,7 +164,7 @@ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
109
164
  "type": "slack",
110
165
  "agentId": "AGENT_ID",
111
166
  "matchMode": "structural",
112
- "config": { "channelId": "C0123456789" },
167
+ "config": { "channelId": "C0123456789", "messagePattern": "approve|deny" },
113
168
  "promptTemplate": "Process approval request from @{{slack.user}}: {{slack.message}}",
114
169
  "enabled": true
115
170
  }
@@ -130,7 +185,8 @@ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
130
185
  "agentId": "AGENT_ID",
131
186
  "matchMode": "llm",
132
187
  "config": { "subjectPattern": "urgent|critical" },
133
- "promptTemplate": "Handle customer inquiry from {{sender}}: {{subject}}\\n\\n{{body}}",
188
+ "llmMatch": { "prompt": "Is this a real customer escalation requiring same-day reply?" },
189
+ "promptTemplate": "Handle customer inquiry from {{email.from}}: {{email.subject}}\\n\\n{{email.body}}",
134
190
  "enabled": true
135
191
  }
136
192
  EOF
@@ -149,20 +205,99 @@ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
149
205
  "type": "jira",
150
206
  "agentId": "AGENT_ID",
151
207
  "matchMode": "structural",
152
- "config": { "projectKey": "INCIDENT", "events": ["jira:issue_created"] },
153
- "promptTemplate": "Respond to P1 incident {{ticket_key}}: {{summary}}",
208
+ "config": { "projectKey": "INCIDENT", "events": ["jira:issue_created"], "jqlFilter": "priority = P1" },
209
+ "promptTemplate": "Respond to P1 incident {{jira.issueKey}}: {{jira.summary}}",
210
+ "enabled": true
211
+ }
212
+ EOF
213
+ \`\`\`
214
+
215
+ ## Create a WhatsApp Trigger
216
+
217
+ \`\`\`bash
218
+ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
219
+ -H "Content-Type: application/json" \\
220
+ -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
221
+ -d @- <<'EOF'
222
+ {
223
+ "name": "Inbound DM Handler",
224
+ "description": "Fire on inbound 1:1 DMs that mention 'order'",
225
+ "type": "whatsapp",
226
+ "agentId": "AGENT_ID",
227
+ "matchMode": "structural",
228
+ "config": { "direction": "inbound", "dmOnly": true, "bodyPattern": "order" },
229
+ "promptTemplate": "Reply to {{whatsapp.fromName}} ({{whatsapp.from}}): {{whatsapp.body}}",
230
+ "enabled": true
231
+ }
232
+ EOF
233
+ \`\`\`
234
+
235
+ ## Bitbucket Cloud Webhooks
236
+
237
+ Use \`type: 'bitbucket'\` so the trigger gets the typed UX in the UI (workspace / repo-slug / event-key fields). The central webhook receiver at \`POST /api/triggers/webhook/:triggerId\` accepts both \`webhook\` and \`bitbucket\` types — its signature, dedupe, and author-loop helpers auto-detect the Bitbucket vs GitHub flavor from request headers, so the trigger type is purely a UI / metadata concern. The \`bitbucket-reviewer\` skill walks through the agent-side prompt; this skill just creates the trigger:
238
+
239
+ \`\`\`bash
240
+ curl -s -X POST "http://localhost:{{PORT}}/api/triggers" \\
241
+ -H "Content-Type: application/json" \\
242
+ -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
243
+ -d @- <<'EOF'
244
+ {
245
+ "name": "Bitbucket PR Reviewer (wind)",
246
+ "description": "Review every PR opened or updated on tide/wind",
247
+ "type": "bitbucket",
248
+ "agentId": "AGENT_ID",
249
+ "matchMode": "structural",
250
+ "config": {
251
+ "method": "POST",
252
+ "secret": "GENERATE_A_RANDOM_HIGH_ENTROPY_STRING",
253
+ "extractFields": [
254
+ "pullrequest.id",
255
+ "pullrequest.title",
256
+ "repository.full_name",
257
+ "actor.nickname"
258
+ ]
259
+ },
260
+ "promptTemplate": "Review pull request #{{pullrequest.id}} on {{repository.full_name}}: {{pullrequest.title}}",
154
261
  "enabled": true
155
262
  }
156
263
  EOF
157
264
  \`\`\`
158
265
 
266
+ After creation, paste the per-trigger URL \`http://<public-host>/api/triggers/webhook/TRIGGER_ID\` and the same \`secret\` into Bitbucket → Repository settings → Webhooks. The receiver verifies the \`X-Hub-Signature\` (Bitbucket's HMAC-SHA256 header), dedupes retries via \`X-Request-UUID\`, and respects an author-loop guard via the \`BITBUCKET_BOT_USERNAME\` env var. See \`docs/bitbucket-pr-review.md\` and the \`bitbucket-reviewer\` skill for the full setup.
267
+
159
268
  ## Matching Modes
160
269
 
161
270
  | Mode | How It Works | Best For |
162
271
  |---|---|---|
163
- | \`structural\` | Pattern matching on defined fields | Predictable, structured events |
164
- | \`llm\` | LLM evaluates if event is relevant | Complex, semantic matching |
165
- | \`hybrid\` | Structural match, then LLM verification | Balance of speed and accuracy |
272
+ | \`structural\` | Per-handler field matching (regex/equality on the typed payload) | Predictable, structured events |
273
+ | \`llm\` | Haiku-by-default LLM evaluates if the event is relevant given \`llmMatch.prompt\` | Complex, semantic matching |
274
+ | \`hybrid\` | Structural pre-filter; if it passes, LLM verifies | Balance of speed and cost |
275
+
276
+ For \`llm\` and \`hybrid\`, also set:
277
+
278
+ \`\`\`json
279
+ "llmMatch": {
280
+ "prompt": "Natural-language condition the event must satisfy",
281
+ "model": "haiku",
282
+ "temperature": 0,
283
+ "maxTokens": 150,
284
+ "minConfidence": 0.6
285
+ }
286
+ \`\`\`
287
+
288
+ ## Variable Extraction (LLM)
289
+
290
+ To pull free-form variables out of unstructured event payloads, set \`extractionMode: "llm"\` and \`llmExtract\`:
291
+
292
+ \`\`\`json
293
+ "extractionMode": "llm",
294
+ "llmExtract": {
295
+ "prompt": "Extract the customer's account ID and request type",
296
+ "variables": ["accountId", "requestType"]
297
+ }
298
+ \`\`\`
299
+
300
+ Variables returned by the extractor are merged on top of the structural variables before the prompt is interpolated.
166
301
 
167
302
  ## Get Trigger Details
168
303
 
@@ -186,92 +321,132 @@ curl -s -X DELETE "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID" \\
186
321
  -H "X-Auth-Token: {{AUTH_TOKEN}}"
187
322
  \`\`\`
188
323
 
189
- ## Test a Trigger's Pattern Matching
324
+ ## Test a Trigger's Match Pipeline (dry run)
325
+
326
+ Send a sample payload through the trigger's full match + extract pipeline without firing the agent:
190
327
 
191
328
  \`\`\`bash
192
329
  curl -s -X POST "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID/test-match" \\
193
330
  -H "Content-Type: application/json" \\
194
331
  -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
195
332
  -d '{
196
- "eventType": "slack_message",
197
- "messageText": "urgent: fix the database",
198
- "sender": "alice@example.com"
333
+ "payload": {
334
+ "messageText": "urgent: fix the database",
335
+ "sender": "alice@example.com"
336
+ }
199
337
  }'
200
338
  \`\`\`
201
339
 
202
- Response: \`{ "matches": true, "confidence": 0.95 }\`
340
+ You may also send a fully-formed event under the \`event\` key: \`{ "event": { "source": "slack", "type": "message", "data": { ... }, "timestamp": 1700000000000 } }\`.
341
+
342
+ Response shape (\`TestMatchResult\` from \`src/packages/shared/trigger-types.ts\`):
343
+
344
+ \`\`\`json
345
+ {
346
+ "structuralMatch": true,
347
+ "llmMatch": { "match": true, "confidence": 0.92, "reason": "...", "durationMs": 412, "model": "haiku", "tokensUsed": 84 },
348
+ "extractedVariables": { "slack.user": "alice", "slack.message": "..." },
349
+ "wouldFire": true,
350
+ "matcherExecutions": [ ... ]
351
+ }
352
+ \`\`\`
203
353
 
204
354
  ## Fire a Trigger Manually
205
355
 
356
+ Bypasses match logic; jumps straight to firing the agent with the supplied template variables:
357
+
206
358
  \`\`\`bash
207
359
  curl -s -X POST "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID/fire" \\
208
360
  -H "Content-Type: application/json" \\
209
361
  -H "X-Auth-Token: {{AUTH_TOKEN}}" \\
210
362
  -d '{
211
- "eventType": "manual",
212
- "data": {
213
- "requester": "alice@example.com",
214
- "priority": "high"
215
- }
363
+ "variables": { "requester": "alice@example.com", "priority": "high" },
364
+ "payload": { "raw": "optional raw payload, recorded with the fire" }
216
365
  }'
217
366
  \`\`\`
218
367
 
219
- ## Check Trigger History
368
+ ## Trigger Fire History
220
369
 
221
370
  \`\`\`bash
222
- curl -s -H "X-Auth-Token: {{AUTH_TOKEN}}" "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID/history?limit=10"
371
+ curl -s -H "X-Auth-Token: {{AUTH_TOKEN}}" "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID/events?limit=10"
223
372
  \`\`\`
224
373
 
225
- Returns recent firing events and their results.
374
+ Returns recent fire records (\`TriggerFireRow\` from \`src/packages/shared/trigger-types.ts\`) with status, variables, payload, LLM results, and duration.
226
375
 
227
- ## Common Template Variables
376
+ ## Matcher Diagnostics
228
377
 
229
- Depending on trigger type, these variables are available in prompts:
378
+ When debugging why a trigger did or didn't fire:
230
379
 
231
- ### Slack triggers
232
- - \`{{slack.user}}\` - Username of the sender
233
- - \`{{slack.userId}}\` - Slack user ID
234
- - \`{{slack.message}}\` - Message text
235
- - \`{{slack.channel}}\` - Channel ID
236
- - \`{{slack.threadTs}}\` - Thread timestamp
380
+ \`\`\`bash
381
+ # All matchers that ran for one trigger event
382
+ curl -s -H "X-Auth-Token: {{AUTH_TOKEN}}" "http://localhost:{{PORT}}/api/triggers/events/EVENT_ID/matchers"
383
+
384
+ # Matcher-execution history for a trigger across all events
385
+ curl -s -H "X-Auth-Token: {{AUTH_TOKEN}}" "http://localhost:{{PORT}}/api/triggers/TRIGGER_ID/matcher-history?limit=100"
386
+
387
+ # Every matcher that evaluated against a specific source message (e.g. one Slack ts)
388
+ curl -s -H "X-Auth-Token: {{AUTH_TOKEN}}" "http://localhost:{{PORT}}/api/triggers/matchers/by-source/SOURCE_TYPE/SOURCE_ID"
389
+ \`\`\`
390
+
391
+ ## Common Template Variables
392
+
393
+ Every fire receives \`{{trigger.name}}\` and \`{{timestamp}}\` (ISO string). The handler-specific variables below come from each integration's \`extractVariables\` implementation.
237
394
 
238
395
  ### Webhook triggers
239
- - \`{{eventType}}\` - Type of event
240
- - \`{{eventData}}\` - Full event payload
396
+ - \`{{payload}}\` full request body, JSON-stringified
397
+ - Each path in \`config.extractFields\` becomes a variable named after the dotted path (e.g. \`{{data.user.id}}\`)
398
+
399
+ ### Cron triggers
400
+ - \`{{cron.expression}}\` — the cron expression (recurring jobs only)
401
+ - \`{{cron.scheduledAt}}\` — ISO timestamp when the fire was scheduled
402
+ - \`{{cron.runAt}}\` — the ISO \`runAt\` (one-shot only)
403
+ - Each key in \`config.payload\` is merged in as a top-level variable
404
+
405
+ ### Slack triggers
406
+ - \`{{slack.user}}\`, \`{{slack.userId}}\`, \`{{slack.message}}\`, \`{{slack.channel}}\`, \`{{slack.threadTs}}\`
407
+ - \`{{slack.fileCount}}\`, \`{{slack.fileIds}}\`, \`{{slack.fileNames}}\`, \`{{slack.instanceId}}\`
241
408
 
242
409
  ### Email triggers
243
- - \`{{sender}}\` - Who sent the email
244
- - \`{{subject}}\` - Email subject
245
- - \`{{body}}\` - Email body
410
+ - \`{{email.from}}\`, \`{{email.to}}\`, \`{{email.subject}}\`, \`{{email.body}}\`
411
+ - \`{{email.threadId}}\`, \`{{email.messageId}}\`, \`{{email.date}}\`
412
+ - \`{{email.hasAttachments}}\`, \`{{email.attachments}}\`, \`{{email.direction}}\`, \`{{email.labels}}\`
246
413
 
247
414
  ### Jira triggers
248
- - \`{{ticket_key}}\` - Jira ticket ID
249
- - \`{{summary}}\` - Issue summary
250
- - \`{{priority}}\` - Priority level
415
+ - \`{{jira.issueKey}}\`, \`{{jira.issueId}}\`, \`{{jira.summary}}\`, \`{{jira.status}}\`
416
+ - \`{{jira.project}}\`, \`{{jira.projectName}}\`, \`{{jira.issueType}}\`, \`{{jira.priority}}\`
417
+ - \`{{jira.assignee}}\`, \`{{jira.reporter}}\`, \`{{jira.user}}\`, \`{{jira.eventType}}\`
418
+ - \`{{jira.labels}}\`, \`{{jira.changes}}\`, \`{{jira.commentAuthor}}\`, \`{{jira.commentBody}}\`
419
+
420
+ ### WhatsApp triggers
421
+ - \`{{whatsapp.from}}\`, \`{{whatsapp.fromName}}\`, \`{{whatsapp.body}}\`
422
+ - \`{{whatsapp.sessionId}}\`, \`{{whatsapp.chatId}}\`, \`{{whatsapp.isGroup}}\`, \`{{whatsapp.groupName}}\`
423
+ - \`{{whatsapp.direction}}\`, \`{{whatsapp.mediaType}}\`, \`{{whatsapp.mediaUrl}}\`, \`{{whatsapp.timestamp}}\`
251
424
 
252
425
  ## Design Guidelines
253
426
 
254
427
  1. **Names**: Use clear, action-oriented names ("Daily Report", "Slack Approval Processor")
255
428
  2. **Prompts**: Be specific. Tell the agent what to do with the event data
256
- 3. **Matching**: Start with \`structural\` for simple patterns, use \`llm\` for complex semantic matching
257
- 4. **Rate Limiting**: Set appropriate limits to prevent duplicate processing
258
- 5. **Testing**: Always test pattern matching before enabling triggers
259
- 6. **Template Variables**: Reference only variables that exist for that trigger type
260
- 7. **Error Handling**: Agent responses are logged in trigger history for debugging
429
+ 3. **Matching**: Start with \`structural\` for simple patterns, use \`hybrid\` to gate LLM calls behind a cheap pre-filter
430
+ 4. **Testing**: Always run \`/test-match\` before enabling a trigger that will fire on production traffic
431
+ 5. **Template Variables**: Reference only variables that exist for that trigger type (see the section above)
432
+ 6. **Debugging**: Use the matcher-diagnostics endpoints to see exactly which matcher passed/failed and why
261
433
 
262
434
  ## Rate Limiting
263
435
 
264
- Triggers enforce rate limits to prevent accidental loops or spam:
265
- - Default: 10 fires per minute
266
- - Minimum: 1 per minute
267
- - Set when creating or updating trigger
436
+ Each trigger is rate-limited to **10 fires per minute**. The cap is hardcoded in \`src/packages/server/services/trigger-service.ts\` (\`RATE_LIMIT_MAX\`) and is not configurable per-trigger via the API. Requests exceeding the cap are dropped silently after the limit is hit; check fire history if a trigger appears to "miss" events.
437
+
438
+ ## Webhook Security
439
+
440
+ Webhook triggers can require one of two authentication mechanisms by setting \`config.secret\`:
441
+
442
+ | Mechanism | Header | Body input |
443
+ |---|---|---|
444
+ | HMAC-SHA256 (GitHub style) | \`X-Hub-Signature-256: sha256=<hex>\` | Raw request bytes |
445
+ | HMAC-SHA256 (Bitbucket style) | \`X-Hub-Signature: sha256=<hex>\` | Raw request bytes |
446
+ | Plain shared secret | \`X-Webhook-Secret: <secret>\` | Header value compared directly to \`config.secret\` |
268
447
 
269
- ## Webhook Trigger Security
448
+ When \`config.secret\` is set, requests with no signature are rejected with \`401 { "error": "Missing signature" }\`. The receiver also dedupes retries via \`X-Request-UUID\` (Bitbucket) or \`X-GitHub-Delivery\` (GitHub) within a 10-minute window.
270
449
 
271
- Webhook triggers accept any POST request. To secure them:
272
- 1. Only expose to trusted systems
273
- 2. Use network-level access controls
274
- 3. Monitor trigger history for unusual activity
275
- 4. Disable triggers when not in use
450
+ If \`config.secret\` is unset, the endpoint accepts any POST only do this for triggers behind a trusted network or dedicated path.
276
451
  `,
277
452
  };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Bitbucket author-loop guard.
3
+ *
4
+ * When the bot account itself comments / approves / requests-changes on a PR,
5
+ * Bitbucket fires another webhook with the bot as `actor`. Without this guard,
6
+ * the agent would react to its own activity in a loop.
7
+ *
8
+ * Scope: only events where the actor's identity is meaningfully separable
9
+ * from the PR author. `pullrequest:created` is excluded — the actor IS the PR
10
+ * author and we DO want to route on PR creation regardless of who opened it.
11
+ *
12
+ * Identity match: compares the configured `BITBUCKET_BOT_USERNAME` against
13
+ * three actor fields in the payload, returning true if any match. The env
14
+ * var name suggests `nickname` (the typical case), but UUID and account_id
15
+ * are also accepted for higher reliability — UUIDs/account_ids never change,
16
+ * whereas a nickname can be re-claimed if the bot is renamed.
17
+ * - `actor.nickname` (e.g. "review-bot") — most readable
18
+ * - `actor.uuid` (e.g. "{abc-1234-...}") — most reliable
19
+ * - `actor.account_id` (e.g. "557058:abcd-...") — also reliable
20
+ */
21
+ const GUARDED_EVENT_KEYS = new Set([
22
+ 'pullrequest:approved',
23
+ 'pullrequest:unapproved',
24
+ 'pullrequest:changes_request_created',
25
+ 'pullrequest:changes_request_removed',
26
+ 'pullrequest:comment_created',
27
+ 'pullrequest:comment_updated',
28
+ 'pullrequest:comment_deleted',
29
+ ]);
30
+ /**
31
+ * Returns true when the event is a guarded action AND the actor in the
32
+ * payload matches the configured bot identifier. Caller should ACK 200 with
33
+ * `{ skipped: 'author-loop-guard' }` and skip routing.
34
+ *
35
+ * Returns false when:
36
+ * - `botIdentifier` is empty (guard disabled)
37
+ * - `eventKey` is not in the guarded set (e.g. pullrequest:created)
38
+ * - the payload's actor doesn't match any of the candidate fields
39
+ */
40
+ export function isBitbucketAuthorLoop(eventKey, payload, botIdentifier) {
41
+ if (!botIdentifier || !eventKey)
42
+ return false;
43
+ if (!GUARDED_EVENT_KEYS.has(eventKey))
44
+ return false;
45
+ if (!payload || typeof payload !== 'object')
46
+ return false;
47
+ const actor = payload.actor;
48
+ if (!actor || typeof actor !== 'object')
49
+ return false;
50
+ const a = actor;
51
+ const candidates = [a.nickname, a.uuid, a.account_id]
52
+ .filter((v) => typeof v === 'string' && v.length > 0);
53
+ return candidates.includes(botIdentifier);
54
+ }