@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.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- 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
|
+
}
|