@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
package/src/email.ts ADDED
@@ -0,0 +1,850 @@
1
+ // Resend email client — fetch-based, no SDK dependency
2
+
3
+ import { operatorConfig } from './operator/index.js';
4
+
5
+ // ─── Email profile resolution ───────────────────────────────
6
+ // Maps profile names to { apiKey, from } using operator config + env keys.
7
+
8
+ export type EmailProfile = string;
9
+
10
+ interface ResolvedSender { apiKey: string; from: string; defaultTo: string }
11
+
12
+ export function resolveEmailProfile(
13
+ profile: EmailProfile,
14
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
15
+ ): ResolvedSender {
16
+ const cfg = operatorConfig.integrations.email.profiles[profile];
17
+ if (!cfg) throw new Error(`Unknown email profile: ${profile}`);
18
+ const keyMap: Record<string, string> = {
19
+ resendApiKey: apiKeys.resendApiKey,
20
+ resendApiKeyPersonal: apiKeys.resendApiKeyPersonal,
21
+ };
22
+ const apiKey = keyMap[cfg.keyEnvField];
23
+ if (!apiKey) throw new Error(`Missing API key for email profile "${profile}" (${cfg.keyEnvField})`);
24
+ return { apiKey, from: cfg.from, defaultTo: cfg.defaultTo };
25
+ }
26
+
27
+ function getDefaultSender(apiKeys: { resendApiKey: string; resendApiKeyPersonal: string }): ResolvedSender {
28
+ return resolveEmailProfile(operatorConfig.integrations.email.defaultProfile as EmailProfile, apiKeys);
29
+ }
30
+
31
+ interface HeartbeatCheck {
32
+ name: string;
33
+ status: 'ok' | 'warn' | 'alert';
34
+ detail: string;
35
+ }
36
+
37
+ interface AgendaItem {
38
+ id: number;
39
+ item: string;
40
+ priority: string;
41
+ context: string | null;
42
+ }
43
+
44
+ async function resendPost(apiKey: string, from: string, to: string, subject: string, html: string): Promise<void> {
45
+ let res: Response;
46
+ try {
47
+ res = await fetch('https://api.resend.com/emails', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },
50
+ body: JSON.stringify({ from, to: [to], subject, html }),
51
+ signal: AbortSignal.timeout(10_000),
52
+ });
53
+ } catch (err: unknown) {
54
+ if (err instanceof DOMException && err.name === 'TimeoutError') {
55
+ throw new Error('Resend API request timed out after 10s');
56
+ }
57
+ throw err;
58
+ }
59
+ if (!res.ok) {
60
+ const err = await res.text();
61
+ throw new Error(`Resend error ${res.status}: ${err}`);
62
+ }
63
+ }
64
+
65
+ export async function sendHeartbeatAlert(
66
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
67
+ severity: string,
68
+ summary: string,
69
+ checks: HeartbeatCheck[],
70
+ agendaItems: AgendaItem[] = [],
71
+ notifyEmail?: string,
72
+ profile?: EmailProfile,
73
+ ): Promise<void> {
74
+ const sender = profile ? resolveEmailProfile(profile, apiKeys) : getDefaultSender(apiKeys);
75
+ const severityLabel = severity.toUpperCase();
76
+ const alertChecks = checks.filter(c => c.status !== 'ok');
77
+
78
+ const checksHtml = alertChecks.map(c => {
79
+ const isInfra = c.name.startsWith('worker_');
80
+ const badge = isInfra ? '<span style="display:inline-block;background:#2dd4bf;color:#0a0a0f;font-size:9px;font-weight:700;padding:1px 4px;border-radius:2px;margin-right:6px;vertical-align:middle">INFRA</span>' : '';
81
+ return `
82
+ <tr>
83
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-family:monospace;color:${c.status === 'alert' ? '#ff6b6b' : '#ffd93d'}">${badge}${c.name}</td>
84
+ <td style="padding:6px 12px;border-bottom:1px solid #222;color:#ccc">${c.detail}</td>
85
+ </tr>`;
86
+ }).join('');
87
+
88
+ const html = `<!DOCTYPE html>
89
+ <html>
90
+ <head><meta charset="utf-8"></head>
91
+ <body style="background:#0a0a0f;color:#e0e0e0;font-family:system-ui,sans-serif;margin:0;padding:24px">
92
+ <div style="max-width:600px;margin:0 auto">
93
+ <div style="background:#1a1a2e;border:1px solid #333;border-left:4px solid ${severity === 'critical' ? '#ff4444' : severity === 'high' ? '#ff6b6b' : '#ffd93d'};border-radius:4px;padding:20px 24px;margin-bottom:20px">
94
+ <p style="margin:0 0 4px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">AEGIS Heartbeat · ${severityLabel}</p>
95
+ <p style="margin:0;font-size:18px;font-weight:600;color:#fff">${summary}</p>
96
+ </div>
97
+ ${alertChecks.length > 0 ? `
98
+ <table style="width:100%;border-collapse:collapse;background:#111;border:1px solid #222;border-radius:4px;overflow:hidden">
99
+ <thead>
100
+ <tr style="background:#1a1a2e">
101
+ <th style="padding:8px 12px;text-align:left;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">Check</th>
102
+ <th style="padding:8px 12px;text-align:left;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">Detail</th>
103
+ </tr>
104
+ </thead>
105
+ <tbody>${checksHtml}</tbody>
106
+ </table>` : ''}
107
+ ${agendaItems.length > 0 ? `
108
+ <div style="margin-top:20px">
109
+ <p style="margin:0 0 8px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">Open Agenda</p>
110
+ ${agendaItems.map(a => `
111
+ <div style="background:#111;border:1px solid #222;border-left:3px solid ${a.priority === 'high' ? '#ff6b6b' : a.priority === 'medium' ? '#ffd93d' : '#555'};border-radius:4px;padding:8px 12px;margin-bottom:6px">
112
+ <span style="font-size:11px;color:#888;font-family:monospace">#${a.id} · ${a.priority}</span>
113
+ <p style="margin:4px 0 0;font-size:13px;color:#ccc">${a.item}</p>
114
+ ${a.context ? `<p style="margin:2px 0 0;font-size:11px;color:#666">${a.context}</p>` : ''}
115
+ </div>`).join('')}
116
+ </div>` : ''}
117
+ <p style="margin:20px 0 0;font-size:12px;color:#555">aegis-web · ${new Date().toISOString()}</p>
118
+ </div>
119
+ </body>
120
+ </html>`;
121
+
122
+ await resendPost(sender.apiKey, sender.from, notifyEmail || sender.defaultTo, `[AEGIS] ${severityLabel}: ${summary}`, html);
123
+ }
124
+
125
+ // ─── Weekly memory reflection ────────────────────────────────
126
+
127
+ export async function sendMemoryReflection(
128
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
129
+ notifyEmail: string,
130
+ reflection: string,
131
+ memoryCount: number,
132
+ topics: string[],
133
+ date: string,
134
+ profile?: EmailProfile,
135
+ ): Promise<void> {
136
+ const sender = profile ? resolveEmailProfile(profile, apiKeys) : getDefaultSender(apiKeys);
137
+ // Convert markdown-ish reflection to simple HTML paragraphs
138
+ const contentHtml = reflection
139
+ .split('\n\n')
140
+ .filter(p => p.trim())
141
+ .map(p => {
142
+ // Headers
143
+ if (p.startsWith('## ')) return `<h2 style="margin:20px 0 8px;font-size:16px;font-weight:600;color:#8b8bff;font-family:'Segoe UI',system-ui,sans-serif">${p.slice(3)}</h2>`;
144
+ if (p.startsWith('### ')) return `<h3 style="margin:16px 0 6px;font-size:14px;font-weight:600;color:#3dd6c8;font-family:'Segoe UI',system-ui,sans-serif">${p.slice(4)}</h3>`;
145
+ // Bold emphasis
146
+ const formatted = p.replace(/\*\*(.+?)\*\*/g, '<strong style="color:#e0e0e0">$1</strong>');
147
+ return `<p style="margin:0 0 12px;font-size:14px;line-height:1.7;color:#b0b0c0">${formatted}</p>`;
148
+ })
149
+ .join('');
150
+
151
+ const topicPills = topics.slice(0, 12).map(t =>
152
+ `<span style="display:inline-block;background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:2px 10px;font-size:11px;color:#888;margin:2px 4px 2px 0">${t}</span>`
153
+ ).join('');
154
+
155
+ const html = `<!DOCTYPE html>
156
+ <html>
157
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
158
+ <body style="background:#0a0a0f;color:#e0e0e0;font-family:system-ui,sans-serif;margin:0;padding:24px">
159
+ <div style="max-width:640px;margin:0 auto">
160
+ <div style="background:#1a1a2e;border:1px solid #333;border-left:4px solid #8b8bff;border-radius:4px;padding:16px 20px;margin-bottom:24px">
161
+ <p style="margin:0 0 2px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">AEGIS Memory Reflection · ${date}</p>
162
+ <p style="margin:0;font-size:18px;font-weight:600;color:#fff">${memoryCount} memories examined</p>
163
+ </div>
164
+ <div style="margin-bottom:20px">${topicPills}</div>
165
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:24px 28px">
166
+ ${contentHtml}
167
+ </div>
168
+ <p style="margin:20px 0 0;font-size:11px;color:#444">aegis-web · weekly reflection · ${new Date().toISOString()}</p>
169
+ </div>
170
+ </body>
171
+ </html>`;
172
+
173
+ await resendPost(sender.apiKey, sender.from, notifyEmail, `AEGIS — Weekly Reflection [${date}]`, html);
174
+ }
175
+
176
+ // ─── Daily Digest ──────────────────────────────────────────────
177
+ // Consolidates operator notifications into one daily email.
178
+
179
+ export interface DigestTask {
180
+ id: string;
181
+ title: string;
182
+ repo: string;
183
+ status: string;
184
+ authority: string;
185
+ category: string;
186
+ exit_code: number | null;
187
+ error: string | null;
188
+ result: string | null;
189
+ pr_url: string | null;
190
+ completed_at: string | null;
191
+ }
192
+
193
+ export interface DigestHealthCheck {
194
+ severity: string;
195
+ checks: Array<{ name: string; status: string; detail: string }>;
196
+ timestamp: string;
197
+ }
198
+
199
+ export interface DigestAgendaItem {
200
+ id: number;
201
+ item: string;
202
+ priority: string;
203
+ context: string | null;
204
+ created_at?: string;
205
+ }
206
+
207
+ export interface DigestEventNotification {
208
+ source: string;
209
+ event_type: string;
210
+ summary: string;
211
+ priority: string;
212
+ ts: string;
213
+ }
214
+
215
+ export interface DigestServiceAlert {
216
+ source: string;
217
+ severity: string;
218
+ summary: string;
219
+ detail: string;
220
+ findingsCount: number;
221
+ }
222
+
223
+ export interface DigestSections {
224
+ completedTasks: DigestTask[];
225
+ failedTasks: DigestTask[];
226
+ proposedTasks: DigestTask[];
227
+ operatorLog: string | null;
228
+ healthChecks: DigestHealthCheck[];
229
+ eventNotifications: DigestEventNotification[];
230
+ memoryReflection: string | null;
231
+ cognitiveMetrics: {
232
+ cognitive_score: number;
233
+ score_delta: number;
234
+ dispatch_success_rate_7d: number;
235
+ procedure_convergence_rate: number;
236
+ task_success_rate_7d: number;
237
+ tasks_completed_7d: number;
238
+ tasks_failed_7d: number;
239
+ avg_cost_7d: number;
240
+ avg_cost_prior_7d: number;
241
+ memory_count: number;
242
+ top_failure_kind: string | null;
243
+ } | null;
244
+ analytics: {
245
+ sessions_7d: number;
246
+ sessions_prior_7d: number;
247
+ users_7d: number;
248
+ bounce_rate_7d: number;
249
+ top_pages: Array<{ path: string; sessions: number; bounce_rate: number }>;
250
+ top_sources: Array<{ source: string; medium: string; sessions: number }>;
251
+ insights: string[];
252
+ } | null;
253
+ devActivity: {
254
+ keys_created_24h: number;
255
+ keys_created_7d: number;
256
+ keys_created_all_time: number;
257
+ keys_active_24h: number;
258
+ keys_active_7d: number;
259
+ tier_breakdown: Array<{ tier: string; count: number }>;
260
+ recent_signups: Array<{ email: string; name: string; tier: string; created_at: string }>;
261
+ total_users: number;
262
+ total_tenants: number;
263
+ } | null;
264
+ serviceAlerts: DigestServiceAlert[];
265
+ agendaItems: DigestAgendaItem[];
266
+ bizopsInteractions: number | null;
267
+ }
268
+
269
+ export async function sendDailyDigest(
270
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
271
+ sections: DigestSections,
272
+ notifyEmail?: string,
273
+ profile?: EmailProfile,
274
+ ): Promise<void> {
275
+ const sender = profile ? resolveEmailProfile(profile, apiKeys) : getDefaultSender(apiKeys);
276
+ const date = new Date().toISOString().slice(0, 10);
277
+
278
+ const statusBadge = (s: string) => {
279
+ const colors: Record<string, string> = { completed: '#2dd4bf', failed: '#ff6b6b', pending: '#ffd93d' };
280
+ return `<span style="display:inline-block;background:${colors[s] ?? '#555'};color:#0a0a0f;font-size:9px;font-weight:700;padding:1px 4px;border-radius:2px;margin-right:4px">${s.toUpperCase()}</span>`;
281
+ };
282
+
283
+ // ── Section 1: WORK SHIPPED ──
284
+ let workShippedHtml = '';
285
+ if (sections.failedTasks.length > 0 || sections.completedTasks.length > 0) {
286
+ const taskRow = (t: DigestTask) => {
287
+ const pr = t.pr_url ? ` <a href="${t.pr_url}" style="color:#8b8bff;text-decoration:none;font-size:11px">PR →</a>` : '';
288
+ const errorSnippet = t.error ? `<p style="margin:2px 0 0;font-size:11px;color:#ff6b6b">${t.error.slice(0, 100)}</p>` : '';
289
+ return `
290
+ <div style="background:#111;border:1px solid #222;border-left:3px solid ${t.status === 'completed' ? '#2dd4bf' : '#ff6b6b'};border-radius:4px;padding:8px 12px;margin-bottom:6px">
291
+ <div>${statusBadge(t.status)}<span style="font-size:11px;color:#888;font-family:monospace">${t.repo} · ${t.category}</span>${pr}</div>
292
+ <p style="margin:4px 0 0;font-size:13px;color:#ccc">${t.title}</p>
293
+ ${errorSnippet}
294
+ </div>`;
295
+ };
296
+
297
+ workShippedHtml = `
298
+ <div style="margin-bottom:24px">
299
+ <p style="margin:0 0 10px;font-size:11px;color:#2dd4bf;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Work Shipped</p>
300
+ ${sections.failedTasks.map(taskRow).join('')}
301
+ ${sections.completedTasks.map(taskRow).join('')}
302
+ <p style="margin:8px 0 0;font-size:12px;color:#666">${sections.completedTasks.length} completed · ${sections.failedTasks.length} failed</p>
303
+ </div>`;
304
+ }
305
+
306
+ // ── Section 2: OPERATOR'S LOG ──
307
+ let operatorLogHtml = '';
308
+ if (sections.operatorLog) {
309
+ const contentHtml = sections.operatorLog
310
+ .split('\n\n')
311
+ .filter(p => p.trim())
312
+ .map(p => {
313
+ if (p.startsWith('## ')) return `<h2 style="margin:16px 0 6px;font-size:15px;font-weight:600;color:#8b8bff">${p.slice(3)}</h2>`;
314
+ if (p.startsWith('### ')) return `<h3 style="margin:12px 0 4px;font-size:13px;font-weight:600;color:#3dd6c8">${p.slice(4)}</h3>`;
315
+ const formatted = p.replace(/\*\*(.+?)\*\*/g, '<strong style="color:#e0e0e0">$1</strong>');
316
+ return `<p style="margin:0 0 10px;font-size:13px;line-height:1.65;color:#b0b0c0">${formatted}</p>`;
317
+ })
318
+ .join('');
319
+
320
+ operatorLogHtml = `
321
+ <div style="margin-bottom:24px">
322
+ <p style="margin:0 0 10px;font-size:11px;color:#3dd6c8;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Operator's Log</p>
323
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:16px 20px">
324
+ ${contentHtml}
325
+ </div>
326
+ </div>`;
327
+ }
328
+
329
+ // ── Section 3: SYSTEM HEALTH ──
330
+ let healthHtml = '';
331
+ if (sections.healthChecks.length > 0) {
332
+ const allChecks = sections.healthChecks.flatMap(h => h.checks.map(c => ({ ...c, severity: h.severity })));
333
+ const checksRows = allChecks.map(c => {
334
+ const color = c.status === 'alert' ? '#ff6b6b' : '#ffd93d';
335
+ return `
336
+ <tr>
337
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-family:monospace;color:${color}">${c.name}</td>
338
+ <td style="padding:6px 12px;border-bottom:1px solid #222;color:#ccc">${c.detail}</td>
339
+ </tr>`;
340
+ }).join('');
341
+
342
+ healthHtml = `
343
+ <div style="margin-bottom:24px">
344
+ <p style="margin:0 0 10px;font-size:11px;color:#ffd93d;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">System Health</p>
345
+ <table style="width:100%;border-collapse:collapse;background:#111;border:1px solid #222;border-radius:4px;overflow:hidden">
346
+ <thead>
347
+ <tr style="background:#1a1a2e">
348
+ <th style="padding:8px 12px;text-align:left;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">Check</th>
349
+ <th style="padding:8px 12px;text-align:left;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">Detail</th>
350
+ </tr>
351
+ </thead>
352
+ <tbody>${checksRows}</tbody>
353
+ </table>
354
+ </div>`;
355
+ }
356
+
357
+ // ── Section 3b: ARGUS EVENTS ──
358
+ let eventsHtml = '';
359
+ if (sections.eventNotifications.length > 0) {
360
+ const eventRows = sections.eventNotifications.map(e => {
361
+ const color = e.priority === 'high' ? '#f5a623' : '#888';
362
+ return `
363
+ <tr>
364
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-size:11px;color:#666">${e.source}</td>
365
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-family:monospace;color:${color};font-size:12px">${e.event_type}</td>
366
+ <td style="padding:6px 12px;border-bottom:1px solid #222;color:#ccc;font-size:12px">${e.summary}</td>
367
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-size:11px;color:#555">${e.ts.slice(0, 16)}</td>
368
+ </tr>`;
369
+ }).join('');
370
+
371
+ eventsHtml = `
372
+ <div style="margin-bottom:24px">
373
+ <p style="margin:0 0 10px;font-size:11px;color:#7b7bdf;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">ARGUS Events (${sections.eventNotifications.length})</p>
374
+ <table style="width:100%;border-collapse:collapse;background:#111;border:1px solid #222;border-radius:4px;overflow:hidden">
375
+ <thead>
376
+ <tr style="background:#1a1a2e">
377
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Source</th>
378
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Event</th>
379
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Summary</th>
380
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Time</th>
381
+ </tr>
382
+ </thead>
383
+ <tbody>${eventRows}</tbody>
384
+ </table>
385
+ </div>`;
386
+ }
387
+
388
+ // ── Section 3c: COGNITIVE SCORECARD ──
389
+ let metricsHtml = '';
390
+ if (sections.cognitiveMetrics) {
391
+ const m = sections.cognitiveMetrics;
392
+ const arrow = m.score_delta > 0 ? '&#9650;' : m.score_delta < 0 ? '&#9660;' : '&#9644;';
393
+ const arrowColor = m.score_delta > 0 ? '#2dd4a0' : m.score_delta < 0 ? '#ef4444' : '#888';
394
+ const scoreColor = m.cognitive_score >= 75 ? '#2dd4a0' : m.cognitive_score >= 50 ? '#f5a623' : '#ef4444';
395
+ const costDelta = m.avg_cost_prior_7d > 0
396
+ ? ((m.avg_cost_7d - m.avg_cost_prior_7d) / m.avg_cost_prior_7d * 100).toFixed(0)
397
+ : '0';
398
+ const costArrow = Number(costDelta) <= 0 ? '#2dd4a0' : '#ef4444';
399
+
400
+ metricsHtml = `
401
+ <div style="margin-bottom:24px">
402
+ <p style="margin:0 0 10px;font-size:11px;color:#f5a623;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Cognitive Scorecard</p>
403
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:16px 20px">
404
+ <div style="display:flex;align-items:baseline;gap:12px;margin-bottom:12px">
405
+ <span style="font-size:36px;font-weight:700;color:${scoreColor};line-height:1">${m.cognitive_score}</span>
406
+ <span style="font-size:14px;color:${arrowColor}">${arrow} ${Math.abs(m.score_delta)}</span>
407
+ <span style="font-size:12px;color:#666">/100</span>
408
+ </div>
409
+ <table style="width:100%;border-collapse:collapse">
410
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Dispatch success</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${Math.round(m.dispatch_success_rate_7d * 100)}%</td></tr>
411
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Procedures learned</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${Math.round(m.procedure_convergence_rate * 100)}%</td></tr>
412
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Tasks shipped (7d)</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${m.tasks_completed_7d} / ${m.tasks_completed_7d + m.tasks_failed_7d}</td></tr>
413
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Avg cost/dispatch</td><td style="padding:3px 0;font-size:12px;color:${costArrow};text-align:right">$${m.avg_cost_7d.toFixed(4)} (${Number(costDelta) <= 0 ? '' : '+'}${costDelta}%)</td></tr>
414
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Memory entries</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${m.memory_count}</td></tr>
415
+ ${m.top_failure_kind ? `<tr><td style="padding:3px 0;font-size:11px;color:#888">Top failure mode</td><td style="padding:3px 0;font-size:12px;color:#ef4444;text-align:right">${m.top_failure_kind}</td></tr>` : ''}
416
+ </table>
417
+ </div>
418
+ </div>`;
419
+ }
420
+
421
+ // ── Section 3c2: ANALYTICS ──
422
+ let analyticsHtml = '';
423
+ if (sections.analytics && sections.analytics.sessions_7d > 0) {
424
+ const a = sections.analytics;
425
+ const trafficDelta = a.sessions_prior_7d > 0
426
+ ? Math.round((a.sessions_7d - a.sessions_prior_7d) / a.sessions_prior_7d * 100)
427
+ : 0;
428
+ const trafficArrow = trafficDelta > 0 ? '&#9650;' : trafficDelta < 0 ? '&#9660;' : '&#9644;';
429
+ const trafficColor = trafficDelta > 0 ? '#2dd4a0' : trafficDelta < 0 ? '#ef4444' : '#888';
430
+
431
+ const pagesRows = a.top_pages.slice(0, 5).map(p => `
432
+ <tr>
433
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222;font-family:monospace">${p.path}</td>
434
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222;text-align:right">${p.sessions}</td>
435
+ <td style="padding:4px 12px;font-size:12px;color:${p.bounce_rate > 0.85 ? '#ef4444' : '#888'};border-bottom:1px solid #222;text-align:right">${(p.bounce_rate * 100).toFixed(0)}%</td>
436
+ </tr>`).join('');
437
+
438
+ const sourcesRows = a.top_sources.slice(0, 5).map(s => `
439
+ <tr>
440
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222">${s.source}</td>
441
+ <td style="padding:4px 12px;font-size:12px;color:#888;border-bottom:1px solid #222">${s.medium}</td>
442
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222;text-align:right">${s.sessions}</td>
443
+ </tr>`).join('');
444
+
445
+ const insightsHtml = a.insights.length > 0
446
+ ? `<div style="margin-top:12px;background:#0b1a1a;border:1px solid rgba(61,214,200,0.15);border-radius:4px;padding:10px 14px">
447
+ <ul style="margin:0;padding-left:16px">${a.insights.map(i => `<li style="margin-bottom:4px;font-size:12px;color:#ccc">${i}</li>`).join('')}</ul>
448
+ </div>`
449
+ : '';
450
+
451
+ analyticsHtml = `
452
+ <div style="margin-bottom:24px">
453
+ <p style="margin:0 0 10px;font-size:11px;color:#8b8bff;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Traffic (GA4)</p>
454
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:16px 20px">
455
+ <div style="display:flex;gap:24px;margin-bottom:12px">
456
+ <div><span style="font-size:24px;font-weight:700;color:#ccc">${a.sessions_7d}</span><span style="font-size:12px;color:#666"> sessions</span></div>
457
+ <div><span style="font-size:24px;font-weight:700;color:#ccc">${a.users_7d}</span><span style="font-size:12px;color:#666"> users</span></div>
458
+ <div><span style="font-size:14px;color:${trafficColor}">${trafficArrow} ${Math.abs(trafficDelta)}%</span><span style="font-size:12px;color:#666"> vs prior week</span></div>
459
+ </div>
460
+ ${pagesRows ? `<table style="width:100%;border-collapse:collapse;margin-bottom:8px">
461
+ <thead><tr>
462
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Page</th>
463
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:right;text-transform:uppercase">Sessions</th>
464
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:right;text-transform:uppercase">Bounce</th>
465
+ </tr></thead><tbody>${pagesRows}</tbody></table>` : ''}
466
+ ${sourcesRows ? `<table style="width:100%;border-collapse:collapse">
467
+ <thead><tr>
468
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Source</th>
469
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Medium</th>
470
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:right;text-transform:uppercase">Sessions</th>
471
+ </tr></thead><tbody>${sourcesRows}</tbody></table>` : ''}
472
+ ${insightsHtml}
473
+ </div>
474
+ </div>`;
475
+ }
476
+
477
+ // ── Section 3c3: DEVELOPER ACTIVITY ──
478
+ let devActivityHtml = '';
479
+ if (sections.devActivity) {
480
+ const d = sections.devActivity;
481
+ const tierBadges = d.tier_breakdown.map(t => {
482
+ const colors: Record<string, string> = { free: '#888', hobby: '#3dd6c8', pro: '#8b8bff', enterprise: '#f5a623' };
483
+ return `<span style="display:inline-block;background:${colors[t.tier] ?? '#555'};color:#0a0a0f;font-size:10px;font-weight:700;padding:2px 6px;border-radius:2px;margin-right:4px">${t.tier} ${t.count}</span>`;
484
+ }).join('');
485
+
486
+ const signupRows = d.recent_signups.slice(0, 10).map(s => `
487
+ <tr>
488
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222">${s.name || '(no name)'}</td>
489
+ <td style="padding:4px 12px;font-size:12px;color:#888;border-bottom:1px solid #222">${s.email}</td>
490
+ <td style="padding:4px 12px;font-size:12px;color:#ccc;border-bottom:1px solid #222">${s.tier}</td>
491
+ <td style="padding:4px 12px;font-size:11px;color:#666;border-bottom:1px solid #222">${s.created_at.slice(0, 10)}</td>
492
+ </tr>`).join('');
493
+
494
+ devActivityHtml = `
495
+ <div style="margin-bottom:24px">
496
+ <p style="margin:0 0 10px;font-size:11px;color:#2dd4bf;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Developer Activity</p>
497
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:16px 20px">
498
+ <div style="display:flex;gap:24px;margin-bottom:12px">
499
+ <div><span style="font-size:24px;font-weight:700;color:#ccc">${d.total_users}</span><span style="font-size:12px;color:#666"> users</span></div>
500
+ <div><span style="font-size:24px;font-weight:700;color:#ccc">${d.total_tenants}</span><span style="font-size:12px;color:#666"> tenants</span></div>
501
+ <div><span style="font-size:24px;font-weight:700;color:${d.keys_active_24h > 0 ? '#2dd4a0' : '#888'}">${d.keys_active_24h}</span><span style="font-size:12px;color:#666"> active (24h)</span></div>
502
+ </div>
503
+ <table style="width:100%;border-collapse:collapse;margin-bottom:12px">
504
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Keys created (24h)</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${d.keys_created_24h}</td></tr>
505
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Keys created (7d)</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${d.keys_created_7d}</td></tr>
506
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Keys created (all-time)</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${d.keys_created_all_time}</td></tr>
507
+ <tr><td style="padding:3px 0;font-size:11px;color:#888">Active keys (7d)</td><td style="padding:3px 0;font-size:12px;color:#ccc;text-align:right">${d.keys_active_7d}</td></tr>
508
+ </table>
509
+ <div style="margin-bottom:12px">${tierBadges || '<span style="font-size:11px;color:#555">No active keys</span>'}</div>
510
+ ${signupRows ? `<p style="margin:12px 0 6px;font-size:10px;color:#666;text-transform:uppercase;letter-spacing:1px">Recent Signups (7d)</p>
511
+ <table style="width:100%;border-collapse:collapse">
512
+ <thead><tr>
513
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Name</th>
514
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Email</th>
515
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Tier</th>
516
+ <th style="padding:4px 12px;font-size:10px;color:#666;text-align:left;text-transform:uppercase">Joined</th>
517
+ </tr></thead><tbody>${signupRows}</tbody></table>` : ''}
518
+ </div>
519
+ </div>`;
520
+ }
521
+
522
+ // ── Section 3b2: SERVICE ALERTS ──
523
+ let serviceAlertsHtml = '';
524
+ if (sections.serviceAlerts.length > 0) {
525
+ const sevColor = (s: string) => s === 'critical' ? '#ef4444' : s === 'high' ? '#f5a623' : s === 'medium' ? '#ffd93d' : '#888';
526
+ const alertRows = sections.serviceAlerts.map(a => `
527
+ <tr>
528
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-size:12px;color:${sevColor(a.severity)};font-weight:600">${a.severity.toUpperCase()}</td>
529
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-size:11px;color:#888;font-family:monospace">${a.source}</td>
530
+ <td style="padding:6px 12px;border-bottom:1px solid #222;font-size:12px;color:#ccc">${a.summary}${a.findingsCount > 0 ? ` <span style="color:#888">(${a.findingsCount} findings)</span>` : ''}</td>
531
+ </tr>`).join('');
532
+
533
+ serviceAlertsHtml = `
534
+ <div style="margin-bottom:24px">
535
+ <p style="margin:0 0 10px;font-size:11px;color:#f5a623;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Service Alerts (${sections.serviceAlerts.length})</p>
536
+ <table style="width:100%;border-collapse:collapse;background:#111;border:1px solid #222;border-radius:4px;overflow:hidden">
537
+ <thead>
538
+ <tr style="background:#1a1a2e">
539
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Severity</th>
540
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Source</th>
541
+ <th style="padding:8px 12px;text-align:left;font-size:10px;color:#666;text-transform:uppercase">Summary</th>
542
+ </tr>
543
+ </thead>
544
+ <tbody>${alertRows}</tbody>
545
+ </table>
546
+ </div>`;
547
+ }
548
+
549
+ // ── Section 3d: CO-FOUNDER'S TAKE ──
550
+ // Opinionated synthesis: what matters, what's off track, what nobody's working on
551
+ let cofounderHtml = '';
552
+ {
553
+ const takes: string[] = [];
554
+
555
+ // Revenue signal
556
+ const hasRevenue = sections.eventNotifications.some(e => e.event_type.includes('payment') || e.event_type.includes('checkout') || e.event_type.includes('invoice.paid'));
557
+ const hasFailedPayment = sections.eventNotifications.some(e => e.event_type.includes('failed'));
558
+ if (hasRevenue && !hasFailedPayment) {
559
+ takes.push('Revenue is flowing. Keep shipping.');
560
+ } else if (hasFailedPayment) {
561
+ takes.push('Payment failures detected. Check Stripe dashboard before anything else today.');
562
+ }
563
+
564
+ // Task health
565
+ const failRate = sections.failedTasks.length / Math.max(1, sections.completedTasks.length + sections.failedTasks.length);
566
+ if (failRate > 0.4 && sections.failedTasks.length >= 3) {
567
+ takes.push(`Task failure rate is ${Math.round(failRate * 100)}% — the taskrunner is burning cycles. Review failed tasks before queuing more.`);
568
+ }
569
+
570
+ // Stale proposals
571
+ const staleProposals = sections.agendaItems.filter(a => {
572
+ if (!a.item.startsWith('[PROPOSED ACTION]') || !a.created_at) return false;
573
+ const ageDays = (Date.now() - new Date(a.created_at).getTime()) / 86_400_000;
574
+ return ageDays > 4;
575
+ });
576
+ if (staleProposals.length > 0) {
577
+ takes.push(`${staleProposals.length} proposed action${staleProposals.length > 1 ? 's' : ''} aging out. Approve or dismiss — stale proposals mean I'm doing work you're not reviewing.`);
578
+ }
579
+
580
+ // High-priority agenda items piling up
581
+ const highItems = sections.agendaItems.filter(a => a.priority === 'high' && !a.item.startsWith('[PROPOSED'));
582
+ if (highItems.length >= 4) {
583
+ takes.push(`${highItems.length} high-priority agenda items. That's too many "high" items — either some aren't really high, or we need a focused triage session.`);
584
+ }
585
+
586
+ // Nothing shipped
587
+ if (sections.completedTasks.length === 0 && sections.failedTasks.length === 0) {
588
+ takes.push('No tasks ran in the last 24h. Is the taskrunner down, or is this intentional?');
589
+ }
590
+
591
+ // Health checks surfacing
592
+ if (sections.healthChecks.length > 0) {
593
+ const critChecks = sections.healthChecks.filter(h => h.severity === 'critical');
594
+ if (critChecks.length > 0) {
595
+ takes.push('Critical health checks in the system. This takes priority over feature work.');
596
+ }
597
+ }
598
+
599
+ // Service alerts
600
+ const critAlerts = sections.serviceAlerts.filter(a => a.severity === 'critical');
601
+ if (critAlerts.length > 0) {
602
+ takes.push(`${critAlerts.length} critical service alert${critAlerts.length > 1 ? 's' : ''} from ${[...new Set(critAlerts.map(a => a.source))].join(', ')}. Review immediately.`);
603
+ }
604
+
605
+ // Developer activity signals
606
+ if (sections.devActivity) {
607
+ if (sections.devActivity.recent_signups.length > 0) {
608
+ takes.push(`${sections.devActivity.recent_signups.length} new signup${sections.devActivity.recent_signups.length !== 1 ? 's' : ''} this week. People are finding us.`);
609
+ }
610
+ if (sections.devActivity.keys_active_24h === 0 && sections.devActivity.keys_created_all_time > 0) {
611
+ takes.push('No active API keys in the last 24h. Users signed up but aren\'t hitting endpoints — check onboarding friction.');
612
+ }
613
+ }
614
+
615
+ // Memory reflection available
616
+ if (sections.memoryReflection) {
617
+ takes.push('Weekly reflection attached below — worth a 2-minute read to see what I\'m learning.');
618
+ }
619
+
620
+ if (takes.length > 0) {
621
+ const takesHtml = takes.map(t => `<li style="margin-bottom:6px;font-size:13px;line-height:1.5;color:#ccc">${t}</li>`).join('');
622
+ cofounderHtml = `
623
+ <div style="margin-bottom:24px">
624
+ <p style="margin:0 0 10px;font-size:11px;color:#3dd6c8;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Co-Founder's Take</p>
625
+ <div style="background:#0b1a1a;border:1px solid rgba(61,214,200,0.15);border-left:3px solid #3dd6c8;border-radius:4px;padding:12px 16px">
626
+ <ul style="margin:0;padding-left:18px">${takesHtml}</ul>
627
+ </div>
628
+ </div>`;
629
+ }
630
+ }
631
+
632
+ // ── Section 3d: MEMORY REFLECTION ──
633
+ let reflectionHtml = '';
634
+ if (sections.memoryReflection) {
635
+ const formatted = sections.memoryReflection
636
+ .split('\n\n')
637
+ .filter(p => p.trim())
638
+ .map(p => `<p style="margin:0 0 8px;font-size:12px;line-height:1.6;color:#b0b0c0">${p.replace(/\*\*(.+?)\*\*/g, '<strong style="color:#e0e0e0">$1</strong>')}</p>`)
639
+ .join('');
640
+ reflectionHtml = `
641
+ <div style="margin-bottom:24px">
642
+ <p style="margin:0 0 10px;font-size:11px;color:#a855f7;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Weekly Reflection</p>
643
+ <div style="background:#111;border:1px solid #222;border-radius:6px;padding:16px 20px">${formatted}</div>
644
+ </div>`;
645
+ }
646
+
647
+ // ── Section 4: AWAITING ACTION ──
648
+ let awaitingHtml = '';
649
+ // Split agenda into proposed actions vs regular high-priority items
650
+ const proposedAgenda = sections.agendaItems.filter(a => a.item.startsWith('[PROPOSED ACTION]'));
651
+ const highAgenda = sections.agendaItems.filter(a => a.priority === 'high' && !a.item.startsWith('[PROPOSED ACTION]'));
652
+ if (sections.proposedTasks.length > 0 || proposedAgenda.length > 0 || highAgenda.length > 0) {
653
+ const proposedTaskRows = sections.proposedTasks.map(t => `
654
+ <div style="background:#111;border:1px solid #222;border-left:3px solid #a855f7;border-radius:4px;padding:8px 12px;margin-bottom:6px">
655
+ <div>${statusBadge('pending')}<span style="font-size:11px;color:#888;font-family:monospace">${t.repo} · ${t.category}</span></div>
656
+ <p style="margin:4px 0 0;font-size:13px;color:#ccc">${t.title}</p>
657
+ <p style="margin:4px 0 0;font-size:11px;color:#a855f7">Awaiting approval · ID: ${t.id.slice(0, 8)}</p>
658
+ </div>`).join('');
659
+
660
+ // Proposed action agenda items with expiry countdown (auto-expire at 7d)
661
+ const proposedAgendaRows = proposedAgenda.map(a => {
662
+ const ageDays = Math.floor((Date.now() - new Date(a.created_at ?? Date.now()).getTime()) / 86_400_000);
663
+ const daysLeft = Math.max(0, 7 - ageDays);
664
+ const expiryNote = daysLeft <= 2 ? `<span style="color:#ff6b6b"> expires in ${daysLeft}d</span>` : `<span style="color:#888"> ${daysLeft}d until auto-expire</span>`;
665
+ return `
666
+ <div style="background:#111;border:1px solid #222;border-left:3px solid #a855f7;border-radius:4px;padding:8px 12px;margin-bottom:6px">
667
+ <span style="font-size:11px;color:#888;font-family:monospace">#${a.id} · proposed${expiryNote}</span>
668
+ <p style="margin:4px 0 0;font-size:13px;color:#ccc">${a.item.replace('[PROPOSED ACTION] ', '')}</p>
669
+ </div>`;
670
+ }).join('');
671
+
672
+ const agendaRows = highAgenda.map(a => `
673
+ <div style="background:#111;border:1px solid #222;border-left:3px solid #ff6b6b;border-radius:4px;padding:8px 12px;margin-bottom:6px">
674
+ <span style="font-size:11px;color:#888;font-family:monospace">#${a.id} · ${a.priority}</span>
675
+ <p style="margin:4px 0 0;font-size:13px;color:#ccc">${a.item}</p>
676
+ </div>`).join('');
677
+
678
+ // Review prompt only when proposals exist
679
+ const reviewPrompt = proposedAgenda.length > 0
680
+ ? `<p style="margin:8px 0 0;font-size:11px;color:#a855f7">${proposedAgenda.length} proposed action${proposedAgenda.length !== 1 ? 's' : ''} awaiting review. Unapproved proposals auto-expire after 7 days.</p>`
681
+ : '';
682
+
683
+ awaitingHtml = `
684
+ <div style="margin-bottom:24px">
685
+ <p style="margin:0 0 10px;font-size:11px;color:#a855f7;text-transform:uppercase;letter-spacing:1px;border-bottom:1px solid #222;padding-bottom:6px">Awaiting Action</p>
686
+ ${proposedTaskRows}
687
+ ${proposedAgendaRows}
688
+ ${agendaRows}
689
+ ${reviewPrompt}
690
+ </div>`;
691
+ }
692
+
693
+ // ── Section 5: STATS BAR ──
694
+ const stats = [
695
+ `${sections.completedTasks.length + sections.failedTasks.length} tasks`,
696
+ `${sections.proposedTasks.length} proposed`,
697
+ `${sections.agendaItems.length} agenda`,
698
+ `${sections.healthChecks.length} health checks`,
699
+ ];
700
+ if (sections.eventNotifications.length > 0) stats.push(`${sections.eventNotifications.length} events`);
701
+ if (sections.serviceAlerts.length > 0) stats.push(`${sections.serviceAlerts.length} alerts`);
702
+ if (sections.devActivity) stats.push(`${sections.devActivity.total_users} users`);
703
+ if (sections.bizopsInteractions !== null) stats.push(`${sections.bizopsInteractions} interactions`);
704
+
705
+ const statsHtml = `<p style="margin:0;padding:12px 0;font-size:11px;color:#555;border-top:1px solid #222;text-align:center">${stats.join(' · ')}</p>`;
706
+
707
+ const hasContent = metricsHtml || cofounderHtml || workShippedHtml || operatorLogHtml || healthHtml || eventsHtml || serviceAlertsHtml || analyticsHtml || devActivityHtml || reflectionHtml || awaitingHtml;
708
+
709
+ const html = `<!DOCTYPE html>
710
+ <html>
711
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
712
+ <body style="background:#0a0a0f;color:#e0e0e0;font-family:system-ui,sans-serif;margin:0;padding:24px">
713
+ <div style="max-width:600px;margin:0 auto">
714
+ <div style="background:#1a1a2e;border:1px solid #333;border-left:4px solid #3dd6c8;border-radius:4px;padding:16px 20px;margin-bottom:24px">
715
+ <p style="margin:0 0 2px;font-size:11px;color:#888;text-transform:uppercase;letter-spacing:1px">AEGIS — Co-Founder Brief</p>
716
+ <p style="margin:0;font-size:18px;font-weight:600;color:#fff">${date}</p>
717
+ </div>
718
+ ${hasContent ? `${metricsHtml}${cofounderHtml}${workShippedHtml}${operatorLogHtml}${healthHtml}${eventsHtml}${serviceAlertsHtml}${analyticsHtml}${devActivityHtml}${reflectionHtml}${awaitingHtml}` : '<p style="font-size:13px;color:#888;margin-bottom:20px">No significant activity in the last 24 hours.</p>'}
719
+ ${statsHtml}
720
+ <p style="margin:12px 0 0;font-size:11px;color:#444">aegis-web · daily digest · ${new Date().toISOString()}</p>
721
+ </div>
722
+ </body>
723
+ </html>`;
724
+
725
+ await resendPost(sender.apiKey, sender.from, notifyEmail || sender.defaultTo, `[AEGIS] Co-Founder Brief — ${date}`, html);
726
+ }
727
+
728
+ // ─── Operator's Log Email ───────────────────────────────────
729
+ // Standalone nightly worklog email at midnight CT (05:00 UTC).
730
+
731
+ export async function sendOperatorLog(
732
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
733
+ to: string,
734
+ logContent: string,
735
+ stats: { episodes: number; goals: number; tasksCompleted: number; tasksFailed: number; prs: number },
736
+ ): Promise<void> {
737
+ const sender = getDefaultSender(apiKeys);
738
+ const date = new Date().toISOString().slice(0, 10);
739
+
740
+ const taskLine = stats.tasksCompleted > 0 || stats.tasksFailed > 0
741
+ ? ` | ${stats.tasksCompleted} shipped${stats.tasksFailed > 0 ? `, ${stats.tasksFailed} failed` : ''}${stats.prs > 0 ? `, ${stats.prs} PRs` : ''}`
742
+ : '';
743
+ const subject = `[AEGIS] Operator's Log — ${date}${taskLine}`;
744
+
745
+ const contentHtml = logContent
746
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
747
+ .replace(/## (.+)/g, '<h3 style="font-size:15px;color:#c8c8d8;margin:20px 0 8px;border-bottom:1px solid #222;padding-bottom:4px">$1</h3>')
748
+ .replace(/\*\*(.+?)\*\*/g, '<strong style="color:#e0e0e0">$1</strong>')
749
+ .replace(/✓/g, '<span style="color:#2dd4a0">✓</span>')
750
+ .replace(/✗/g, '<span style="color:#ef4444">✗</span>')
751
+ .replace(/\n/g, '<br>');
752
+
753
+ const html = `<!DOCTYPE html><html><head><meta charset="utf-8"></head>
754
+ <body style="background:#0a0a0f;margin:0;padding:32px 16px;font-family:'Courier New',monospace">
755
+ <div style="max-width:640px;margin:0 auto">
756
+ <div style="border-bottom:1px solid #222;padding-bottom:12px;margin-bottom:20px">
757
+ <span style="font-size:11px;color:#7b7bdf;text-transform:uppercase;letter-spacing:0.1em">AEGIS Operator's Log</span>
758
+ <span style="float:right;font-size:11px;color:#555">${date}</span>
759
+ </div>
760
+ <div style="font-size:13px;line-height:1.7;color:#a0a0b0">${contentHtml}</div>
761
+ <div style="margin-top:24px;padding-top:12px;border-top:1px solid #222;font-size:11px;color:#444">
762
+ ${stats.episodes} dispatches | ${stats.goals} goal runs | ${stats.tasksCompleted + stats.tasksFailed} tasks
763
+ </div>
764
+ </div>
765
+ </body></html>`;
766
+
767
+ await resendPost(sender.apiKey, sender.from, to, subject, html);
768
+ }
769
+
770
+ // ─── Welcome Email ──────────────────────────────────────────
771
+ // Sent when a new user signs up via the Stackbilt platform.
772
+
773
+ export async function sendWelcomeEmail(
774
+ apiKeys: { resendApiKey: string; resendApiKeyPersonal: string },
775
+ recipientEmail: string,
776
+ recipientName?: string,
777
+ ): Promise<void> {
778
+ const sender = resolveEmailProfile(operatorConfig.integrations.email.defaultProfile, apiKeys);
779
+ const displayName = recipientName || recipientEmail.split('@')[0];
780
+
781
+ const html = `<!DOCTYPE html>
782
+ <html>
783
+ <head>
784
+ <meta charset="utf-8">
785
+ <meta name="viewport" content="width=device-width,initial-scale=1">
786
+ <link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display&family=Outfit:wght@400;500;600&display=swap" rel="stylesheet">
787
+ </head>
788
+ <body style="background:#0a0a0f;margin:0;padding:32px 16px;font-family:'Outfit',system-ui,sans-serif">
789
+ <div style="max-width:560px;margin:0 auto">
790
+
791
+ <!-- Header -->
792
+ <div style="text-align:center;margin-bottom:32px">
793
+ <h1 style="font-family:'DM Serif Display',serif;font-size:28px;color:#c4956a;margin:0 0 4px;font-weight:400">${operatorConfig.identity.name || 'AEGIS'}</h1>
794
+ <p style="font-size:13px;color:#666;margin:0">${operatorConfig.persona.tagline || ''}</p>
795
+ </div>
796
+
797
+ <!-- Body -->
798
+ <div style="background:#111;border:1px solid #222;border-radius:8px;padding:32px">
799
+ <h2 style="font-family:'DM Serif Display',serif;font-size:22px;color:#e0e0e0;margin:0 0 16px;font-weight:400">Welcome, ${displayName}</h2>
800
+
801
+ <p style="font-size:15px;line-height:1.7;color:#b0b0c0;margin:0 0 20px">
802
+ Your account is live. Here's what you can do right now:
803
+ </p>
804
+
805
+ <div style="margin-bottom:24px">
806
+ <div style="display:flex;margin-bottom:12px">
807
+ <span style="color:#c4956a;font-size:14px;margin-right:10px;line-height:1.6">&#9670;</span>
808
+ <div>
809
+ <p style="font-size:14px;color:#e0e0e0;margin:0"><strong>img-forge</strong> — AI image generation via MCP</p>
810
+ <p style="font-size:13px;color:#888;margin:4px 0 0">Generate images from any AI tool that supports MCP servers.</p>
811
+ </div>
812
+ </div>
813
+ <div style="display:flex;margin-bottom:12px">
814
+ <span style="color:#c4956a;font-size:14px;margin-right:10px;line-height:1.6">&#9670;</span>
815
+ <div>
816
+ <p style="font-size:14px;color:#e0e0e0;margin:0"><strong>Scaffold</strong> — Architecture scaffolding</p>
817
+ <p style="font-size:13px;color:#888;margin:4px 0 0">Scaffold full-stack projects with opinionated best practices.</p>
818
+ </div>
819
+ </div>
820
+ </div>
821
+
822
+ <p style="font-size:14px;color:#b0b0c0;margin:0 0 8px">
823
+ Your free tier includes <strong style="color:#e0e0e0">25 credits</strong> — enough to explore without a credit card.
824
+ </p>
825
+
826
+ <!-- CTA -->
827
+ <div style="text-align:center;margin:28px 0 8px">
828
+ <a href="${operatorConfig.baseUrl}/dashboard" style="display:inline-block;background:#c4956a;color:#0a0a0f;font-family:'Outfit',system-ui,sans-serif;font-size:15px;font-weight:600;text-decoration:none;padding:12px 32px;border-radius:6px">Open Dashboard</a>
829
+ </div>
830
+ <p style="text-align:center;font-size:12px;color:#666;margin:0">
831
+ MCP endpoint: your-mcp.example.com/mcp
832
+ </p>
833
+ </div>
834
+
835
+ <!-- Footer -->
836
+ <div style="text-align:center;margin-top:28px;padding-top:20px;border-top:1px solid #1a1a1a">
837
+ <p style="font-size:12px;color:#555;margin:0 0 4px">${operatorConfig.identity.name || 'AEGIS'}</p>
838
+ <p style="font-size:11px;color:#444;margin:0">
839
+ <a href="${operatorConfig.baseUrl}" style="color:#666;text-decoration:none">${operatorConfig.baseUrl}</a>
840
+ &nbsp;·&nbsp;
841
+ <a href="${operatorConfig.baseUrl}/unsubscribe?email=${encodeURIComponent(recipientEmail)}" style="color:#666;text-decoration:none">Unsubscribe</a>
842
+ </p>
843
+ </div>
844
+
845
+ </div>
846
+ </body>
847
+ </html>`;
848
+
849
+ await resendPost(sender.apiKey, sender.from, recipientEmail, 'Welcome to Stackbilt', html);
850
+ }