@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,110 @@
1
+ // --- Board Sync -- GitHub Projects v2 Reconciliation ---
2
+ // Hourly safety net: fetches all project items from GitHub and
3
+ // reconciles with D1 board_items cache. Catches manual moves,
4
+ // external additions, and any webhook events we missed.
5
+ //
6
+ // Runs every 2 hours (time-gated in scheduled/index.ts).
7
+ // Cost: 2-4 GraphQL API calls per run.
8
+
9
+ import { type EdgeEnv } from '../dispatch.js';
10
+ import { listProjectItems, type ProjectItem } from '../../github-projects.js';
11
+ import { upsertBoardItem, type BoardColumn, PROJECT_TITLE, ORG_LOGIN } from '../board.js';
12
+ import { findOrCreateProject } from '../../github-projects.js';
13
+
14
+ /** Map GitHub Projects Status field values to our internal column names. */
15
+ function resolveStatus(item: ProjectItem): BoardColumn {
16
+ const statusNode = item.fieldValues.nodes.find(n => n.field?.name === 'Status');
17
+ const statusName = statusNode?.name ?? '';
18
+ const map: Record<string, BoardColumn> = {
19
+ 'Backlog': 'backlog',
20
+ 'Queued': 'queued',
21
+ 'In Progress': 'in_progress',
22
+ 'Blocked': 'blocked',
23
+ 'Shipped': 'shipped',
24
+ };
25
+ return map[statusName] ?? 'backlog';
26
+ }
27
+
28
+ export async function runBoardSync(env: EdgeEnv): Promise<void> {
29
+ if (!env.githubToken) {
30
+ console.log('[board-sync] No GITHUB_TOKEN -- skipping');
31
+ return;
32
+ }
33
+
34
+ // Get project ID (cached in D1 via web_events)
35
+ const cachedId = await env.db.prepare(
36
+ "SELECT received_at FROM web_events WHERE event_id = 'board_project_id'"
37
+ ).first<{ received_at: string }>();
38
+
39
+ let projectId = cachedId?.received_at;
40
+ if (!projectId) {
41
+ try {
42
+ projectId = await findOrCreateProject(env.githubToken, ORG_LOGIN, PROJECT_TITLE);
43
+ await env.db.prepare(
44
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('board_project_id', ?)"
45
+ ).bind(projectId).run();
46
+ } catch (err) {
47
+ console.error(`[board-sync] Failed to find/create project: ${err instanceof Error ? err.message : String(err)}`);
48
+ return;
49
+ }
50
+ }
51
+
52
+ // Fetch all items from the project (paginated)
53
+ const allItems: ProjectItem[] = [];
54
+ let cursor: string | null = null;
55
+ const maxPages = 4; // Safety: 50 items/page x 4 = 200 max
56
+
57
+ for (let page = 0; page < maxPages; page++) {
58
+ const result = await listProjectItems(env.githubToken, projectId, cursor ?? undefined);
59
+ allItems.push(...result.items);
60
+ if (!result.nextCursor) break;
61
+ cursor = result.nextCursor;
62
+ }
63
+
64
+ // Track which repo+number combinations we see from GitHub
65
+ const seenKeys = new Set<string>();
66
+
67
+ // Upsert each item into D1
68
+ for (const item of allItems) {
69
+ if (!item.content) continue; // Draft items have no content
70
+
71
+ const repo = item.content.repository.nameWithOwner;
72
+ const number = item.content.number;
73
+ const key = `${repo}:${number}`;
74
+ seenKeys.add(key);
75
+
76
+ const labels = item.content.labels.nodes.map(l => l.name);
77
+ const assignee = item.content.assignees.nodes[0]?.login ?? null;
78
+ const status = resolveStatus(item);
79
+
80
+ await upsertBoardItem(env.db, {
81
+ id: item.id,
82
+ project_id: projectId,
83
+ content_type: item.content.__typename === 'PullRequest' ? 'pr' : 'issue',
84
+ content_node_id: '', // Not available from list query -- set on first add
85
+ repo,
86
+ number,
87
+ title: item.content.title,
88
+ status,
89
+ labels: JSON.stringify(labels),
90
+ assignee,
91
+ });
92
+ }
93
+
94
+ // Remove D1 items that no longer exist on the project
95
+ // (deleted via GitHub UI or removed from project)
96
+ const localItems = await env.db.prepare(
97
+ "SELECT repo, number FROM board_items WHERE status != 'shipped'"
98
+ ).all<{ repo: string; number: number }>();
99
+
100
+ for (const local of localItems.results ?? []) {
101
+ const key = `${local.repo}:${local.number}`;
102
+ if (!seenKeys.has(key)) {
103
+ await env.db.prepare(
104
+ 'DELETE FROM board_items WHERE repo = ? AND number = ?'
105
+ ).bind(local.repo, local.number).run();
106
+ }
107
+ }
108
+
109
+ console.log(`[board-sync] Synced ${allItems.length} items, ${seenKeys.size} active`);
110
+ }
@@ -0,0 +1,125 @@
1
+ // --- CI Failure Watcher ---
2
+ // Polls GitHub Actions workflow runs for watched repos and surfaces
3
+ // failures via the agenda system. Zero LM calls -- pure GitHub API
4
+ // + string matching. Runs hourly in the heartbeat phase.
5
+
6
+ import { type EdgeEnv } from '../dispatch.js';
7
+ import { listWorkflowRuns, resolveRepoName, type WorkflowRun } from '../../github.js';
8
+ import { addAgendaItem } from '../memory/agenda.js';
9
+
10
+ const WATCHED_REPO = 'aegis-oss';
11
+ const WATERMARK_KEY = 'last_ci_watcher_run';
12
+ const CHECK_INTERVAL_HOURS = 1;
13
+
14
+ // Keywords in workflow run names or commit messages that indicate
15
+ // breaking-change risk to key interfaces.
16
+ const BREAKING_CHANGE_KEYWORDS = [
17
+ 'breaking',
18
+ 'interface',
19
+ 'type change',
20
+ 'constructor',
21
+ 'signature',
22
+ 'schema',
23
+ 'migration',
24
+ ];
25
+
26
+ /** Check if a workflow run is relevant to breaking-change interfaces */
27
+ function isBreakingChangeRelated(run: WorkflowRun): boolean {
28
+ const text = `${run.name} ${run.branch}`.toLowerCase();
29
+ return BREAKING_CHANGE_KEYWORDS.some(kw => text.includes(kw));
30
+ }
31
+
32
+ export async function runCiWatcher(env: EdgeEnv): Promise<void> {
33
+ if (!env.githubToken) {
34
+ console.log('[ci-watcher] Skipped: missing githubToken');
35
+ return;
36
+ }
37
+
38
+ // Rate-limit: run once per hour
39
+ const lastRun = await env.db.prepare(
40
+ `SELECT received_at FROM web_events WHERE event_id = ?`
41
+ ).bind(WATERMARK_KEY).first<{ received_at: string }>();
42
+
43
+ if (lastRun) {
44
+ const hoursSince = (Date.now() - new Date(lastRun.received_at + 'Z').getTime()) / (1000 * 60 * 60);
45
+ if (hoursSince < CHECK_INTERVAL_HOURS) return;
46
+ }
47
+
48
+ const repo = resolveRepoName(WATCHED_REPO);
49
+
50
+ // Fetch recent workflow runs (main branch + PRs)
51
+ let runs: WorkflowRun[];
52
+ try {
53
+ runs = await listWorkflowRuns(env.githubToken, repo, undefined, 10);
54
+ } catch (err) {
55
+ console.warn(`[ci-watcher] Failed to list workflow runs for ${repo}:`, err instanceof Error ? err.message : String(err));
56
+ return;
57
+ }
58
+
59
+ if (runs.length === 0) {
60
+ console.log(`[ci-watcher] ${repo}: no workflow runs found`);
61
+ await advanceWatermark(env.db);
62
+ return;
63
+ }
64
+
65
+ // Find failed runs that completed since our last check
66
+ const sinceDate = lastRun
67
+ ? new Date(lastRun.received_at + 'Z')
68
+ : new Date(Date.now() - CHECK_INTERVAL_HOURS * 60 * 60 * 1000);
69
+
70
+ const newFailures = runs.filter(r =>
71
+ r.conclusion === 'failure' &&
72
+ r.status === 'completed' &&
73
+ new Date(r.created_at) > sinceDate
74
+ );
75
+
76
+ if (newFailures.length === 0) {
77
+ console.log(`[ci-watcher] ${repo}: no new failures`);
78
+ await advanceWatermark(env.db);
79
+ return;
80
+ }
81
+
82
+ console.log(`[ci-watcher] ${repo}: ${newFailures.length} new CI failure(s)`);
83
+
84
+ for (const run of newFailures) {
85
+ // Dedup: skip if we already created an agenda item for this run
86
+ const existing = await env.db.prepare(
87
+ `SELECT 1 FROM web_events WHERE event_id = ? LIMIT 1`
88
+ ).bind(`ci-fail:${repo}:${run.id}`).first();
89
+
90
+ if (existing) continue;
91
+
92
+ const isBreaking = isBreakingChangeRelated(run);
93
+ const priority = run.branch === 'main'
94
+ ? 'high'
95
+ : (isBreaking ? 'high' : 'medium');
96
+
97
+ const breakingTag = isBreaking ? ' [possible breaking change]' : '';
98
+ const branchTag = run.branch === 'main' ? ' (main branch)' : ` (branch: ${run.branch})`;
99
+
100
+ await addAgendaItem(
101
+ env.db,
102
+ `CI failure: "${run.name}"${branchTag}${breakingTag}`,
103
+ `Workflow run #${run.id} failed on ${run.branch} (event: ${run.event}). ` +
104
+ `Created: ${run.created_at}. ` +
105
+ `${isBreaking ? 'May affect key interfaces or schemas. ' : ''}` +
106
+ `URL: ${run.url}`,
107
+ priority,
108
+ );
109
+
110
+ // Mark this run as reported
111
+ await env.db.prepare(
112
+ `INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))`
113
+ ).bind(`ci-fail:${repo}:${run.id}`).run();
114
+
115
+ console.log(`[ci-watcher] Agenda item created for run #${run.id} (${priority} priority${breakingTag})`);
116
+ }
117
+
118
+ await advanceWatermark(env.db);
119
+ }
120
+
121
+ async function advanceWatermark(db: D1Database): Promise<void> {
122
+ await db.prepare(
123
+ `INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES (?, datetime('now'))`
124
+ ).bind(WATERMARK_KEY).run();
125
+ }
@@ -0,0 +1,377 @@
1
+ // Cognitive Self-Measurement (#255)
2
+ // Computes whether AEGIS is getting smarter by aggregating existing data.
3
+ // Runs daily. Stores a snapshot to D1 for trend analysis.
4
+ // No LLM calls — pure D1 queries.
5
+
6
+ import { type EdgeEnv } from '../dispatch.js';
7
+
8
+ // ─── Metrics Schema ──────────────────────────────────────────
9
+
10
+ export interface CognitiveSnapshot {
11
+ // Dispatch quality
12
+ dispatch_success_rate_7d: number; // % success in last 7 days
13
+ dispatch_success_rate_prior_7d: number; // % success in prior 7 days (for delta)
14
+ dispatch_volume_7d: number;
15
+ avg_cost_7d: number;
16
+ avg_cost_prior_7d: number;
17
+ avg_latency_ms_7d: number;
18
+ avg_latency_ms_prior_7d: number;
19
+ reclassification_rate_7d: number; // % of dispatches that got reclassified
20
+
21
+ // Procedural learning
22
+ procedures_learned: number;
23
+ procedures_learning: number;
24
+ procedures_degraded: number;
25
+ procedures_broken: number;
26
+ procedure_convergence_rate: number; // learned / total
27
+
28
+ // Task execution
29
+ tasks_completed_7d: number;
30
+ tasks_failed_7d: number;
31
+ task_success_rate_7d: number;
32
+ task_success_rate_prior_7d: number;
33
+ top_failure_kind: string | null;
34
+
35
+ // Scheduled task health
36
+ scheduled_task_error_rate_24h: number;
37
+
38
+ // Classifier source breakdown (classify-cast monitoring)
39
+ classify_cast_rate_48h: number; // % of dispatches using classify-cast
40
+ user_correction_rate_48h: number; // % of dispatches followed by user_correction
41
+ classify_cast_correction_rate_48h: number; // user_correction rate specifically for classify-cast episodes
42
+
43
+ // Memory growth
44
+ memory_count: number;
45
+ memory_count_prior: number; // from last snapshot
46
+
47
+ // Composite score (0-100)
48
+ cognitive_score: number;
49
+ score_delta: number; // vs last snapshot
50
+
51
+ computed_at: string;
52
+ }
53
+
54
+ // ─── Queries ─────────────────────────────────────────────────
55
+
56
+ async function queryDispatchMetrics(db: D1Database): Promise<{
57
+ success_rate_7d: number;
58
+ success_rate_prior_7d: number;
59
+ volume_7d: number;
60
+ avg_cost_7d: number;
61
+ avg_cost_prior_7d: number;
62
+ avg_latency_7d: number;
63
+ avg_latency_prior_7d: number;
64
+ reclassification_rate_7d: number;
65
+ }> {
66
+ const [current, prior, reclass] = await Promise.all([
67
+ db.prepare(`
68
+ SELECT COUNT(*) as total,
69
+ SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
70
+ AVG(cost) as avg_cost,
71
+ AVG(latency_ms) as avg_latency
72
+ FROM episodic_memory
73
+ WHERE created_at > datetime('now', '-7 days')
74
+ `).first<{ total: number; successes: number; avg_cost: number; avg_latency: number }>(),
75
+
76
+ db.prepare(`
77
+ SELECT COUNT(*) as total,
78
+ SUM(CASE WHEN outcome = 'success' THEN 1 ELSE 0 END) as successes,
79
+ AVG(cost) as avg_cost,
80
+ AVG(latency_ms) as avg_latency
81
+ FROM episodic_memory
82
+ WHERE created_at > datetime('now', '-14 days')
83
+ AND created_at <= datetime('now', '-7 days')
84
+ `).first<{ total: number; successes: number; avg_cost: number; avg_latency: number }>(),
85
+
86
+ db.prepare(`
87
+ SELECT COUNT(*) as total,
88
+ SUM(CASE WHEN reclassified = 1 THEN 1 ELSE 0 END) as reclassified
89
+ FROM episodic_memory
90
+ WHERE created_at > datetime('now', '-7 days')
91
+ `).first<{ total: number; reclassified: number }>(),
92
+ ]);
93
+
94
+ const curTotal = current?.total ?? 0;
95
+ const priorTotal = prior?.total ?? 0;
96
+
97
+ return {
98
+ success_rate_7d: curTotal > 0 ? (current?.successes ?? 0) / curTotal : 1,
99
+ success_rate_prior_7d: priorTotal > 0 ? (prior?.successes ?? 0) / priorTotal : 1,
100
+ volume_7d: curTotal,
101
+ avg_cost_7d: current?.avg_cost ?? 0,
102
+ avg_cost_prior_7d: prior?.avg_cost ?? 0,
103
+ avg_latency_7d: current?.avg_latency ?? 0,
104
+ avg_latency_prior_7d: prior?.avg_latency ?? 0,
105
+ reclassification_rate_7d: (reclass?.total ?? 0) > 0
106
+ ? (reclass?.reclassified ?? 0) / (reclass?.total ?? 1)
107
+ : 0,
108
+ };
109
+ }
110
+
111
+ async function queryProceduralMetrics(db: D1Database): Promise<{
112
+ learned: number;
113
+ learning: number;
114
+ degraded: number;
115
+ broken: number;
116
+ convergence_rate: number;
117
+ }> {
118
+ const result = await db.prepare(`
119
+ SELECT status, COUNT(*) as count
120
+ FROM procedural_memory
121
+ GROUP BY status
122
+ `).all<{ status: string; count: number }>();
123
+
124
+ const counts: Record<string, number> = {};
125
+ for (const row of result.results) {
126
+ counts[row.status] = row.count;
127
+ }
128
+
129
+ const total = Object.values(counts).reduce((a, b) => a + b, 0);
130
+
131
+ return {
132
+ learned: counts['learned'] ?? 0,
133
+ learning: counts['learning'] ?? 0,
134
+ degraded: counts['degraded'] ?? 0,
135
+ broken: counts['broken'] ?? 0,
136
+ convergence_rate: total > 0 ? (counts['learned'] ?? 0) / total : 0,
137
+ };
138
+ }
139
+
140
+ async function queryTaskMetrics(db: D1Database): Promise<{
141
+ completed_7d: number;
142
+ failed_7d: number;
143
+ success_rate_7d: number;
144
+ success_rate_prior_7d: number;
145
+ top_failure_kind: string | null;
146
+ }> {
147
+ const [current, prior, failures] = await Promise.all([
148
+ db.prepare(`
149
+ SELECT COUNT(*) as total,
150
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
151
+ SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed
152
+ FROM cc_tasks
153
+ WHERE completed_at > datetime('now', '-7 days')
154
+ `).first<{ total: number; completed: number; failed: number }>(),
155
+
156
+ db.prepare(`
157
+ SELECT COUNT(*) as total,
158
+ SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed
159
+ FROM cc_tasks
160
+ WHERE completed_at > datetime('now', '-14 days')
161
+ AND completed_at <= datetime('now', '-7 days')
162
+ `).first<{ total: number; completed: number }>(),
163
+
164
+ db.prepare(`
165
+ SELECT failure_kind, COUNT(*) as cnt
166
+ FROM cc_tasks
167
+ WHERE status = 'failed' AND completed_at > datetime('now', '-7 days')
168
+ AND failure_kind IS NOT NULL
169
+ GROUP BY failure_kind
170
+ ORDER BY cnt DESC
171
+ LIMIT 1
172
+ `).first<{ failure_kind: string; cnt: number }>(),
173
+ ]);
174
+
175
+ const curTotal = (current?.completed ?? 0) + (current?.failed ?? 0);
176
+ const priorTotal = prior?.total ?? 0;
177
+
178
+ return {
179
+ completed_7d: current?.completed ?? 0,
180
+ failed_7d: current?.failed ?? 0,
181
+ success_rate_7d: curTotal > 0 ? (current?.completed ?? 0) / curTotal : 1,
182
+ success_rate_prior_7d: priorTotal > 0 ? (prior?.completed ?? 0) / priorTotal : 1,
183
+ top_failure_kind: failures?.failure_kind ?? null,
184
+ };
185
+ }
186
+
187
+ async function queryScheduledTaskHealth(db: D1Database): Promise<number> {
188
+ const result = await db.prepare(`
189
+ SELECT COUNT(*) as total,
190
+ SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors
191
+ FROM task_runs
192
+ WHERE created_at > datetime('now', '-24 hours')
193
+ `).first<{ total: number; errors: number }>();
194
+
195
+ const total = result?.total ?? 0;
196
+ return total > 0 ? (result?.errors ?? 0) / total : 0;
197
+ }
198
+
199
+ async function queryClassifierMetrics(db: D1Database): Promise<{
200
+ classify_cast_rate: number;
201
+ user_correction_rate: number;
202
+ classify_cast_correction_rate: number;
203
+ }> {
204
+ const [totals, corrections] = await Promise.all([
205
+ db.prepare(`
206
+ SELECT COUNT(*) as total,
207
+ SUM(CASE WHEN summary LIKE '%[clf:classify-cast]%' THEN 1 ELSE 0 END) as classify_cast,
208
+ SUM(CASE WHEN intent_class = 'user_correction' THEN 1 ELSE 0 END) as user_corrections
209
+ FROM episodic_memory
210
+ WHERE created_at > datetime('now', '-48 hours')
211
+ AND channel != 'internal'
212
+ `).first<{ total: number; classify_cast: number; user_corrections: number }>(),
213
+
214
+ // Count user_corrections that follow a classify-cast episode in the same thread
215
+ db.prepare(`
216
+ SELECT COUNT(DISTINCT e2.id) as cc_corrections
217
+ FROM episodic_memory e2
218
+ INNER JOIN episodic_memory e1
219
+ ON e1.thread_id = e2.thread_id
220
+ AND e1.created_at < e2.created_at
221
+ WHERE e2.intent_class = 'user_correction'
222
+ AND e1.summary LIKE '%[clf:classify-cast]%'
223
+ AND e2.created_at > datetime('now', '-48 hours')
224
+ `).first<{ cc_corrections: number }>(),
225
+ ]);
226
+
227
+ const total = totals?.total ?? 0;
228
+ const ccTotal = totals?.classify_cast ?? 0;
229
+
230
+ return {
231
+ classify_cast_rate: total > 0 ? ccTotal / total : 0,
232
+ user_correction_rate: total > 0 ? (totals?.user_corrections ?? 0) / total : 0,
233
+ classify_cast_correction_rate: ccTotal > 0 ? (corrections?.cc_corrections ?? 0) / ccTotal : 0,
234
+ };
235
+ }
236
+
237
+ async function queryMemoryCount(db: D1Database): Promise<number> {
238
+ const result = await db.prepare(
239
+ "SELECT COUNT(*) as cnt FROM memory_entries WHERE valid_until IS NULL"
240
+ ).first<{ cnt: number }>();
241
+ return result?.cnt ?? 0;
242
+ }
243
+
244
+ // ─── Composite Score ─────────────────────────────────────────
245
+ // Weighted average of key health indicators, 0-100 scale.
246
+
247
+ function computeScore(snapshot: Omit<CognitiveSnapshot, 'cognitive_score' | 'score_delta' | 'computed_at'>): number {
248
+ const weights = {
249
+ dispatch_success: 25, // most important — is the kernel routing correctly?
250
+ procedure_convergence: 20, // is learning working?
251
+ task_success: 20, // is autonomous work succeeding?
252
+ cost_efficiency: 15, // is cost trending down?
253
+ routing_stability: 10, // low reclassification = stable routing
254
+ scheduled_health: 10, // are cron tasks healthy?
255
+ };
256
+
257
+ const scores = {
258
+ dispatch_success: snapshot.dispatch_success_rate_7d * 100,
259
+ procedure_convergence: snapshot.procedure_convergence_rate * 100,
260
+ task_success: snapshot.task_success_rate_7d * 100,
261
+ cost_efficiency: snapshot.avg_cost_prior_7d > 0
262
+ ? Math.min(100, (1 - (snapshot.avg_cost_7d - snapshot.avg_cost_prior_7d) / snapshot.avg_cost_prior_7d) * 50 + 50)
263
+ : 50, // neutral if no prior data
264
+ routing_stability: (1 - snapshot.reclassification_rate_7d) * 100,
265
+ scheduled_health: (1 - snapshot.scheduled_task_error_rate_24h) * 100,
266
+ };
267
+
268
+ let weighted = 0;
269
+ let totalWeight = 0;
270
+ for (const [key, weight] of Object.entries(weights)) {
271
+ weighted += (scores[key as keyof typeof scores] ?? 50) * weight;
272
+ totalWeight += weight;
273
+ }
274
+
275
+ return Math.round(Math.max(0, Math.min(100, weighted / totalWeight)));
276
+ }
277
+
278
+ // ─── Storage ─────────────────────────────────────────────────
279
+
280
+ // Store snapshots in digest_sections for the Co-Founder Brief
281
+ async function storeSnapshot(db: D1Database, snapshot: CognitiveSnapshot): Promise<void> {
282
+ const payload = JSON.stringify({
283
+ type: 'cognitive_metrics',
284
+ ...snapshot,
285
+ });
286
+ await db.prepare(
287
+ "INSERT INTO digest_sections (section, payload) VALUES ('cognitive_metrics', ?)"
288
+ ).bind(payload).run();
289
+ }
290
+
291
+ async function getLastScore(db: D1Database): Promise<{ score: number; memory_count: number } | null> {
292
+ const row = await db.prepare(
293
+ "SELECT payload FROM digest_sections WHERE section = 'cognitive_metrics' ORDER BY created_at DESC LIMIT 1"
294
+ ).first<{ payload: string }>();
295
+
296
+ if (!row) return null;
297
+ try {
298
+ const parsed = JSON.parse(row.payload);
299
+ return { score: parsed.cognitive_score ?? 50, memory_count: parsed.memory_count ?? 0 };
300
+ } catch { return null; }
301
+ }
302
+
303
+ // ─── Main ────────────────────────────────────────────────────
304
+
305
+ export async function runCognitiveMetrics(env: EdgeEnv): Promise<void> {
306
+ // Daily gate — run once per day at 11 UTC (before the 12 UTC digest)
307
+ const hour = new Date().getUTCHours();
308
+ if (hour !== 11) return;
309
+
310
+ // Cooldown: 22 hours
311
+ const lastRun = await env.db.prepare(
312
+ "SELECT received_at FROM web_events WHERE event_id = 'cognitive_metrics'"
313
+ ).first<{ received_at: string }>();
314
+
315
+ if (lastRun) {
316
+ const elapsed = Date.now() - new Date(lastRun.received_at + 'Z').getTime();
317
+ if (elapsed < 22 * 60 * 60 * 1000) return;
318
+ }
319
+
320
+ // Gather all metrics in parallel
321
+ const [dispatch, procedural, tasks, scheduledHealth, memoryCount, lastSnapshot, classifier] = await Promise.all([
322
+ queryDispatchMetrics(env.db),
323
+ queryProceduralMetrics(env.db),
324
+ queryTaskMetrics(env.db),
325
+ queryScheduledTaskHealth(env.db),
326
+ queryMemoryCount(env.db),
327
+ getLastScore(env.db),
328
+ queryClassifierMetrics(env.db),
329
+ ]);
330
+
331
+ const partial = {
332
+ dispatch_success_rate_7d: dispatch.success_rate_7d,
333
+ dispatch_success_rate_prior_7d: dispatch.success_rate_prior_7d,
334
+ dispatch_volume_7d: dispatch.volume_7d,
335
+ avg_cost_7d: dispatch.avg_cost_7d,
336
+ avg_cost_prior_7d: dispatch.avg_cost_prior_7d,
337
+ avg_latency_ms_7d: dispatch.avg_latency_7d,
338
+ avg_latency_ms_prior_7d: dispatch.avg_latency_prior_7d,
339
+ reclassification_rate_7d: dispatch.reclassification_rate_7d,
340
+ procedures_learned: procedural.learned,
341
+ procedures_learning: procedural.learning,
342
+ procedures_degraded: procedural.degraded,
343
+ procedures_broken: procedural.broken,
344
+ procedure_convergence_rate: procedural.convergence_rate,
345
+ tasks_completed_7d: tasks.completed_7d,
346
+ tasks_failed_7d: tasks.failed_7d,
347
+ task_success_rate_7d: tasks.success_rate_7d,
348
+ task_success_rate_prior_7d: tasks.success_rate_prior_7d,
349
+ top_failure_kind: tasks.top_failure_kind,
350
+ scheduled_task_error_rate_24h: scheduledHealth,
351
+ classify_cast_rate_48h: classifier.classify_cast_rate,
352
+ user_correction_rate_48h: classifier.user_correction_rate,
353
+ classify_cast_correction_rate_48h: classifier.classify_cast_correction_rate,
354
+ memory_count: memoryCount,
355
+ memory_count_prior: lastSnapshot?.memory_count ?? memoryCount,
356
+ };
357
+
358
+ const score = computeScore(partial);
359
+ const scoreDelta = score - (lastSnapshot?.score ?? score);
360
+
361
+ const snapshot: CognitiveSnapshot = {
362
+ ...partial,
363
+ cognitive_score: score,
364
+ score_delta: scoreDelta,
365
+ computed_at: new Date().toISOString(),
366
+ };
367
+
368
+ await storeSnapshot(env.db, snapshot);
369
+
370
+ await env.db.prepare(
371
+ "INSERT OR REPLACE INTO web_events (event_id, received_at) VALUES ('cognitive_metrics', datetime('now'))"
372
+ ).run();
373
+
374
+ const arrow = scoreDelta > 0 ? '+' : scoreDelta < 0 ? '' : '=';
375
+ console.log(`[cognitive-metrics] Score: ${score}/100 (${arrow}${scoreDelta}) | dispatch: ${Math.round(dispatch.success_rate_7d * 100)}% | procedures: ${procedural.learned}/${procedural.learned + procedural.learning} learned | tasks: ${tasks.completed_7d}/${tasks.completed_7d + tasks.failed_7d}`);
376
+ console.log(`[cognitive-metrics] classifier: classify-cast=${Math.round(classifier.classify_cast_rate * 100)}% | correction_rate=${Math.round(classifier.user_correction_rate * 100)}% | cc_correction_rate=${Math.round(classifier.classify_cast_correction_rate * 100)}%`);
377
+ }