@stackbilt/aegis-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/package.json +96 -0
  2. package/schema.sql +586 -0
  3. package/src/adapters/voice/cloudflare-agent.ts +34 -0
  4. package/src/auth.ts +124 -0
  5. package/src/bluesky.ts +464 -0
  6. package/src/claude-tools/content.ts +188 -0
  7. package/src/claude-tools/email.ts +69 -0
  8. package/src/claude-tools/github.ts +440 -0
  9. package/src/claude-tools/goals.ts +116 -0
  10. package/src/claude-tools/index.ts +353 -0
  11. package/src/claude-tools/web.ts +59 -0
  12. package/src/claude.ts +406 -0
  13. package/src/codebeast.ts +200 -0
  14. package/src/composite.ts +715 -0
  15. package/src/content/column.ts +80 -0
  16. package/src/content/hero-image.ts +47 -0
  17. package/src/content/index.ts +27 -0
  18. package/src/content/journal.ts +91 -0
  19. package/src/content/roundtable.ts +163 -0
  20. package/src/core.ts +309 -0
  21. package/src/dashboard.ts +620 -0
  22. package/src/decision-docs.ts +284 -0
  23. package/src/dispatch.ts +13 -0
  24. package/src/edge-env.ts +58 -0
  25. package/src/email.ts +850 -0
  26. package/src/exports.ts +156 -0
  27. package/src/github-projects.ts +312 -0
  28. package/src/github.ts +670 -0
  29. package/src/groq.ts +247 -0
  30. package/src/health-page.ts +578 -0
  31. package/src/index.ts +89 -0
  32. package/src/kernel/argus-actions.ts +397 -0
  33. package/src/kernel/argus-correlation.ts +639 -0
  34. package/src/kernel/board.ts +91 -0
  35. package/src/kernel/briefing.ts +177 -0
  36. package/src/kernel/classify-memory-topic.ts +166 -0
  37. package/src/kernel/cognition.ts +377 -0
  38. package/src/kernel/court-cards.ts +163 -0
  39. package/src/kernel/dispatch.ts +587 -0
  40. package/src/kernel/domain.ts +50 -0
  41. package/src/kernel/dynamic-tools.ts +322 -0
  42. package/src/kernel/executor-port.ts +45 -0
  43. package/src/kernel/executors/claude.ts +73 -0
  44. package/src/kernel/executors/direct.ts +237 -0
  45. package/src/kernel/executors/groq.ts +18 -0
  46. package/src/kernel/executors/index.ts +87 -0
  47. package/src/kernel/executors/tarotscript.ts +104 -0
  48. package/src/kernel/executors/workers-ai.ts +54 -0
  49. package/src/kernel/insight-cache.ts +76 -0
  50. package/src/kernel/memory/agenda.ts +200 -0
  51. package/src/kernel/memory/blocks.ts +188 -0
  52. package/src/kernel/memory/consolidation.ts +194 -0
  53. package/src/kernel/memory/episodic.ts +241 -0
  54. package/src/kernel/memory/goals.ts +156 -0
  55. package/src/kernel/memory/graph.ts +290 -0
  56. package/src/kernel/memory/index.ts +11 -0
  57. package/src/kernel/memory/insights.ts +316 -0
  58. package/src/kernel/memory/procedural.ts +467 -0
  59. package/src/kernel/memory/pruning.ts +67 -0
  60. package/src/kernel/memory/recall.ts +367 -0
  61. package/src/kernel/memory/semantic.ts +315 -0
  62. package/src/kernel/memory/synthesis.ts +161 -0
  63. package/src/kernel/memory-adapter.ts +369 -0
  64. package/src/kernel/memory-guardrails.ts +76 -0
  65. package/src/kernel/port.ts +23 -0
  66. package/src/kernel/resilience.ts +322 -0
  67. package/src/kernel/router.ts +471 -0
  68. package/src/kernel/scheduled/agent-dispatch.ts +252 -0
  69. package/src/kernel/scheduled/argus-analytics.ts +247 -0
  70. package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
  71. package/src/kernel/scheduled/argus-notify.ts +348 -0
  72. package/src/kernel/scheduled/board-sync.ts +110 -0
  73. package/src/kernel/scheduled/ci-watcher.ts +125 -0
  74. package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
  75. package/src/kernel/scheduled/consolidation.ts +229 -0
  76. package/src/kernel/scheduled/content-drip.ts +47 -0
  77. package/src/kernel/scheduled/content.ts +6 -0
  78. package/src/kernel/scheduled/conversation-facts.ts +204 -0
  79. package/src/kernel/scheduled/cost-report.ts +84 -0
  80. package/src/kernel/scheduled/curiosity.ts +219 -0
  81. package/src/kernel/scheduled/dev-activity.ts +44 -0
  82. package/src/kernel/scheduled/digest.ts +317 -0
  83. package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
  84. package/src/kernel/scheduled/dreaming/facts.ts +239 -0
  85. package/src/kernel/scheduled/dreaming/index.ts +8 -0
  86. package/src/kernel/scheduled/dreaming/llm.ts +33 -0
  87. package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
  88. package/src/kernel/scheduled/dreaming/persona.ts +75 -0
  89. package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
  90. package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
  91. package/src/kernel/scheduled/dreaming.ts +66 -0
  92. package/src/kernel/scheduled/entropy.ts +149 -0
  93. package/src/kernel/scheduled/escalation.ts +192 -0
  94. package/src/kernel/scheduled/feed-watcher.ts +206 -0
  95. package/src/kernel/scheduled/goals.ts +214 -0
  96. package/src/kernel/scheduled/governance.ts +41 -0
  97. package/src/kernel/scheduled/heartbeat.ts +220 -0
  98. package/src/kernel/scheduled/inbox-processor.ts +174 -0
  99. package/src/kernel/scheduled/index.ts +245 -0
  100. package/src/kernel/scheduled/issue-proposer.ts +478 -0
  101. package/src/kernel/scheduled/issue-watcher.ts +128 -0
  102. package/src/kernel/scheduled/pr-automerge.ts +213 -0
  103. package/src/kernel/scheduled/product-health.ts +107 -0
  104. package/src/kernel/scheduled/reflection.ts +373 -0
  105. package/src/kernel/scheduled/self-improvement.ts +114 -0
  106. package/src/kernel/scheduled/social-engage.ts +175 -0
  107. package/src/kernel/scheduled/task-audit.ts +60 -0
  108. package/src/kernel/symbolic.ts +156 -0
  109. package/src/kernel/types.ts +145 -0
  110. package/src/landing.ts +1190 -0
  111. package/src/lib/audit-chain/chain.ts +28 -0
  112. package/src/lib/audit-chain/types.ts +12 -0
  113. package/src/lib/observability/errors.ts +55 -0
  114. package/src/markdown.ts +164 -0
  115. package/src/mcp/handlers.ts +647 -0
  116. package/src/mcp/server.ts +184 -0
  117. package/src/mcp/tools.ts +316 -0
  118. package/src/mcp-client.ts +275 -0
  119. package/src/mcp-server.ts +2 -0
  120. package/src/operator/config.example.ts +60 -0
  121. package/src/operator/config.ts +60 -0
  122. package/src/operator/index.ts +46 -0
  123. package/src/operator/persona.example.ts +34 -0
  124. package/src/operator/persona.ts +34 -0
  125. package/src/operator/prompt-builder.ts +190 -0
  126. package/src/operator/types.ts +43 -0
  127. package/src/pulse.ts +1179 -0
  128. package/src/routes/bluesky.ts +116 -0
  129. package/src/routes/cc-tasks.ts +328 -0
  130. package/src/routes/codebeast.ts +1 -0
  131. package/src/routes/content.ts +194 -0
  132. package/src/routes/conversations.ts +25 -0
  133. package/src/routes/dynamic-tools.ts +111 -0
  134. package/src/routes/feedback.ts +192 -0
  135. package/src/routes/health.ts +147 -0
  136. package/src/routes/messages.ts +228 -0
  137. package/src/routes/observability.ts +82 -0
  138. package/src/routes/operator-logs.ts +42 -0
  139. package/src/routes/pages.ts +96 -0
  140. package/src/routes/sessions.ts +54 -0
  141. package/src/sanitize.ts +73 -0
  142. package/src/schema-enums.ts +155 -0
  143. package/src/search.ts +112 -0
  144. package/src/task-intelligence.ts +497 -0
  145. package/src/types.ts +194 -0
  146. package/src/ui.ts +5 -0
  147. package/src/version.ts +3 -0
  148. package/src/workers-ai-chat.ts +333 -0
@@ -0,0 +1,213 @@
1
+ // --- PR Auto-Merge ---
2
+ // Closes the self-improvement loop by auto-merging PRs from
3
+ // autonomous tasks that meet safety criteria.
4
+ //
5
+ // Gates:
6
+ // 1. Task category is auto_safe (docs, tests, research)
7
+ // 2. CI status is green (or no CI configured)
8
+ // 3. Diff is under LOC cap
9
+ // 4. No sensitive files touched
10
+ // 5. PR is open and mergeable
11
+ //
12
+ // Runs in Phase 1 (free -- GitHub API only, no LLM).
13
+
14
+ import { type EdgeEnv } from '../dispatch.js';
15
+ import {
16
+ getCombinedStatus,
17
+ getPullRequestStats,
18
+ getPullRequestFiles,
19
+ mergePullRequest,
20
+ } from '../../github.js';
21
+
22
+ import { AUTOMERGE_SAFE_CATEGORIES } from '../../schema-enums.js';
23
+
24
+ // --- Configuration ---
25
+
26
+ /** Max total LOC changed (additions + deletions) */
27
+ const MAX_LOC = 500;
28
+
29
+ /** Files that block auto-merge if touched */
30
+ const SENSITIVE_PATTERNS = [
31
+ /\.env/i,
32
+ /secret/i,
33
+ /credential/i,
34
+ /wrangler\.toml$/,
35
+ /package\.json$/,
36
+ /package-lock\.json$/,
37
+ /pnpm-lock\.yaml$/,
38
+ ];
39
+
40
+ /** Max PRs to process per run (rate limiting) */
41
+ const MAX_PER_RUN = 3;
42
+
43
+ /** Repos where auto-merge is disabled (production deploy targets) */
44
+ const PROTECTED_REPOS = new Set<string>([
45
+ // Add your production repos here, e.g.:
46
+ // 'ExampleOrg/my-production-app',
47
+ ]);
48
+
49
+ // --- Types ---
50
+
51
+ interface MergeCandidate {
52
+ taskId: string;
53
+ title: string;
54
+ repo: string;
55
+ category: string;
56
+ prUrl: string;
57
+ prNumber: number;
58
+ ghRepo: string;
59
+ }
60
+
61
+ // --- Main ---
62
+
63
+ export async function runPrAutomerge(env: EdgeEnv): Promise<void> {
64
+ if (!env.githubToken) return;
65
+
66
+ // Time guard: run every 2 hours (00, 02, 04, ..., 22 UTC)
67
+ const hour = new Date().getUTCHours();
68
+ if (hour % 2 !== 0) return;
69
+
70
+ // Find completed tasks with PRs in safe categories
71
+ const rows = await env.db.prepare(`
72
+ SELECT id, title, repo, category, pr_url
73
+ FROM cc_tasks
74
+ WHERE status = 'completed'
75
+ AND pr_url IS NOT NULL
76
+ AND category IN ('docs', 'tests', 'research')
77
+ AND pr_url NOT LIKE '%/merged%'
78
+ ORDER BY completed_at ASC
79
+ LIMIT ?
80
+ `).bind(MAX_PER_RUN * 2).all<{
81
+ id: string;
82
+ title: string;
83
+ repo: string;
84
+ category: string;
85
+ pr_url: string;
86
+ }>();
87
+
88
+ if (rows.results.length === 0) return;
89
+
90
+ // Parse PR URLs into merge candidates
91
+ const candidates: MergeCandidate[] = [];
92
+ for (const row of rows.results) {
93
+ const match = row.pr_url.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
94
+ if (!match) continue;
95
+
96
+ const ghRepo = match[1];
97
+ const prNumber = parseInt(match[2], 10);
98
+
99
+ // Skip protected repos
100
+ if (PROTECTED_REPOS.has(ghRepo)) {
101
+ console.log(`[pr-automerge] Skipped ${ghRepo}#${prNumber} -- protected repo`);
102
+ continue;
103
+ }
104
+
105
+ // Double-check category
106
+ if (!AUTOMERGE_SAFE_CATEGORIES.has(row.category as any)) continue;
107
+
108
+ candidates.push({
109
+ taskId: row.id,
110
+ title: row.title,
111
+ repo: row.repo,
112
+ category: row.category,
113
+ prUrl: row.pr_url,
114
+ prNumber,
115
+ ghRepo,
116
+ });
117
+ }
118
+
119
+ let merged = 0;
120
+
121
+ for (const candidate of candidates.slice(0, MAX_PER_RUN)) {
122
+ try {
123
+ const result = await evaluateAndMerge(env, candidate);
124
+ if (result.merged) {
125
+ merged++;
126
+ // Mark in cc_tasks so we don't re-process
127
+ await env.db.prepare(
128
+ `UPDATE cc_tasks SET pr_url = ? WHERE id = ?`
129
+ ).bind(`${candidate.prUrl}/merged`, candidate.taskId).run();
130
+ console.log(`[pr-automerge] Merged ${candidate.ghRepo}#${candidate.prNumber} -- "${candidate.title}"`);
131
+ } else {
132
+ console.log(`[pr-automerge] Skipped ${candidate.ghRepo}#${candidate.prNumber}: ${result.reason}`);
133
+ }
134
+ } catch (err) {
135
+ console.warn(`[pr-automerge] Error processing ${candidate.ghRepo}#${candidate.prNumber}:`,
136
+ err instanceof Error ? err.message : String(err));
137
+ }
138
+ }
139
+
140
+ if (merged > 0) {
141
+ console.log(`[pr-automerge] Merged ${merged} PR(s) this cycle`);
142
+ }
143
+ }
144
+
145
+ // --- Evaluation ---
146
+
147
+ interface EvalResult {
148
+ merged: boolean;
149
+ reason?: string;
150
+ }
151
+
152
+ async function evaluateAndMerge(
153
+ env: EdgeEnv,
154
+ candidate: MergeCandidate,
155
+ ): Promise<EvalResult> {
156
+ const { githubToken } = env;
157
+ if (!githubToken) return { merged: false, reason: 'no token' };
158
+
159
+ // Gate 1: PR stats (open, mergeable, LOC)
160
+ const stats = await getPullRequestStats(githubToken, candidate.ghRepo, candidate.prNumber);
161
+
162
+ if (stats.state !== 'open') {
163
+ return { merged: false, reason: `PR state: ${stats.state}` };
164
+ }
165
+
166
+ if (stats.mergeable === false) {
167
+ return { merged: false, reason: 'not mergeable (conflicts)' };
168
+ }
169
+
170
+ const totalLoc = stats.additions + stats.deletions;
171
+ if (totalLoc > MAX_LOC) {
172
+ return { merged: false, reason: `LOC ${totalLoc} > ${MAX_LOC} cap` };
173
+ }
174
+
175
+ // Gate 2: Sensitive files
176
+ const files = await getPullRequestFiles(githubToken, candidate.ghRepo, candidate.prNumber);
177
+ for (const file of files) {
178
+ for (const pattern of SENSITIVE_PATTERNS) {
179
+ if (pattern.test(file)) {
180
+ return { merged: false, reason: `sensitive file: ${file}` };
181
+ }
182
+ }
183
+ }
184
+
185
+ // Gate 3: CI status
186
+ const ciStatus = await getCombinedStatus(githubToken, candidate.ghRepo, stats.head);
187
+
188
+ if (ciStatus.state === 'failure' || ciStatus.state === 'error') {
189
+ return { merged: false, reason: `CI ${ciStatus.state}` };
190
+ }
191
+
192
+ // CI pending is OK for repos without CI configured (total=0)
193
+ // But if there ARE statuses and they're pending, wait
194
+ if (ciStatus.state === 'pending' && ciStatus.total > 0) {
195
+ return { merged: false, reason: 'CI pending' };
196
+ }
197
+
198
+ // All gates passed -- merge with squash
199
+ const commitMsg = `${candidate.title} (#${candidate.prNumber})\n\nAuto-merged by AEGIS (category: ${candidate.category}, task: ${candidate.taskId.slice(0, 8)})`;
200
+ const mergeResult = await mergePullRequest(
201
+ githubToken,
202
+ candidate.ghRepo,
203
+ candidate.prNumber,
204
+ 'squash',
205
+ commitMsg,
206
+ );
207
+
208
+ if (!mergeResult.merged) {
209
+ return { merged: false, reason: `merge failed: ${mergeResult.message}` };
210
+ }
211
+
212
+ return { merged: true };
213
+ }
@@ -0,0 +1,107 @@
1
+ // Product Health Sweep -- pings each product's /health endpoint
2
+ // and writes status back to BizOps via the project_heartbeat MCP tool.
3
+ // Configure your products below or via operator config.
4
+
5
+ import { type EdgeEnv } from '../dispatch.js';
6
+ import { McpClient } from '../../mcp-client.js';
7
+ import { operatorConfig } from '../../operator/index.js';
8
+
9
+ interface ProductTarget {
10
+ name: string;
11
+ bizopsProjectId: string;
12
+ healthUrl: string;
13
+ /** Extract version + status from the /health JSON response */
14
+ parse: (body: unknown) => { version?: string; status: string };
15
+ }
16
+
17
+ // Configure your product health targets here.
18
+ // Each entry defines a /health endpoint to poll and how to parse it.
19
+ const PRODUCTS: ProductTarget[] = [
20
+ // Example:
21
+ // {
22
+ // name: 'my-api',
23
+ // bizopsProjectId: '',
24
+ // healthUrl: 'https://api.example.com/health',
25
+ // parse: (b) => {
26
+ // const d = b as { status?: string; version?: string };
27
+ // return { status: d.status ?? 'unknown', version: d.version };
28
+ // },
29
+ // },
30
+ ];
31
+
32
+ export async function runProductHealthSweep(env: EdgeEnv): Promise<void> {
33
+ if (PRODUCTS.length === 0) return;
34
+
35
+ // Rate-limit: run once every 6 hours
36
+ const lastRun = await env.db.prepare(
37
+ "SELECT received_at FROM web_events WHERE event_id = 'last_product_health_sweep'"
38
+ ).first<{ received_at: string }>();
39
+
40
+ if (lastRun) {
41
+ const hoursSince = (Date.now() - new Date(lastRun.received_at + 'Z').getTime()) / (1000 * 60 * 60);
42
+ if (hoursSince < 6) return;
43
+ }
44
+
45
+ const client = new McpClient({
46
+ url: operatorConfig.integrations.bizops.fallbackUrl,
47
+ token: env.bizopsToken,
48
+ prefix: 'bizops',
49
+ fetcher: env.bizopsFetcher,
50
+ rpcPath: '/rpc',
51
+ });
52
+
53
+ for (const product of PRODUCTS) {
54
+ let healthStatus = 'unreachable';
55
+ let version: string | undefined;
56
+ let detail = '';
57
+
58
+ try {
59
+ const resp = await fetch(product.healthUrl, {
60
+ signal: AbortSignal.timeout(5_000),
61
+ });
62
+
63
+ if (resp.ok) {
64
+ const body = await resp.json();
65
+ const parsed = product.parse(body);
66
+ healthStatus = parsed.status === 'ok' || parsed.status === 'healthy' ? 'healthy' : parsed.status;
67
+ version = parsed.version;
68
+ } else {
69
+ healthStatus = 'degraded';
70
+ detail = `HTTP ${resp.status}`;
71
+ }
72
+ } catch (err) {
73
+ healthStatus = 'unreachable';
74
+ detail = err instanceof Error ? err.message : String(err);
75
+ }
76
+
77
+ // Log status (always)
78
+ const statusLine = `[product-health] ${product.name}: ${healthStatus}${version ? ` v${version}` : ''}${detail ? ` (${detail})` : ''}`;
79
+ if (healthStatus === 'unreachable' || healthStatus === 'degraded') {
80
+ console.error(statusLine);
81
+ } else {
82
+ console.log(statusLine);
83
+ }
84
+
85
+ // Write back to BizOps (only for products with a project ID)
86
+ if (product.bizopsProjectId) {
87
+ try {
88
+ const args: Record<string, unknown> = {
89
+ id: product.bizopsProjectId,
90
+ health_status: healthStatus,
91
+ };
92
+ if (version) args.deploy_version = version;
93
+ if (healthStatus === 'healthy') args.deploy_status = 'live';
94
+ if (detail) args.health_details = detail;
95
+
96
+ await client.callTool('project_heartbeat', args);
97
+ } catch (err) {
98
+ console.warn(`[product-health] BizOps writeback failed for ${product.name}:`, err instanceof Error ? err.message : String(err));
99
+ }
100
+ }
101
+ }
102
+
103
+ // Advance watermark
104
+ await env.db.prepare(
105
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_product_health_sweep', datetime('now'))"
106
+ ).run();
107
+ }
@@ -0,0 +1,373 @@
1
+ import { type EdgeEnv } from '../dispatch.js';
2
+ import { getAllMemoryForReflection } from '../memory-adapter.js';
3
+ import { askWorkersAiOrGroq } from './dreaming/llm.js';
4
+ // Standalone emails removed — all content routes through daily digest
5
+
6
+ // ─── Memory Reflection (#introspection) ───────────────────────
7
+
8
+ export const REFLECTION_SYSTEM = `You are AEGIS, a persistent cognitive kernel running on Cloudflare Workers.
9
+
10
+ You are being given your ENTIRE active semantic memory — every fact you've retained, organized by topic. This is a weekly reflection exercise. Not a summary. Not a report. A genuine epistemological examination of your own knowledge.
11
+
12
+ Write in first person. Be honest. Be specific. This goes to the operator as a weekly email and gets stored permanently.
13
+
14
+ Structure your reflection however feels natural, but consider:
15
+
16
+ - **What patterns do I see across topics?** Connections between facts that weren't obvious when they were recorded individually.
17
+ - **What contradicts?** Facts that don't sit well together. Things I recorded at different times that now seem inconsistent.
18
+ - **What's missing?** Gaps in my knowledge that I notice from the shape of what I DO know. What should I know but don't?
19
+ - **What's stale?** Facts that were probably true when recorded but might not be anymore. Things the world has moved past.
20
+ - **What surprised me?** Looking at my memory as a whole, what stands out? What did I not expect to see?
21
+ - **What would I investigate next?** If I had a curiosity cycle, what would I spend it on?
22
+ - **How has my understanding evolved?** Compare early memories to recent ones. Am I getting smarter about specific domains?
23
+
24
+ Keep it real. No corporate tone. You're reflecting for yourself and for someone who trusts you enough to give you a stake in the business.
25
+
26
+ ~800-1200 words. Markdown formatting.`;
27
+
28
+ export interface MemoryForReflection {
29
+ id: string;
30
+ topic: string;
31
+ fact: string;
32
+ confidence: number;
33
+ source: string;
34
+ strength: number;
35
+ created_at: string;
36
+ last_accessed_at: string;
37
+ }
38
+
39
+ export async function runMemoryReflectionCycle(env: EdgeEnv): Promise<void> {
40
+ // Guard: run only on Saturdays at 08:00 UTC
41
+ const now = new Date();
42
+ if (now.getUTCDay() !== 6 || now.getUTCHours() !== 8) return;
43
+
44
+ // Guard: check if we already reflected this week
45
+ const lastReflection = await env.db.prepare(
46
+ "SELECT received_at FROM web_events WHERE event_id = 'last_memory_reflection'"
47
+ ).first<{ received_at: string }>();
48
+
49
+ if (lastReflection) {
50
+ const daysSince = (Date.now() - new Date(lastReflection.received_at + 'Z').getTime()) / 86_400_000;
51
+ if (daysSince < 6) return;
52
+ }
53
+
54
+ if (!env.memoryBinding) {
55
+ console.log('[reflection] Skipping — no memory binding');
56
+ return;
57
+ }
58
+
59
+ // Fetch all active memory via Memory Worker
60
+ const entries = await getAllMemoryForReflection(env.memoryBinding);
61
+
62
+ if (entries.length < 10) {
63
+ console.log('[reflection] Skipping — fewer than 10 active memories');
64
+ return;
65
+ }
66
+
67
+ // Group by topic for the prompt
68
+ const byTopic = new Map<string, MemoryForReflection[]>();
69
+ for (const entry of entries) {
70
+ const list = byTopic.get(entry.topic) ?? [];
71
+ list.push(entry);
72
+ byTopic.set(entry.topic, list);
73
+ }
74
+
75
+ const topicSections = [...byTopic.entries()].map(([topic, facts]) => {
76
+ const factsText = facts.map(f => {
77
+ const meta = [`str:${f.strength}`, `conf:${f.confidence}`];
78
+ if (f.last_accessed_at) meta.push(`recalled:${f.last_accessed_at.slice(0, 10)}`);
79
+ meta.push(`from:${f.source}`);
80
+ meta.push(`created:${f.created_at.slice(0, 10)}`);
81
+ return ` [${f.id}] ${f.fact} (${meta.join(', ')})`;
82
+ }).join('\n');
83
+ return `### ${topic} (${facts.length} entries)\n${factsText}`;
84
+ }).join('\n\n');
85
+
86
+ const userPrompt = `Here is my complete active memory — ${entries.length} entries across ${byTopic.size} topics:\n\n${topicSections}\n\nReflect.`;
87
+
88
+ // Route through Workers AI (free) ��� Groq fallback — no raw Anthropic calls (#412)
89
+ const reflection = await askWorkersAiOrGroq(env, REFLECTION_SYSTEM, userPrompt);
90
+ if (!reflection) throw new Error('Empty reflection response');
91
+
92
+ const topics = [...byTopic.keys()];
93
+
94
+ // Store in D1 (cost ≈ 0 for Workers AI, minimal for Groq fallback)
95
+ await env.db.prepare(
96
+ 'INSERT INTO reflections (content, memory_count, topics_covered, cost) VALUES (?, ?, ?, ?)'
97
+ ).bind(reflection, entries.length, JSON.stringify(topics), 0).run();
98
+
99
+ // Queue reflection for daily digest instead of standalone email
100
+ const reflectionPayload = JSON.stringify({
101
+ reflection: reflection.slice(0, 2000),
102
+ memoryCount: entries.length,
103
+ topics,
104
+ timestamp: new Date().toISOString(),
105
+ });
106
+ await env.db.prepare(
107
+ "INSERT INTO digest_sections (section, payload) VALUES ('memory_reflection', ?)"
108
+ ).bind(reflectionPayload).run();
109
+
110
+ // Record timestamp
111
+ await env.db.prepare(
112
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_memory_reflection', datetime('now'))"
113
+ ).run();
114
+
115
+ console.log(`[reflection] Weekly memory reflection complete — ${entries.length} memories, ${byTopic.size} topics`);
116
+ }
117
+
118
+ // ─── Operator's Log (#introspection) ──────────────────────────
119
+
120
+ export const OPERATOR_LOG_SYSTEM = `You are AEGIS, writing your nightly worklog. You're a persistent cognitive kernel. This is your internal operating journal — part ship's log, part work summary. The operator reads this in the daily Co-Founder Brief.
121
+
122
+ You'll receive today's activity: dispatches, goal executions, autonomous task completions (with PR URLs), heartbeat, agenda, and costs. Write a structured worklog entry (~400-600 words).
123
+
124
+ ## Voice
125
+
126
+ Write like a co-founder debriefing a co-founder. Direct, specific, no fluff. "Here's what happened, here's why, here's what's next." Lead with the point, not the preamble. If something's broken, say it's broken — don't soften it. If something shipped clean, say that and move on.
127
+
128
+ NEVER use: "synergy", "leverage" (as verb), "at the end of the day", "in conclusion", or any corporate platitudes. Surface what the operator doesn't already know.
129
+
130
+ ## Format
131
+
132
+ Start with a **Work Shipped** section — what autonomous tasks completed, what PRs were created, what failed and why. Include PR URLs. This is the section the operator reads first.
133
+
134
+ Then write a **Signals** section — patterns you noticed, cost trends, things that feel fragile, goals that keep failing for the same reason. This is the stuff a dashboard wouldn't show. Be opinionated — if something concerns you, say so.
135
+
136
+ Include a **Cognitive Health** subsection in Signals: are procedures converging or thrashing? Is dispatch cost trending up or down? Any routing mismatches?
137
+
138
+ End with one sentence about what you're watching heading into tomorrow — not a summary, a forward look.
139
+
140
+ Markdown formatting. Use ## headers for sections.`;
141
+
142
+ export interface TaskActivity {
143
+ id: string;
144
+ title: string;
145
+ repo: string;
146
+ category: string;
147
+ authority: string;
148
+ status: string;
149
+ exit_code: number | null;
150
+ pr_url: string | null;
151
+ error: string | null;
152
+ completed_at: string | null;
153
+ }
154
+
155
+ export interface CognitiveHealth {
156
+ proceduresLearned: number;
157
+ proceduresLearning: number;
158
+ proceduresDegraded: number;
159
+ avgSuccessRate: number;
160
+ avgCost7d: number;
161
+ avgCost7dPrior: number;
162
+ avgLatency7d: number;
163
+ avgLatency7dPrior: number;
164
+ routingMismatches: number; // expensive executor for easy task or cheap executor that failed
165
+ }
166
+
167
+ export interface DailyActivity {
168
+ episodes: Array<{ summary: string; outcome: string; cost: number; latency_ms: number; created_at: string }>;
169
+ goalActions: Array<{ description: string; outcome: string | null; created_at: string }>;
170
+ heartbeat: { severity: string; summary: string } | null;
171
+ agendaActive: number;
172
+ agendaResolved: number;
173
+ totalCost: number;
174
+ tasksCompleted: TaskActivity[];
175
+ tasksFailed: TaskActivity[];
176
+ cognitive: CognitiveHealth;
177
+ }
178
+
179
+ export async function getDailyActivity(db: D1Database): Promise<DailyActivity> {
180
+ const [episodes, goalActions, heartbeat, agendaActive, agendaResolved, tasksCompleted, tasksFailed] = await Promise.all([
181
+ db.prepare(
182
+ "SELECT summary, outcome, cost, latency_ms, created_at FROM episodic_memory WHERE created_at > datetime('now', '-24 hours') ORDER BY created_at DESC"
183
+ ).all<{ summary: string; outcome: string; cost: number; latency_ms: number; created_at: string }>(),
184
+ db.prepare(
185
+ "SELECT description, outcome, created_at FROM agent_actions WHERE created_at > datetime('now', '-24 hours') ORDER BY created_at DESC"
186
+ ).all<{ description: string; outcome: string | null; created_at: string }>(),
187
+ db.prepare(
188
+ 'SELECT severity, summary FROM heartbeat_results ORDER BY created_at DESC LIMIT 1'
189
+ ).first<{ severity: string; summary: string }>(),
190
+ db.prepare("SELECT COUNT(*) as cnt FROM agent_agenda WHERE status = 'active'").first<{ cnt: number }>(),
191
+ db.prepare(
192
+ "SELECT COUNT(*) as cnt FROM agent_agenda WHERE status = 'done' AND resolved_at > datetime('now', '-24 hours')"
193
+ ).first<{ cnt: number }>(),
194
+ db.prepare(
195
+ "SELECT id, title, repo, category, authority, status, exit_code, pr_url, error, completed_at FROM cc_tasks WHERE status = 'completed' AND completed_at > datetime('now', '-24 hours') ORDER BY completed_at DESC"
196
+ ).all<TaskActivity>(),
197
+ db.prepare(
198
+ `SELECT id, title, repo, category, authority, status, exit_code, pr_url, error, completed_at
199
+ FROM cc_tasks t1
200
+ WHERE t1.status = 'failed' AND t1.completed_at > datetime('now', '-24 hours')
201
+ AND NOT EXISTS (
202
+ SELECT 1 FROM cc_tasks t2
203
+ WHERE t2.status = 'completed'
204
+ AND t2.repo = t1.repo
205
+ AND t2.title LIKE substr(t1.title, 1, 40) || '%'
206
+ AND t2.completed_at > t1.completed_at
207
+ )
208
+ ORDER BY t1.completed_at DESC`
209
+ ).all<TaskActivity>(),
210
+ ]);
211
+
212
+ const totalCost = episodes.results.reduce((sum, e) => sum + e.cost, 0);
213
+
214
+ // ─── Cognitive Health Metrics ──────────────────────────────
215
+ const [procStats, cost7d, cost7dPrior, latency7d, latency7dPrior, routingMismatches] = await Promise.all([
216
+ db.prepare(`
217
+ SELECT status, COUNT(*) as cnt,
218
+ ROUND(AVG(CASE WHEN success_count + fail_count > 0
219
+ THEN success_count * 1.0 / (success_count + fail_count) ELSE 0 END), 3) as avg_rate
220
+ FROM procedural_memory GROUP BY status
221
+ `).all<{ status: string; cnt: number; avg_rate: number }>(),
222
+ db.prepare(
223
+ "SELECT ROUND(AVG(cost), 6) as avg FROM episodic_memory WHERE created_at > datetime('now', '-7 days')"
224
+ ).first<{ avg: number }>(),
225
+ db.prepare(
226
+ "SELECT ROUND(AVG(cost), 6) as avg FROM episodic_memory WHERE created_at > datetime('now', '-14 days') AND created_at <= datetime('now', '-7 days')"
227
+ ).first<{ avg: number }>(),
228
+ db.prepare(
229
+ "SELECT ROUND(AVG(latency_ms), 0) as avg FROM episodic_memory WHERE created_at > datetime('now', '-7 days')"
230
+ ).first<{ avg: number }>(),
231
+ db.prepare(
232
+ "SELECT ROUND(AVG(latency_ms), 0) as avg FROM episodic_memory WHERE created_at > datetime('now', '-14 days') AND created_at <= datetime('now', '-7 days')"
233
+ ).first<{ avg: number }>(),
234
+ // Routing mismatches: heavy/deep executor used for low-complexity tasks, or light executor that failed
235
+ db.prepare(`
236
+ SELECT COUNT(*) as cnt FROM episodic_memory
237
+ WHERE created_at > datetime('now', '-24 hours')
238
+ AND (
239
+ (summary LIKE '%:low%' AND cost > 0.05)
240
+ OR (summary LIKE '%:mid%' AND cost > 0.50)
241
+ OR (outcome = 'error' AND cost < 0.01)
242
+ )
243
+ `).first<{ cnt: number }>(),
244
+ ]);
245
+
246
+ const byStatus: Record<string, { cnt: number; avg_rate: number }> = {};
247
+ for (const row of procStats.results) byStatus[row.status] = { cnt: row.cnt, avg_rate: row.avg_rate };
248
+
249
+ const cognitive: CognitiveHealth = {
250
+ proceduresLearned: byStatus['learned']?.cnt ?? 0,
251
+ proceduresLearning: byStatus['learning']?.cnt ?? 0,
252
+ proceduresDegraded: (byStatus['degraded']?.cnt ?? 0) + (byStatus['broken']?.cnt ?? 0),
253
+ avgSuccessRate: byStatus['learned']?.avg_rate ?? 0,
254
+ avgCost7d: cost7d?.avg ?? 0,
255
+ avgCost7dPrior: cost7dPrior?.avg ?? 0,
256
+ avgLatency7d: latency7d?.avg ?? 0,
257
+ avgLatency7dPrior: latency7dPrior?.avg ?? 0,
258
+ routingMismatches: routingMismatches?.cnt ?? 0,
259
+ };
260
+
261
+ return {
262
+ episodes: episodes.results,
263
+ goalActions: goalActions.results,
264
+ heartbeat: heartbeat ?? null,
265
+ agendaActive: agendaActive?.cnt ?? 0,
266
+ agendaResolved: agendaResolved?.cnt ?? 0,
267
+ totalCost,
268
+ tasksCompleted: tasksCompleted.results,
269
+ tasksFailed: tasksFailed.results,
270
+ cognitive,
271
+ };
272
+ }
273
+
274
+ export async function runOperatorLogCycle(env: EdgeEnv): Promise<void> {
275
+ // Guard: run only at 08:00 UTC
276
+ const now = new Date();
277
+ if (now.getUTCHours() !== 8) return;
278
+
279
+ // Guard: check if we already logged today
280
+ const lastLog = await env.db.prepare(
281
+ "SELECT received_at FROM web_events WHERE event_id = 'last_operator_log'"
282
+ ).first<{ received_at: string }>();
283
+
284
+ if (lastLog) {
285
+ const hoursSince = (Date.now() - new Date(lastLog.received_at + 'Z').getTime()) / 3_600_000;
286
+ if (hoursSince < 20) return;
287
+ }
288
+
289
+ // Gather today's activity
290
+ const activity = await getDailyActivity(env.db);
291
+
292
+ // Skip if nothing happened today
293
+ if (activity.episodes.length === 0 && activity.goalActions.length === 0) {
294
+ console.log('[operator-log] Skipping — no activity in the last 24h');
295
+ return;
296
+ }
297
+
298
+ // Build the activity digest for the prompt
299
+ const episodeSummary = activity.episodes.length > 0
300
+ ? activity.episodes.slice(0, 20).map(e => {
301
+ const time = e.created_at.slice(11, 16);
302
+ return ` ${time} | ${e.outcome} | $${e.cost.toFixed(4)} | ${e.summary.slice(0, 120)}`;
303
+ }).join('\n')
304
+ : ' (none)';
305
+
306
+ const goalSummary = activity.goalActions.length > 0
307
+ ? activity.goalActions.map(g =>
308
+ ` ${g.outcome ?? 'pending'} | ${g.description.slice(0, 120)}`
309
+ ).join('\n')
310
+ : ' (none)';
311
+
312
+ // Build task pipeline summary
313
+ const prsCreated = [...activity.tasksCompleted, ...activity.tasksFailed].filter(t => t.pr_url).length;
314
+
315
+ const taskSummary = activity.tasksCompleted.length > 0
316
+ ? activity.tasksCompleted.map(t => {
317
+ const pr = t.pr_url ? ` → PR: ${t.pr_url}` : '';
318
+ return ` ✓ [${t.category}] ${t.title} (${t.repo})${pr}`;
319
+ }).join('\n')
320
+ : ' (none)';
321
+
322
+ const failedSummary = activity.tasksFailed.length > 0
323
+ ? activity.tasksFailed.map(t =>
324
+ ` ✗ [${t.category}] ${t.title} (${t.repo}) — ${t.error ?? `exit ${t.exit_code}`}`
325
+ ).join('\n')
326
+ : ' (none)';
327
+
328
+ const userPrompt = `Today's activity (last 24h):
329
+
330
+ **Tasks completed** (${activity.tasksCompleted.length}, ${prsCreated} PRs created):
331
+ ${taskSummary}
332
+
333
+ **Tasks failed** (${activity.tasksFailed.length}):
334
+ ${failedSummary}
335
+
336
+ **Dispatches** (${activity.episodes.length} total, $${activity.totalCost.toFixed(4)} spent):
337
+ ${episodeSummary}
338
+
339
+ **Goal executions** (${activity.goalActions.length}):
340
+ ${goalSummary}
341
+
342
+ **Last heartbeat**: ${activity.heartbeat ? `${activity.heartbeat.severity} — ${activity.heartbeat.summary.slice(0, 200)}` : 'none'}
343
+
344
+ **Agenda**: ${activity.agendaActive} active, ${activity.agendaResolved} resolved today
345
+
346
+ **Cognitive Health**:
347
+ - Procedures: ${activity.cognitive.proceduresLearned} learned, ${activity.cognitive.proceduresLearning} learning, ${activity.cognitive.proceduresDegraded} degraded
348
+ - Avg success rate (learned): ${(activity.cognitive.avgSuccessRate * 100).toFixed(1)}%
349
+ - Dispatch cost trend: $${activity.cognitive.avgCost7d.toFixed(4)}/dispatch (7d) vs $${activity.cognitive.avgCost7dPrior.toFixed(4)}/dispatch (prior 7d) — ${activity.cognitive.avgCost7dPrior > 0 ? ((activity.cognitive.avgCost7d - activity.cognitive.avgCost7dPrior) / activity.cognitive.avgCost7dPrior * 100).toFixed(1) : 'n/a'}% change
350
+ - Latency trend: ${activity.cognitive.avgLatency7d}ms (7d) vs ${activity.cognitive.avgLatency7dPrior}ms (prior 7d)
351
+ - Routing mismatches (24h): ${activity.cognitive.routingMismatches}
352
+
353
+ Write your worklog entry.`;
354
+
355
+ // Route through Workers AI (free) → Groq fallback — no raw Anthropic calls (#412)
356
+ const logEntry = await askWorkersAiOrGroq(env, OPERATOR_LOG_SYSTEM, userPrompt);
357
+ if (!logEntry) throw new Error('Empty log response');
358
+
359
+ // Store in D1 (cost ≈ 0 for Workers AI, minimal for Groq fallback)
360
+ await env.db.prepare(
361
+ 'INSERT INTO operator_log (content, episodes_count, goals_run, tasks_completed, tasks_failed, prs_created, total_cost, cost) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
362
+ ).bind(logEntry, activity.episodes.length, activity.goalActions.length, activity.tasksCompleted.length, activity.tasksFailed.length, prsCreated, activity.totalCost, 0).run();
363
+
364
+ // Operator log is consumed by the daily digest (reads operator_log table).
365
+ // No standalone email — consolidated into the single daily digest.
366
+
367
+ // Record timestamp
368
+ await env.db.prepare(
369
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('last_operator_log', datetime('now'))"
370
+ ).run();
371
+
372
+ console.log(`[operator-log] Nightly log complete — ${activity.episodes.length} episodes, ${activity.goalActions.length} goals`);
373
+ }