@stackbilt/aegis-core 0.1.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 (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,397 @@
1
+ // ─── ARGUS Phase 3: Event-Driven Actions ─────────────────────
2
+ // Fires autonomous actions in response to webhook events at
3
+ // ingestion time (not on the hourly cron). Responses are near-instant.
4
+ //
5
+ // Action rules are declarative: { event pattern → action + guardrails }.
6
+ // All actions are logged to D1 for audit. Rate-limited per action type.
7
+ //
8
+ // Authority model:
9
+ // READ_ONLY — label, comment, create agenda item (auto)
10
+ // MUTATION — close issue, merge PR (requires operator approval)
11
+
12
+ import { commentOnIssue, addLabelsToIssue, resolveRepoName } from '../github.js';
13
+ import {
14
+ ensureOnBoard,
15
+ moveBoardItemLocal,
16
+ } from './board.js';
17
+
18
+ // ─── Types ────────────────────────────────────────────────────
19
+
20
+ interface ActionContext {
21
+ githubToken: string;
22
+ db: D1Database;
23
+ }
24
+
25
+ interface WebhookEnvelope {
26
+ source: string;
27
+ event_type: string;
28
+ payload: Record<string, unknown>;
29
+ timestamp: string;
30
+ }
31
+
32
+ interface ActionResult {
33
+ action: string;
34
+ target: string;
35
+ success: boolean;
36
+ detail?: string;
37
+ }
38
+
39
+ // ─── Rate Limiting ────────────────────────────────────────────
40
+
41
+ const MAX_ACTIONS_PER_HOUR = 10;
42
+ const RATE_LIMIT_KEY = 'argus_action_count';
43
+
44
+ async function checkRateLimit(db: D1Database): Promise<boolean> {
45
+ const row = await db.prepare(
46
+ "SELECT received_at FROM web_events WHERE event_id = ?"
47
+ ).bind(RATE_LIMIT_KEY).first<{ received_at: string }>();
48
+
49
+ if (!row) return true; // No record = first action
50
+
51
+ // Parse stored value: "count:timestamp"
52
+ const [countStr, tsStr] = row.received_at.split('|');
53
+ const count = parseInt(countStr, 10) || 0;
54
+ const ts = new Date(tsStr).getTime();
55
+
56
+ // Reset if older than 1 hour
57
+ if (Date.now() - ts > 60 * 60 * 1000) return true;
58
+
59
+ return count < MAX_ACTIONS_PER_HOUR;
60
+ }
61
+
62
+ async function incrementRateLimit(db: D1Database): Promise<void> {
63
+ const row = await db.prepare(
64
+ "SELECT received_at FROM web_events WHERE event_id = ?"
65
+ ).bind(RATE_LIMIT_KEY).first<{ received_at: string }>();
66
+
67
+ let count = 1;
68
+ let ts = new Date().toISOString();
69
+
70
+ if (row) {
71
+ const [countStr, tsStr] = row.received_at.split('|');
72
+ const oldTs = new Date(tsStr).getTime();
73
+ if (Date.now() - oldTs < 60 * 60 * 1000) {
74
+ count = (parseInt(countStr, 10) || 0) + 1;
75
+ ts = tsStr; // Keep original window start
76
+ }
77
+ }
78
+
79
+ await db.prepare(
80
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, ?)"
81
+ ).bind(RATE_LIMIT_KEY, `${count}|${ts}`).run();
82
+ }
83
+
84
+ // ─── Action Log ───────────────────────────────────────────────
85
+
86
+ async function logAction(db: D1Database, result: ActionResult, _envelope: WebhookEnvelope): Promise<void> {
87
+ try {
88
+ await db.prepare(
89
+ `INSERT INTO web_events (event_id, received_at) VALUES (?, datetime('now'))`
90
+ ).bind(`argus_action_${Date.now()}_${result.action}`).run();
91
+ console.log(`[argus-actions] ${result.action} → ${result.target}: ${result.success ? 'ok' : 'failed'}${result.detail ? ` (${result.detail})` : ''}`);
92
+ } catch {
93
+ // Non-fatal
94
+ }
95
+ }
96
+
97
+ // ─── Dedup ────────────────────────────────────────────────────
98
+ // Prevent duplicate actions on the same event (e.g., GitHub retries)
99
+
100
+ async function hasActed(db: D1Database, eventKey: string): Promise<boolean> {
101
+ const row = await db.prepare(
102
+ "SELECT event_id FROM web_events WHERE event_id = ?"
103
+ ).bind(`argus_acted_${eventKey}`).first();
104
+ return row !== null;
105
+ }
106
+
107
+ async function markActed(db: D1Database, eventKey: string): Promise<void> {
108
+ await db.prepare(
109
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))"
110
+ ).bind(`argus_acted_${eventKey}`).run();
111
+ }
112
+
113
+ // ─── GitHub Actions ───────────────────────────────────────────
114
+
115
+ async function handleIssueOpened(ctx: ActionContext, payload: Record<string, unknown>): Promise<ActionResult[]> {
116
+ const results: ActionResult[] = [];
117
+ const issue = payload.issue as Record<string, unknown> | undefined;
118
+ const repo = (payload.repository as Record<string, unknown>)?.full_name as string | undefined;
119
+ if (!issue || !repo) return results;
120
+
121
+ const number = issue.number as number;
122
+ const title = (issue.title as string) ?? '';
123
+ const body = (issue.body as string) ?? '';
124
+ const author = (issue.user as Record<string, unknown>)?.login as string ?? '';
125
+ const combined = `${title} ${body}`.toLowerCase();
126
+
127
+ // Skip issues created by bots
128
+ if (author.includes('[bot]')) return results;
129
+
130
+ // Auto-label based on keywords
131
+ const labels: string[] = [];
132
+ if (combined.includes('bug') || combined.includes('error') || combined.includes('broken') || combined.includes('crash')) {
133
+ labels.push('bug');
134
+ }
135
+ if (combined.includes('feature') || combined.includes('request') || combined.includes('enhancement')) {
136
+ labels.push('enhancement');
137
+ }
138
+ if (combined.includes('question') || combined.includes('how do') || combined.includes('help')) {
139
+ labels.push('question');
140
+ }
141
+ if (combined.includes('docs') || combined.includes('documentation') || combined.includes('readme')) {
142
+ labels.push('documentation');
143
+ }
144
+
145
+ if (labels.length > 0) {
146
+ try {
147
+ await addLabelsToIssue(ctx.githubToken, repo, number, labels);
148
+ results.push({ action: 'label', target: `${repo}#${number}`, success: true, detail: labels.join(', ') });
149
+ } catch (err) {
150
+ results.push({ action: 'label', target: `${repo}#${number}`, success: false, detail: err instanceof Error ? err.message : String(err) });
151
+ }
152
+ }
153
+
154
+ // Auto-acknowledge external issues (not from the operator's bot)
155
+ const ack = `Thanks for opening this issue! AEGIS has triaged it${labels.length > 0 ? ` and applied labels: ${labels.map(l => `\`${l}\``).join(', ')}` : ''}. A maintainer will review shortly.\n\n*[AEGIS] — automated triage*`;
156
+ try {
157
+ await commentOnIssue(ctx.githubToken, repo, number, ack);
158
+ results.push({ action: 'acknowledge', target: `${repo}#${number}`, success: true });
159
+ } catch (err) {
160
+ results.push({ action: 'acknowledge', target: `${repo}#${number}`, success: false, detail: err instanceof Error ? err.message : String(err) });
161
+ }
162
+
163
+ return results;
164
+ }
165
+
166
+ async function handlePrOpened(ctx: ActionContext, payload: Record<string, unknown>): Promise<ActionResult[]> {
167
+ const results: ActionResult[] = [];
168
+ const pr = payload.pull_request as Record<string, unknown> | undefined;
169
+ const repo = (payload.repository as Record<string, unknown>)?.full_name as string | undefined;
170
+ if (!pr || !repo) return results;
171
+
172
+ const number = pr.number as number;
173
+ const author = (pr.user as Record<string, unknown>)?.login as string ?? '';
174
+
175
+ // Skip bot PRs and auto/ branches
176
+ const head = (pr.head as Record<string, unknown>)?.ref as string ?? '';
177
+ if (author.includes('[bot]') || head.startsWith('auto/') || head.startsWith('aegis/')) return results;
178
+
179
+ // Acknowledge external PRs
180
+ const ack = `Thanks for the PR! AEGIS will run initial checks. A maintainer will review shortly.\n\n*[AEGIS] — automated triage*`;
181
+ try {
182
+ await commentOnIssue(ctx.githubToken, repo, number, ack);
183
+ results.push({ action: 'acknowledge_pr', target: `${repo}#${number}`, success: true });
184
+ } catch (err) {
185
+ results.push({ action: 'acknowledge_pr', target: `${repo}#${number}`, success: false, detail: err instanceof Error ? err.message : String(err) });
186
+ }
187
+
188
+ return results;
189
+ }
190
+
191
+ async function handleCiFailure(ctx: ActionContext, payload: Record<string, unknown>): Promise<ActionResult[]> {
192
+ const results: ActionResult[] = [];
193
+ const checkRun = payload.check_run as Record<string, unknown> | undefined;
194
+ const repo = (payload.repository as Record<string, unknown>)?.full_name as string | undefined;
195
+ if (!checkRun || !repo) return results;
196
+
197
+ const conclusion = checkRun.conclusion as string;
198
+ if (conclusion !== 'failure') return results;
199
+
200
+ // Find associated PRs
201
+ const pullRequests = checkRun.pull_requests as Array<Record<string, unknown>> | undefined;
202
+ if (!pullRequests || pullRequests.length === 0) return results;
203
+
204
+ for (const pr of pullRequests) {
205
+ const prNumber = pr.number as number;
206
+ if (!prNumber) continue;
207
+
208
+ const name = (checkRun.name as string) ?? 'CI';
209
+ const comment = `CI check **${name}** failed on this PR. Please review the [check output](${(checkRun.html_url as string) ?? ''}).\n\n*[AEGIS] — CI watcher*`;
210
+ try {
211
+ await commentOnIssue(ctx.githubToken, repo, prNumber, comment);
212
+ results.push({ action: 'ci_failure_comment', target: `${repo}#${prNumber}`, success: true, detail: name });
213
+ } catch (err) {
214
+ results.push({ action: 'ci_failure_comment', target: `${repo}#${prNumber}`, success: false, detail: err instanceof Error ? err.message : String(err) });
215
+ }
216
+ }
217
+
218
+ return results;
219
+ }
220
+
221
+ // ─── Stripe Actions ───────────────────────────────────────────
222
+
223
+ async function handleStripeEvent(ctx: ActionContext, event_type: string, payload: Record<string, unknown>): Promise<ActionResult[]> {
224
+ const results: ActionResult[] = [];
225
+
226
+ // Create tracking entries for payment events (lightweight D1 writes)
227
+ const data = payload.data as Record<string, unknown> | undefined;
228
+ const obj = data?.object as Record<string, unknown> | undefined;
229
+
230
+ if (event_type === 'customer.subscription.created') {
231
+ const customerId = (obj?.customer as string) ?? 'unknown';
232
+ await ctx.db.prepare(
233
+ "INSERT INTO web_events (event_id, received_at) VALUES (?, datetime('now'))"
234
+ ).bind(`argus_stripe_new_sub_${customerId}_${Date.now()}`).run();
235
+ results.push({ action: 'track_new_subscription', target: customerId, success: true });
236
+ }
237
+
238
+ if (event_type === 'customer.subscription.deleted') {
239
+ const customerId = (obj?.customer as string) ?? 'unknown';
240
+ await ctx.db.prepare(
241
+ "INSERT INTO web_events (event_id, received_at) VALUES (?, datetime('now'))"
242
+ ).bind(`argus_stripe_churn_${customerId}_${Date.now()}`).run();
243
+ results.push({ action: 'track_churn', target: customerId, success: true });
244
+ }
245
+
246
+ return results;
247
+ }
248
+
249
+ // ─── Board Transitions ───────────────────────────────────────
250
+
251
+ async function handleBoardTransition(ctx: ActionContext, envelope: WebhookEnvelope): Promise<ActionResult[]> {
252
+ const results: ActionResult[] = [];
253
+ const payload = envelope.payload;
254
+ const repo = (payload.repository as Record<string, unknown>)?.full_name as string | undefined;
255
+ if (!repo) return results;
256
+
257
+ try {
258
+ const eventType = envelope.event_type;
259
+
260
+ // ── Issue closed → mark shipped ──────────────────────
261
+ if (eventType === 'issues.closed') {
262
+ const issue = payload.issue as Record<string, unknown> | undefined;
263
+ if (!issue) return results;
264
+ const number = issue.number as number;
265
+ try {
266
+ await moveBoardItemLocal(ctx.db, repo, number, 'shipped');
267
+ results.push({ action: 'board_move', target: `${repo}#${number}`, success: true, detail: 'shipped' });
268
+ } catch {
269
+ // Board not configured — non-fatal
270
+ }
271
+ }
272
+
273
+ // ── Issue reopened → back to backlog ─────────────────
274
+ if (eventType === 'issues.reopened') {
275
+ const issue = payload.issue as Record<string, unknown> | undefined;
276
+ if (!issue) return results;
277
+ const number = issue.number as number;
278
+ try {
279
+ await moveBoardItemLocal(ctx.db, repo, number, 'backlog');
280
+ results.push({ action: 'board_move', target: `${repo}#${number}`, success: true, detail: 'backlog' });
281
+ } catch {
282
+ // Board not configured — non-fatal
283
+ }
284
+ }
285
+
286
+ // ── PR opened — move linked issue to in_progress ─────
287
+ if (eventType === 'pull_request.opened' || eventType === 'pull_request.merged') {
288
+ const pr = payload.pull_request as Record<string, unknown> | undefined;
289
+ if (!pr) return results;
290
+ const body = (pr.body as string) ?? '';
291
+ const title = (pr.title as string) ?? '';
292
+ const combined = `${title} ${body}`;
293
+
294
+ // Extract issue references (#NNN)
295
+ const issueRefs = [...combined.matchAll(/#(\d+)/g)].map(m => parseInt(m[1], 10));
296
+ const targetStatus = eventType === 'pull_request.merged' ? 'shipped' as const : 'in_progress' as const;
297
+
298
+ for (const issueNum of issueRefs) {
299
+ try {
300
+ await moveBoardItemLocal(ctx.db, repo, issueNum, targetStatus);
301
+ results.push({ action: 'board_move', target: `${repo}#${issueNum}`, success: true, detail: targetStatus });
302
+ } catch {
303
+ // Board not configured — non-fatal
304
+ }
305
+ }
306
+ }
307
+
308
+ // ── PR closed (not merged) — move linked issues back ─
309
+ if (eventType === 'pull_request.closed') {
310
+ const pr = payload.pull_request as Record<string, unknown> | undefined;
311
+ if (!pr) return results;
312
+ const merged = pr.merged as boolean;
313
+ if (merged) return results; // Handled above as pull_request.merged
314
+
315
+ const body = (pr.body as string) ?? '';
316
+ const title = (pr.title as string) ?? '';
317
+ const issueRefs = [...`${title} ${body}`.matchAll(/#(\d+)/g)].map(m => parseInt(m[1], 10));
318
+
319
+ for (const issueNum of issueRefs) {
320
+ try {
321
+ await moveBoardItemLocal(ctx.db, repo, issueNum, 'backlog');
322
+ results.push({ action: 'board_move', target: `${repo}#${issueNum}`, success: true, detail: 'backlog' });
323
+ } catch {
324
+ // Board not configured — non-fatal
325
+ }
326
+ }
327
+ }
328
+ } catch (err) {
329
+ console.warn(`[argus-actions] Board transition error: ${err instanceof Error ? err.message : String(err)}`);
330
+ results.push({ action: 'board_transition', target: repo, success: false, detail: err instanceof Error ? err.message : String(err) });
331
+ }
332
+
333
+ return results;
334
+ }
335
+
336
+ // ─── Main Dispatcher ──────────────────────────────────────────
337
+
338
+ export async function dispatchActions(
339
+ envelope: WebhookEnvelope,
340
+ githubToken: string | undefined,
341
+ db: D1Database,
342
+ ): Promise<void> {
343
+ if (!githubToken) return;
344
+
345
+ // Rate limit check
346
+ if (!(await checkRateLimit(db))) {
347
+ console.log('[argus-actions] Rate limit reached — skipping actions');
348
+ return;
349
+ }
350
+
351
+ // Dedup check
352
+ const eventKey = `${envelope.source}_${envelope.event_type}_${envelope.timestamp}`;
353
+ if (await hasActed(db, eventKey)) return;
354
+
355
+ const ctx: ActionContext = { githubToken, db };
356
+ let results: ActionResult[] = [];
357
+
358
+ try {
359
+ // GitHub event routing
360
+ if (envelope.source === 'github') {
361
+ switch (envelope.event_type) {
362
+ case 'issues.opened':
363
+ results = await handleIssueOpened(ctx, envelope.payload);
364
+ break;
365
+ case 'pull_request.opened':
366
+ results = await handlePrOpened(ctx, envelope.payload);
367
+ break;
368
+ case 'check_run.completed':
369
+ results = await handleCiFailure(ctx, envelope.payload);
370
+ break;
371
+ }
372
+
373
+ // Board transitions — runs for all GitHub events (issues + PRs)
374
+ const boardResults = await handleBoardTransition(ctx, envelope);
375
+ results = results.concat(boardResults);
376
+ }
377
+
378
+ // Stripe event routing
379
+ if (envelope.source === 'stripe') {
380
+ results = await handleStripeEvent(ctx, envelope.event_type, envelope.payload);
381
+ }
382
+ } catch (err) {
383
+ console.error('[argus-actions] Dispatch error:', err instanceof Error ? err.message : String(err));
384
+ return;
385
+ }
386
+
387
+ // Log results and update rate limit
388
+ if (results.length > 0) {
389
+ await markActed(db, eventKey);
390
+ for (const result of results) {
391
+ await logAction(db, result, envelope);
392
+ if (result.success) {
393
+ await incrementRateLimit(db);
394
+ }
395
+ }
396
+ }
397
+ }