iriai-build 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 (80) hide show
  1. package/bin/iriai-build.js +78 -0
  2. package/bridge-v3.js +98 -0
  3. package/cli/bootstrap.js +83 -0
  4. package/cli/commands/implementation.js +64 -0
  5. package/cli/commands/index.js +46 -0
  6. package/cli/commands/launch.js +153 -0
  7. package/cli/commands/plan.js +117 -0
  8. package/cli/commands/setup.js +80 -0
  9. package/cli/commands/slack.js +97 -0
  10. package/cli/commands/transfer.js +111 -0
  11. package/cli/config.js +92 -0
  12. package/cli/display.js +121 -0
  13. package/cli/terminal-input.js +666 -0
  14. package/cli/wait.js +82 -0
  15. package/index.js +1488 -0
  16. package/lib/agent-process.js +170 -0
  17. package/lib/bridge-state.js +126 -0
  18. package/lib/constants.js +137 -0
  19. package/lib/health-monitor.js +113 -0
  20. package/lib/prompt-builder.js +565 -0
  21. package/lib/signal-watcher.js +215 -0
  22. package/lib/slack-helpers.js +224 -0
  23. package/lib/state-machines/feature-lead.js +408 -0
  24. package/lib/state-machines/operator-agent.js +173 -0
  25. package/lib/state-machines/planning-role.js +161 -0
  26. package/lib/state-machines/role-agent.js +186 -0
  27. package/lib/state-machines/team-orchestrator.js +160 -0
  28. package/package.json +31 -0
  29. package/v3/.handover-html-evidence.md +35 -0
  30. package/v3/KICKOFF-HTML-EVIDENCE.md +98 -0
  31. package/v3/PLAN-HTML-EVIDENCE-HARDENING.md +603 -0
  32. package/v3/adapters/desktop-adapter.js +78 -0
  33. package/v3/adapters/interface.js +146 -0
  34. package/v3/adapters/slack-adapter.js +608 -0
  35. package/v3/adapters/slack-helpers.js +179 -0
  36. package/v3/adapters/terminal-adapter.js +249 -0
  37. package/v3/agent-supervisor.js +320 -0
  38. package/v3/artifact-portal.js +1184 -0
  39. package/v3/bridge.db +0 -0
  40. package/v3/constants.js +170 -0
  41. package/v3/db.js +76 -0
  42. package/v3/file-io.js +216 -0
  43. package/v3/helpers.js +174 -0
  44. package/v3/operator.js +364 -0
  45. package/v3/orchestrator.js +2886 -0
  46. package/v3/plan-compiler.js +440 -0
  47. package/v3/prompt-builder.js +849 -0
  48. package/v3/queries.js +461 -0
  49. package/v3/recovery.js +508 -0
  50. package/v3/review-sessions.js +360 -0
  51. package/v3/roles/accessibility-auditor/CLAUDE.md +50 -0
  52. package/v3/roles/analytics-engineer/CLAUDE.md +40 -0
  53. package/v3/roles/architect/CLAUDE.md +809 -0
  54. package/v3/roles/backend-implementer/CLAUDE.md +97 -0
  55. package/v3/roles/code-reviewer/CLAUDE.md +89 -0
  56. package/v3/roles/database-implementer/CLAUDE.md +97 -0
  57. package/v3/roles/deployer/CLAUDE.md +42 -0
  58. package/v3/roles/designer/CLAUDE.md +386 -0
  59. package/v3/roles/documentation/CLAUDE.md +40 -0
  60. package/v3/roles/feature-lead/CLAUDE.md +233 -0
  61. package/v3/roles/frontend-implementer/CLAUDE.md +97 -0
  62. package/v3/roles/implementer/CLAUDE.md +97 -0
  63. package/v3/roles/integration-tester/CLAUDE.md +174 -0
  64. package/v3/roles/observability-engineer/CLAUDE.md +40 -0
  65. package/v3/roles/operator/CLAUDE.md +322 -0
  66. package/v3/roles/orchestrator/CLAUDE.md +288 -0
  67. package/v3/roles/package-implementer/CLAUDE.md +47 -0
  68. package/v3/roles/performance-analyst/CLAUDE.md +49 -0
  69. package/v3/roles/plan-compiler/CLAUDE.md +163 -0
  70. package/v3/roles/planning-lead/CLAUDE.md +41 -0
  71. package/v3/roles/pm/CLAUDE.md +806 -0
  72. package/v3/roles/regression-tester/CLAUDE.md +135 -0
  73. package/v3/roles/release-manager/CLAUDE.md +43 -0
  74. package/v3/roles/security-auditor/CLAUDE.md +90 -0
  75. package/v3/roles/smoke-tester/CLAUDE.md +97 -0
  76. package/v3/roles/test-author/CLAUDE.md +42 -0
  77. package/v3/roles/verifier/CLAUDE.md +90 -0
  78. package/v3/schema.sql +134 -0
  79. package/v3/slack-adapter.js +510 -0
  80. package/v3/slack-helpers.js +346 -0
package/v3/queries.js ADDED
@@ -0,0 +1,461 @@
1
+ // queries.js — Named SQL query functions (CRUD for all tables).
2
+
3
+ import * as db from "./db.js";
4
+
5
+ // ─── Features ────────────────────────────────────────────────────────────────
6
+
7
+ export function createFeature({ slug, threadTs, signalDir, numTeams = 2 }) {
8
+ const d = db.get();
9
+ const stmt = d.prepare(`
10
+ INSERT INTO features (slug, thread_ts, signal_dir, num_teams)
11
+ VALUES (?, ?, ?, ?)
12
+ `);
13
+ const result = stmt.run(slug, threadTs, signalDir, numTeams);
14
+ return getFeatureById(result.lastInsertRowid);
15
+ }
16
+
17
+ export function getFeatureById(id) {
18
+ const d = db.get();
19
+ return d.prepare("SELECT * FROM features WHERE id = ?").get(id);
20
+ }
21
+
22
+ export function getAllFeatures() {
23
+ const d = db.get();
24
+ return d.prepare("SELECT * FROM features ORDER BY updated_at DESC").all();
25
+ }
26
+
27
+ export function getFeatureBySlug(slug) {
28
+ const d = db.get();
29
+ return d.prepare("SELECT * FROM features WHERE slug = ?").get(slug);
30
+ }
31
+
32
+ export function getFeatureByThreadTs(threadTs) {
33
+ const d = db.get();
34
+ return d.prepare("SELECT * FROM features WHERE thread_ts = ?").get(threadTs);
35
+ }
36
+
37
+ export function getActiveFeatures() {
38
+ const d = db.get();
39
+ return d.prepare("SELECT * FROM features WHERE phase NOT IN ('complete', 'failed')").all();
40
+ }
41
+
42
+ export function getFeaturesByPhase(phase) {
43
+ const d = db.get();
44
+ return d.prepare("SELECT * FROM features WHERE phase = ?").all(phase);
45
+ }
46
+
47
+ export function getFeaturesForPlanCommand() {
48
+ const d = db.get();
49
+ return d.prepare(
50
+ "SELECT * FROM features WHERE phase IN ('planning', 'plan-approval') ORDER BY updated_at DESC"
51
+ ).all();
52
+ }
53
+
54
+ export function getFeaturesForImplCommand() {
55
+ const d = db.get();
56
+ return d.prepare(
57
+ "SELECT * FROM features WHERE phase IN ('plan-approval', 'impl') ORDER BY updated_at DESC"
58
+ ).all();
59
+ }
60
+
61
+ export function getTerminalFeatures() {
62
+ const d = db.get();
63
+ // Terminal adapter sets thread_ts = "terminal-{timestamp}" and feature_channel = slug.
64
+ // Slack channel IDs match pattern C + 10 uppercase alphanumeric chars (e.g. C0123456789).
65
+ // Exclude features already synced to Slack (feature_channel is a Slack channel ID).
66
+ return d.prepare(
67
+ "SELECT * FROM features WHERE thread_ts LIKE 'terminal-%' AND (feature_channel IS NULL OR feature_channel NOT GLOB 'C[0-9A-Z]*') AND phase NOT IN ('complete', 'failed')"
68
+ ).all();
69
+ }
70
+
71
+ export function updateFeaturePhase(featureId, phase) {
72
+ const d = db.get();
73
+ d.prepare(`
74
+ UPDATE features SET phase = ?, updated_at = datetime('now') WHERE id = ?
75
+ `).run(phase, featureId);
76
+ }
77
+
78
+ export function updateFeaturePlanningRole(featureId, role) {
79
+ const d = db.get();
80
+ d.prepare(`
81
+ UPDATE features SET active_planning_role = ?, updated_at = datetime('now') WHERE id = ?
82
+ `).run(role, featureId);
83
+ }
84
+
85
+ export function updateFeatureChannel(featureId, channelId) {
86
+ const d = db.get();
87
+ d.prepare(`
88
+ UPDATE features SET feature_channel = ?, updated_at = datetime('now') WHERE id = ?
89
+ `).run(channelId, featureId);
90
+ }
91
+
92
+ export function updateFeatureGate(featureId, gateNumber) {
93
+ const d = db.get();
94
+ d.prepare(`
95
+ UPDATE features SET gate_number = ?, updated_at = datetime('now') WHERE id = ?
96
+ `).run(gateNumber, featureId);
97
+ }
98
+
99
+ export function updateFeatureGateEvidenceTs(featureId, ts) {
100
+ const d = db.get();
101
+ d.prepare(`
102
+ UPDATE features SET gate_evidence_ts = ?, updated_at = datetime('now') WHERE id = ?
103
+ `).run(ts, featureId);
104
+ }
105
+
106
+ export function updateFeaturePlanSummaryTs(featureId, ts) {
107
+ const d = db.get();
108
+ d.prepare(`
109
+ UPDATE features SET plan_summary_ts = ?, updated_at = datetime('now') WHERE id = ?
110
+ `).run(ts, featureId);
111
+ }
112
+
113
+ export function updateFeatureMetadata(featureId, metadata) {
114
+ const d = db.get();
115
+ d.prepare(`
116
+ UPDATE features SET metadata = ?, updated_at = datetime('now') WHERE id = ?
117
+ `).run(JSON.stringify(metadata), featureId);
118
+ }
119
+
120
+ export function getFeatureMetadata(featureId) {
121
+ const row = getFeatureById(featureId);
122
+ return row ? JSON.parse(row.metadata || "{}") : {};
123
+ }
124
+
125
+ // ─── Agents ──────────────────────────────────────────────────────────────────
126
+
127
+ export function createAgent({ featureId, agentType, agentKey, roleName, teamNum, signalDir, cwd, model = "opus", maxRetries = 2 }) {
128
+ const d = db.get();
129
+ const stmt = d.prepare(`
130
+ INSERT INTO agents (feature_id, agent_type, agent_key, role_name, team_num, signal_dir, cwd, model, max_retries)
131
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
132
+ `);
133
+ const result = stmt.run(featureId, agentType, agentKey, roleName ?? null, teamNum ?? null, signalDir, cwd, model, maxRetries);
134
+ return getAgentById(result.lastInsertRowid);
135
+ }
136
+
137
+ export function getAgentById(id) {
138
+ const d = db.get();
139
+ return d.prepare("SELECT * FROM agents WHERE id = ?").get(id);
140
+ }
141
+
142
+ export function getAgentByKey(agentKey) {
143
+ const d = db.get();
144
+ return d.prepare("SELECT * FROM agents WHERE agent_key = ?").get(agentKey);
145
+ }
146
+
147
+ export function getAgentsByFeature(featureId) {
148
+ const d = db.get();
149
+ return d.prepare("SELECT * FROM agents WHERE feature_id = ?").all(featureId);
150
+ }
151
+
152
+ export function getAgentsByFeatureAndType(featureId, agentType) {
153
+ const d = db.get();
154
+ return d.prepare("SELECT * FROM agents WHERE feature_id = ? AND agent_type = ?").all(featureId, agentType);
155
+ }
156
+
157
+ export function getRunningAgents(featureId) {
158
+ const d = db.get();
159
+ return d.prepare("SELECT * FROM agents WHERE feature_id = ? AND status IN ('starting', 'running', 'retrying')").all(featureId);
160
+ }
161
+
162
+ export function getAllRunningAgents() {
163
+ const d = db.get();
164
+ return d.prepare("SELECT * FROM agents WHERE status IN ('starting', 'running', 'retrying')").all();
165
+ }
166
+
167
+ export function updateAgentStatus(agentId, status) {
168
+ const d = db.get();
169
+ const updates = { status };
170
+ if (status === "running" || status === "starting") {
171
+ d.prepare(`
172
+ UPDATE agents SET status = ?, started_at = datetime('now'), updated_at = datetime('now') WHERE id = ?
173
+ `).run(status, agentId);
174
+ } else if (status === "done" || status === "crashed" || status === "killed") {
175
+ d.prepare(`
176
+ UPDATE agents SET status = ?, exited_at = datetime('now'), updated_at = datetime('now') WHERE id = ?
177
+ `).run(status, agentId);
178
+ } else {
179
+ d.prepare(`
180
+ UPDATE agents SET status = ?, updated_at = datetime('now') WHERE id = ?
181
+ `).run(status, agentId);
182
+ }
183
+ }
184
+
185
+ export function updateAgentPid(agentId, pid) {
186
+ const d = db.get();
187
+ d.prepare("UPDATE agents SET pid = ?, updated_at = datetime('now') WHERE id = ?").run(pid, agentId);
188
+ }
189
+
190
+ export function updateAgentExit(agentId, { exitCode, elapsedMs }) {
191
+ const d = db.get();
192
+ d.prepare(`
193
+ UPDATE agents SET last_exit_code = ?, last_exit_elapsed_ms = ?, exited_at = datetime('now'), updated_at = datetime('now')
194
+ WHERE id = ?
195
+ `).run(exitCode, elapsedMs, agentId);
196
+ }
197
+
198
+ export function incrementAgentRetry(agentId) {
199
+ const d = db.get();
200
+ d.prepare(`
201
+ UPDATE agents SET retry_count = retry_count + 1, status = 'retrying', updated_at = datetime('now')
202
+ WHERE id = ?
203
+ `).run(agentId);
204
+ }
205
+
206
+ export function resetAgentRetry(agentId) {
207
+ const d = db.get();
208
+ d.prepare(`
209
+ UPDATE agents SET retry_count = 0, updated_at = datetime('now') WHERE id = ?
210
+ `).run(agentId);
211
+ }
212
+
213
+ export function updateAgentMaxRetries(agentId, maxRetries) {
214
+ const d = db.get();
215
+ d.prepare(`
216
+ UPDATE agents SET max_retries = ?, updated_at = datetime('now') WHERE id = ?
217
+ `).run(maxRetries, agentId);
218
+ }
219
+
220
+ export function deleteAgentsByFeature(featureId) {
221
+ const d = db.get();
222
+ d.prepare("DELETE FROM agents WHERE feature_id = ?").run(featureId);
223
+ }
224
+
225
+ // ─── Events ──────────────────────────────────────────────────────────────────
226
+
227
+ export function insertEvent(featureId, eventType, source, content, metadata = {}, slackTs = null) {
228
+ const d = db.get();
229
+ const stmt = d.prepare(`
230
+ INSERT INTO events (feature_id, event_type, source, content, metadata, slack_ts)
231
+ VALUES (?, ?, ?, ?, ?, ?)
232
+ `);
233
+ const result = stmt.run(featureId, eventType, source, content ?? null, JSON.stringify(metadata), slackTs ?? null);
234
+ return result.lastInsertRowid;
235
+ }
236
+
237
+ export function getEventsByFeature(featureId, { limit = 50, offset = 0, eventType = null } = {}) {
238
+ const d = db.get();
239
+ if (eventType) {
240
+ return d.prepare(`
241
+ SELECT * FROM events WHERE feature_id = ? AND event_type = ?
242
+ ORDER BY created_at DESC LIMIT ? OFFSET ?
243
+ `).all(featureId, eventType, limit, offset);
244
+ }
245
+ return d.prepare(`
246
+ SELECT * FROM events WHERE feature_id = ?
247
+ ORDER BY created_at DESC LIMIT ? OFFSET ?
248
+ `).all(featureId, limit, offset);
249
+ }
250
+
251
+ export function getRecentEvents(featureId, limit = 30) {
252
+ const d = db.get();
253
+ return d.prepare(`
254
+ SELECT * FROM events WHERE feature_id = ?
255
+ ORDER BY created_at DESC LIMIT ?
256
+ `).all(featureId, limit);
257
+ }
258
+
259
+ export function getUserMessages(featureId, limit = 20) {
260
+ const d = db.get();
261
+ return d.prepare(`
262
+ SELECT DISTINCT source, content, created_at
263
+ FROM events
264
+ WHERE feature_id = ? AND source LIKE 'user:%'
265
+ ORDER BY created_at DESC LIMIT ?
266
+ `).all(featureId, limit);
267
+ }
268
+
269
+ export function getEventById(eventId) {
270
+ const d = db.get();
271
+ return d.prepare("SELECT * FROM events WHERE id = ?").get(eventId);
272
+ }
273
+
274
+ // ─── Decisions ───────────────────────────────────────────────────────────────
275
+
276
+ export function createDecision({ featureId, decisionId, decisionType, title, contextText, options, multi = false }) {
277
+ const d = db.get();
278
+ const stmt = d.prepare(`
279
+ INSERT INTO decisions (feature_id, decision_id, decision_type, title, context_text, options, multi)
280
+ VALUES (?, ?, ?, ?, ?, ?, ?)
281
+ ON CONFLICT(feature_id, decision_id) DO UPDATE SET
282
+ title = excluded.title,
283
+ context_text = excluded.context_text,
284
+ options = excluded.options,
285
+ status = 'pending',
286
+ selected_option = NULL,
287
+ resolved_by = NULL,
288
+ resolved_at = NULL
289
+ `);
290
+ stmt.run(featureId, decisionId, decisionType, title, contextText ?? null, JSON.stringify(options), multi ? 1 : 0);
291
+ return getDecision(featureId, decisionId);
292
+ }
293
+
294
+ export function getDecision(featureId, decisionId) {
295
+ const d = db.get();
296
+ return d.prepare("SELECT * FROM decisions WHERE feature_id = ? AND decision_id = ?").get(featureId, decisionId);
297
+ }
298
+
299
+ export function getDecisionByChannel(slackChannel, decisionId) {
300
+ const d = db.get();
301
+ return d.prepare("SELECT * FROM decisions WHERE slack_channel = ? AND decision_id = ? AND status = 'pending'").get(slackChannel, decisionId);
302
+ }
303
+
304
+ export function getPendingDecision(featureId) {
305
+ const d = db.get();
306
+ return d.prepare("SELECT * FROM decisions WHERE feature_id = ? AND status = 'pending' LIMIT 1").get(featureId);
307
+ }
308
+
309
+ export function resolveDecision(featureId, decisionId, { selectedOption, resolvedBy, evidence, links, media }) {
310
+ const d = db.get();
311
+ d.prepare(`
312
+ UPDATE decisions SET
313
+ status = 'resolved',
314
+ selected_option = ?,
315
+ resolved_by = ?,
316
+ resolved_at = datetime('now'),
317
+ evidence = ?,
318
+ links = ?,
319
+ media = ?
320
+ WHERE feature_id = ? AND decision_id = ?
321
+ `).run(
322
+ selectedOption ?? null, resolvedBy ?? null,
323
+ evidence ? JSON.stringify(evidence) : null,
324
+ links ? JSON.stringify(links) : null,
325
+ media ? JSON.stringify(media) : null,
326
+ featureId, decisionId
327
+ );
328
+ }
329
+
330
+ export function deferDecision(featureId, decisionId) {
331
+ const d = db.get();
332
+ d.prepare(`
333
+ UPDATE decisions SET status = 'deferred' WHERE feature_id = ? AND decision_id = ?
334
+ `).run(featureId, decisionId);
335
+ }
336
+
337
+ export function updateDecisionSlack(featureId, decisionId, { slackTs, slackChannel, permalink }) {
338
+ const d = db.get();
339
+ d.prepare(`
340
+ UPDATE decisions SET slack_ts = ?, slack_channel = ?, permalink = ?
341
+ WHERE feature_id = ? AND decision_id = ?
342
+ `).run(slackTs, slackChannel, permalink, featureId, decisionId);
343
+ }
344
+
345
+ // ─── Operator Relay Queue ────────────────────────────────────────────────
346
+
347
+ export function insertRelayEntry({ featureId, sourceAgent, eventHint, rawContent }) {
348
+ const d = db.get();
349
+ const stmt = d.prepare(`
350
+ INSERT INTO operator_relay_queue (feature_id, source_agent, event_hint, raw_content)
351
+ VALUES (?, ?, ?, ?)
352
+ `);
353
+ const result = stmt.run(featureId, sourceAgent, eventHint, rawContent);
354
+ return d.prepare("SELECT * FROM operator_relay_queue WHERE id = ?").get(result.lastInsertRowid);
355
+ }
356
+
357
+ export function getNextPendingRelay(featureId) {
358
+ const d = db.get();
359
+ return d.prepare(`
360
+ SELECT * FROM operator_relay_queue
361
+ WHERE feature_id = ? AND status = 'pending'
362
+ ORDER BY created_at ASC LIMIT 1
363
+ `).get(featureId);
364
+ }
365
+
366
+ export function updateRelayStatus(id, status, { processedAt = null } = {}) {
367
+ const d = db.get();
368
+ if (processedAt) {
369
+ d.prepare(`
370
+ UPDATE operator_relay_queue SET status = ?, processed_at = ? WHERE id = ?
371
+ `).run(status, processedAt, id);
372
+ } else {
373
+ d.prepare(`
374
+ UPDATE operator_relay_queue SET status = ? WHERE id = ?
375
+ `).run(status, id);
376
+ }
377
+ }
378
+
379
+ export function incrementRelayRetry(id) {
380
+ const d = db.get();
381
+ d.prepare(`
382
+ UPDATE operator_relay_queue SET retry_count = retry_count + 1 WHERE id = ?
383
+ `).run(id);
384
+ }
385
+
386
+ export function getProcessingRelays(featureId) {
387
+ const d = db.get();
388
+ return d.prepare(`
389
+ SELECT * FROM operator_relay_queue
390
+ WHERE feature_id = ? AND status = 'processing'
391
+ ORDER BY created_at ASC
392
+ `).all(featureId);
393
+ }
394
+
395
+ export function getAllProcessingRelays() {
396
+ const d = db.get();
397
+ return d.prepare(`
398
+ SELECT * FROM operator_relay_queue WHERE status = 'processing'
399
+ ORDER BY created_at ASC
400
+ `).all();
401
+ }
402
+
403
+ // ─── Review Sessions ────────────────────────────────────────────────────────
404
+
405
+ export function insertReviewSession({ decisionId, featureId, sessionId, port, docPath, type = "doc" }) {
406
+ const d = db.get();
407
+ d.prepare(`
408
+ INSERT OR REPLACE INTO review_sessions (decision_id, feature_id, session_id, port, doc_path, type)
409
+ VALUES (?, ?, ?, ?, ?, ?)
410
+ `).run(decisionId, featureId, sessionId, port, docPath, type);
411
+ }
412
+
413
+ export function getReviewSession(decisionId) {
414
+ const d = db.get();
415
+ return d.prepare("SELECT * FROM review_sessions WHERE decision_id = ?").get(decisionId);
416
+ }
417
+
418
+ export function updateReviewSessionQa(decisionId, { qaSessionId, qaPort, qaTargetUrl }) {
419
+ const d = db.get();
420
+ d.prepare(`
421
+ UPDATE review_sessions SET qa_session_id = ?, qa_port = ?, qa_target_url = ?
422
+ WHERE decision_id = ?
423
+ `).run(qaSessionId, qaPort, qaTargetUrl, decisionId);
424
+ }
425
+
426
+ export function deleteReviewSession(decisionId) {
427
+ const d = db.get();
428
+ d.prepare("DELETE FROM review_sessions WHERE decision_id = ?").run(decisionId);
429
+ }
430
+
431
+ export function getReviewSessionsByFeature(featureId) {
432
+ const d = db.get();
433
+ return d.prepare("SELECT * FROM review_sessions WHERE feature_id = ?").all(featureId);
434
+ }
435
+
436
+ // ─── Slack Posts (Dedup) ─────────────────────────────────────────────────────
437
+
438
+ export function recordSlackPost(eventId, featureId, channel, slackTs) {
439
+ const d = db.get();
440
+ try {
441
+ d.prepare(`
442
+ INSERT INTO slack_posts (event_id, feature_id, channel, slack_ts)
443
+ VALUES (?, ?, ?, ?)
444
+ `).run(eventId, featureId, channel, slackTs);
445
+ return true;
446
+ } catch {
447
+ // UNIQUE constraint violation = already posted
448
+ return false;
449
+ }
450
+ }
451
+
452
+ export function isEventPosted(eventId, channel) {
453
+ const d = db.get();
454
+ const row = d.prepare("SELECT 1 FROM slack_posts WHERE event_id = ? AND channel = ?").get(eventId, channel);
455
+ return !!row;
456
+ }
457
+
458
+ export function getSlackPost(eventId, channel) {
459
+ const d = db.get();
460
+ return d.prepare("SELECT * FROM slack_posts WHERE event_id = ? AND channel = ?").get(eventId, channel);
461
+ }