@stackbilt/aegis-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +96 -0
- package/schema.sql +586 -0
- package/src/adapters/voice/cloudflare-agent.ts +34 -0
- package/src/auth.ts +124 -0
- package/src/bluesky.ts +464 -0
- package/src/claude-tools/content.ts +188 -0
- package/src/claude-tools/email.ts +69 -0
- package/src/claude-tools/github.ts +440 -0
- package/src/claude-tools/goals.ts +116 -0
- package/src/claude-tools/index.ts +353 -0
- package/src/claude-tools/web.ts +59 -0
- package/src/claude.ts +406 -0
- package/src/codebeast.ts +200 -0
- package/src/composite.ts +715 -0
- package/src/content/column.ts +80 -0
- package/src/content/hero-image.ts +47 -0
- package/src/content/index.ts +27 -0
- package/src/content/journal.ts +91 -0
- package/src/content/roundtable.ts +163 -0
- package/src/core.ts +309 -0
- package/src/dashboard.ts +620 -0
- package/src/decision-docs.ts +284 -0
- package/src/dispatch.ts +13 -0
- package/src/edge-env.ts +58 -0
- package/src/email.ts +850 -0
- package/src/exports.ts +156 -0
- package/src/github-projects.ts +312 -0
- package/src/github.ts +670 -0
- package/src/groq.ts +247 -0
- package/src/health-page.ts +578 -0
- package/src/index.ts +89 -0
- package/src/kernel/argus-actions.ts +397 -0
- package/src/kernel/argus-correlation.ts +639 -0
- package/src/kernel/board.ts +91 -0
- package/src/kernel/briefing.ts +177 -0
- package/src/kernel/classify-memory-topic.ts +166 -0
- package/src/kernel/cognition.ts +377 -0
- package/src/kernel/court-cards.ts +163 -0
- package/src/kernel/dispatch.ts +587 -0
- package/src/kernel/domain.ts +50 -0
- package/src/kernel/dynamic-tools.ts +322 -0
- package/src/kernel/executor-port.ts +45 -0
- package/src/kernel/executors/claude.ts +73 -0
- package/src/kernel/executors/direct.ts +237 -0
- package/src/kernel/executors/groq.ts +18 -0
- package/src/kernel/executors/index.ts +87 -0
- package/src/kernel/executors/tarotscript.ts +104 -0
- package/src/kernel/executors/workers-ai.ts +54 -0
- package/src/kernel/insight-cache.ts +76 -0
- package/src/kernel/memory/agenda.ts +200 -0
- package/src/kernel/memory/blocks.ts +188 -0
- package/src/kernel/memory/consolidation.ts +194 -0
- package/src/kernel/memory/episodic.ts +241 -0
- package/src/kernel/memory/goals.ts +156 -0
- package/src/kernel/memory/graph.ts +290 -0
- package/src/kernel/memory/index.ts +11 -0
- package/src/kernel/memory/insights.ts +316 -0
- package/src/kernel/memory/procedural.ts +467 -0
- package/src/kernel/memory/pruning.ts +67 -0
- package/src/kernel/memory/recall.ts +367 -0
- package/src/kernel/memory/semantic.ts +315 -0
- package/src/kernel/memory/synthesis.ts +161 -0
- package/src/kernel/memory-adapter.ts +369 -0
- package/src/kernel/memory-guardrails.ts +76 -0
- package/src/kernel/port.ts +23 -0
- package/src/kernel/resilience.ts +322 -0
- package/src/kernel/router.ts +471 -0
- package/src/kernel/scheduled/agent-dispatch.ts +252 -0
- package/src/kernel/scheduled/argus-analytics.ts +247 -0
- package/src/kernel/scheduled/argus-heartbeat.ts +320 -0
- package/src/kernel/scheduled/argus-notify.ts +348 -0
- package/src/kernel/scheduled/board-sync.ts +110 -0
- package/src/kernel/scheduled/ci-watcher.ts +125 -0
- package/src/kernel/scheduled/cognitive-metrics.ts +377 -0
- package/src/kernel/scheduled/consolidation.ts +229 -0
- package/src/kernel/scheduled/content-drip.ts +47 -0
- package/src/kernel/scheduled/content.ts +6 -0
- package/src/kernel/scheduled/conversation-facts.ts +204 -0
- package/src/kernel/scheduled/cost-report.ts +84 -0
- package/src/kernel/scheduled/curiosity.ts +219 -0
- package/src/kernel/scheduled/dev-activity.ts +44 -0
- package/src/kernel/scheduled/digest.ts +317 -0
- package/src/kernel/scheduled/dreaming/agenda-triage.ts +115 -0
- package/src/kernel/scheduled/dreaming/facts.ts +239 -0
- package/src/kernel/scheduled/dreaming/index.ts +8 -0
- package/src/kernel/scheduled/dreaming/llm.ts +33 -0
- package/src/kernel/scheduled/dreaming/pattern-synthesis.ts +124 -0
- package/src/kernel/scheduled/dreaming/persona.ts +75 -0
- package/src/kernel/scheduled/dreaming/symbolic.ts +31 -0
- package/src/kernel/scheduled/dreaming/task-proposals.ts +80 -0
- package/src/kernel/scheduled/dreaming.ts +66 -0
- package/src/kernel/scheduled/entropy.ts +149 -0
- package/src/kernel/scheduled/escalation.ts +192 -0
- package/src/kernel/scheduled/feed-watcher.ts +206 -0
- package/src/kernel/scheduled/goals.ts +214 -0
- package/src/kernel/scheduled/governance.ts +41 -0
- package/src/kernel/scheduled/heartbeat.ts +220 -0
- package/src/kernel/scheduled/inbox-processor.ts +174 -0
- package/src/kernel/scheduled/index.ts +245 -0
- package/src/kernel/scheduled/issue-proposer.ts +478 -0
- package/src/kernel/scheduled/issue-watcher.ts +128 -0
- package/src/kernel/scheduled/pr-automerge.ts +213 -0
- package/src/kernel/scheduled/product-health.ts +107 -0
- package/src/kernel/scheduled/reflection.ts +373 -0
- package/src/kernel/scheduled/self-improvement.ts +114 -0
- package/src/kernel/scheduled/social-engage.ts +175 -0
- package/src/kernel/scheduled/task-audit.ts +60 -0
- package/src/kernel/symbolic.ts +156 -0
- package/src/kernel/types.ts +145 -0
- package/src/landing.ts +1190 -0
- package/src/lib/audit-chain/chain.ts +28 -0
- package/src/lib/audit-chain/types.ts +12 -0
- package/src/lib/observability/errors.ts +55 -0
- package/src/markdown.ts +164 -0
- package/src/mcp/handlers.ts +647 -0
- package/src/mcp/server.ts +184 -0
- package/src/mcp/tools.ts +316 -0
- package/src/mcp-client.ts +275 -0
- package/src/mcp-server.ts +2 -0
- package/src/operator/config.example.ts +60 -0
- package/src/operator/config.ts +60 -0
- package/src/operator/index.ts +46 -0
- package/src/operator/persona.example.ts +34 -0
- package/src/operator/persona.ts +34 -0
- package/src/operator/prompt-builder.ts +190 -0
- package/src/operator/types.ts +43 -0
- package/src/pulse.ts +1179 -0
- package/src/routes/bluesky.ts +116 -0
- package/src/routes/cc-tasks.ts +328 -0
- package/src/routes/codebeast.ts +1 -0
- package/src/routes/content.ts +194 -0
- package/src/routes/conversations.ts +25 -0
- package/src/routes/dynamic-tools.ts +111 -0
- package/src/routes/feedback.ts +192 -0
- package/src/routes/health.ts +147 -0
- package/src/routes/messages.ts +228 -0
- package/src/routes/observability.ts +82 -0
- package/src/routes/operator-logs.ts +42 -0
- package/src/routes/pages.ts +96 -0
- package/src/routes/sessions.ts +54 -0
- package/src/sanitize.ts +73 -0
- package/src/schema-enums.ts +155 -0
- package/src/search.ts +112 -0
- package/src/task-intelligence.ts +497 -0
- package/src/types.ts +194 -0
- package/src/ui.ts +5 -0
- package/src/version.ts +3 -0
- package/src/workers-ai-chat.ts +333 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// --- Project Board State Manager ---
|
|
2
|
+
// D1 CRUD for board_items. Called by ARGUS webhooks, issue-watcher,
|
|
3
|
+
// CC task lifecycle, and board-sync reconciliation.
|
|
4
|
+
|
|
5
|
+
import type { BoardColumn } from '../schema-enums.js';
|
|
6
|
+
export type { BoardColumn } from '../schema-enums.js';
|
|
7
|
+
|
|
8
|
+
export interface BoardItem {
|
|
9
|
+
id: string;
|
|
10
|
+
project_id: string;
|
|
11
|
+
content_type: 'issue' | 'pr';
|
|
12
|
+
content_node_id: string;
|
|
13
|
+
repo: string;
|
|
14
|
+
number: number;
|
|
15
|
+
title: string;
|
|
16
|
+
status: BoardColumn;
|
|
17
|
+
labels: string;
|
|
18
|
+
assignee: string | null;
|
|
19
|
+
cc_task_id: string | null;
|
|
20
|
+
updated_at: string;
|
|
21
|
+
synced_at: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- Config ---
|
|
25
|
+
// Override via operator config for your org
|
|
26
|
+
export const PROJECT_TITLE = 'AEGIS Operations';
|
|
27
|
+
export const ORG_LOGIN = 'ExampleOrg';
|
|
28
|
+
|
|
29
|
+
// --- D1 Operations ---
|
|
30
|
+
|
|
31
|
+
/** Upsert a board item into D1. */
|
|
32
|
+
export async function upsertBoardItem(db: D1Database, item: Partial<BoardItem> & { repo: string; number: number }): Promise<void> {
|
|
33
|
+
await db.prepare(`
|
|
34
|
+
INSERT INTO board_items (id, project_id, content_type, content_node_id, repo, number, title, status, labels, assignee, cc_task_id, updated_at, synced_at)
|
|
35
|
+
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, datetime('now'), datetime('now'))
|
|
36
|
+
ON CONFLICT(repo, number) DO UPDATE SET
|
|
37
|
+
id = COALESCE(?1, board_items.id),
|
|
38
|
+
project_id = COALESCE(?2, board_items.project_id),
|
|
39
|
+
content_type = COALESCE(?3, board_items.content_type),
|
|
40
|
+
content_node_id = COALESCE(?4, board_items.content_node_id),
|
|
41
|
+
title = COALESCE(?7, board_items.title),
|
|
42
|
+
status = COALESCE(?8, board_items.status),
|
|
43
|
+
labels = COALESCE(?9, board_items.labels),
|
|
44
|
+
assignee = COALESCE(?10, board_items.assignee),
|
|
45
|
+
cc_task_id = COALESCE(?11, board_items.cc_task_id),
|
|
46
|
+
updated_at = datetime('now'),
|
|
47
|
+
synced_at = datetime('now')
|
|
48
|
+
`).bind(
|
|
49
|
+
item.id ?? null,
|
|
50
|
+
item.project_id ?? null,
|
|
51
|
+
item.content_type ?? null,
|
|
52
|
+
item.content_node_id ?? null,
|
|
53
|
+
item.repo,
|
|
54
|
+
item.number,
|
|
55
|
+
item.title ?? null,
|
|
56
|
+
item.status ?? null,
|
|
57
|
+
item.labels ?? null,
|
|
58
|
+
item.assignee ?? null,
|
|
59
|
+
item.cc_task_id ?? null,
|
|
60
|
+
).run();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function ensureOnBoard(
|
|
64
|
+
db: D1Database,
|
|
65
|
+
githubToken: string,
|
|
66
|
+
projectId: string,
|
|
67
|
+
repo: string,
|
|
68
|
+
issueNumber: number,
|
|
69
|
+
title: string,
|
|
70
|
+
status: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
throw new Error('not implemented');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function moveBoardItemLocal(
|
|
76
|
+
db: D1Database,
|
|
77
|
+
repo: string,
|
|
78
|
+
issueNumber: number,
|
|
79
|
+
status: 'queued' | 'in_progress' | 'shipped' | 'blocked' | 'backlog',
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
throw new Error('not implemented');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function linkTaskToBoard(
|
|
85
|
+
db: D1Database,
|
|
86
|
+
taskId: string,
|
|
87
|
+
repo: string,
|
|
88
|
+
issueNumber: number,
|
|
89
|
+
): Promise<void> {
|
|
90
|
+
throw new Error('not implemented');
|
|
91
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Re-entry Briefing: "Last time on the operator's empire"
|
|
2
|
+
// Generates a concise situational briefing when the operator starts a session.
|
|
3
|
+
// Thinks like the operator: what happened, what needs me, what's AEGIS working on.
|
|
4
|
+
// No fluff, no preamble. Answer before the question is asked.
|
|
5
|
+
|
|
6
|
+
import type { EdgeEnv } from './dispatch.js';
|
|
7
|
+
|
|
8
|
+
export interface Briefing {
|
|
9
|
+
// What happened while you were away
|
|
10
|
+
shipped: string[]; // tasks completed, PRs merged
|
|
11
|
+
failed: string[]; // tasks that need attention
|
|
12
|
+
// What needs you right now
|
|
13
|
+
decisions: string[]; // proposed actions, stale agenda, approvals needed
|
|
14
|
+
// What AEGIS is working on
|
|
15
|
+
active: string[]; // running tasks, pending queue
|
|
16
|
+
// Signals
|
|
17
|
+
alerts: string[]; // ARGUS events, health issues
|
|
18
|
+
// One-liner state
|
|
19
|
+
cognitiveScore: number | null;
|
|
20
|
+
eventCount24h: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function generateBriefing(env: EdgeEnv): Promise<Briefing> {
|
|
24
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
|
25
|
+
|
|
26
|
+
// All queries in parallel — fast
|
|
27
|
+
const [
|
|
28
|
+
completedTasks,
|
|
29
|
+
failedTasks,
|
|
30
|
+
pendingProposed,
|
|
31
|
+
runningTasks,
|
|
32
|
+
pendingTasks,
|
|
33
|
+
staleAgenda,
|
|
34
|
+
recentEvents,
|
|
35
|
+
healthAlerts,
|
|
36
|
+
cogMetrics,
|
|
37
|
+
] = await Promise.all([
|
|
38
|
+
// Shipped in last 24h
|
|
39
|
+
env.db.prepare(
|
|
40
|
+
"SELECT title, repo, pr_url FROM cc_tasks WHERE status = 'completed' AND completed_at > ? ORDER BY completed_at DESC LIMIT 5"
|
|
41
|
+
).bind(since).all<{ title: string; repo: string; pr_url: string | null }>(),
|
|
42
|
+
|
|
43
|
+
// Failed in last 24h
|
|
44
|
+
env.db.prepare(
|
|
45
|
+
"SELECT title, repo, error FROM cc_tasks WHERE status = 'failed' AND completed_at > ? ORDER BY completed_at DESC LIMIT 3"
|
|
46
|
+
).bind(since).all<{ title: string; repo: string; error: string | null }>(),
|
|
47
|
+
|
|
48
|
+
// Proposed tasks awaiting approval
|
|
49
|
+
env.db.prepare(
|
|
50
|
+
"SELECT title, repo FROM cc_tasks WHERE authority = 'proposed' AND status = 'pending' ORDER BY created_at ASC LIMIT 5"
|
|
51
|
+
).all<{ title: string; repo: string }>(),
|
|
52
|
+
|
|
53
|
+
// Currently running
|
|
54
|
+
env.db.prepare(
|
|
55
|
+
"SELECT title, repo FROM cc_tasks WHERE status = 'running' LIMIT 3"
|
|
56
|
+
).all<{ title: string; repo: string }>(),
|
|
57
|
+
|
|
58
|
+
// Pending queue depth
|
|
59
|
+
env.db.prepare(
|
|
60
|
+
"SELECT COUNT(*) as cnt FROM cc_tasks WHERE status = 'pending'"
|
|
61
|
+
).first<{ cnt: number }>(),
|
|
62
|
+
|
|
63
|
+
// High-priority agenda items older than 3 days (stale = needs attention)
|
|
64
|
+
env.db.prepare(
|
|
65
|
+
"SELECT item FROM agent_agenda WHERE status = 'active' AND priority = 'high' AND created_at < datetime('now', '-3 days') ORDER BY created_at ASC LIMIT 3"
|
|
66
|
+
).all<{ item: string }>(),
|
|
67
|
+
|
|
68
|
+
// ARGUS events in last 24h
|
|
69
|
+
env.db.prepare(
|
|
70
|
+
"SELECT COUNT(*) as cnt FROM events WHERE ts > ?"
|
|
71
|
+
).bind(since).first<{ cnt: number }>(),
|
|
72
|
+
|
|
73
|
+
// Recent health alerts (non-ok heartbeats in last 24h)
|
|
74
|
+
env.db.prepare(
|
|
75
|
+
"SELECT severity, summary FROM heartbeat_results WHERE severity IN ('high', 'critical') AND created_at > ? ORDER BY created_at DESC LIMIT 2"
|
|
76
|
+
).bind(since).all<{ severity: string; summary: string }>(),
|
|
77
|
+
|
|
78
|
+
// Latest cognitive score
|
|
79
|
+
env.db.prepare(
|
|
80
|
+
"SELECT payload FROM digest_sections WHERE section = 'cognitive_metrics' ORDER BY created_at DESC LIMIT 1"
|
|
81
|
+
).first<{ payload: string }>(),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
// Parse cognitive score
|
|
85
|
+
let cognitiveScore: number | null = null;
|
|
86
|
+
if (cogMetrics?.payload) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(cogMetrics.payload);
|
|
89
|
+
cognitiveScore = parsed.cognitive_score ?? null;
|
|
90
|
+
} catch { /* skip */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build briefing
|
|
94
|
+
const shipped = completedTasks.results.map(t =>
|
|
95
|
+
t.pr_url ? `${t.title} (${t.repo}) — PR: ${t.pr_url}` : `${t.title} (${t.repo})`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const failed = failedTasks.results.map(t =>
|
|
99
|
+
`${t.title} (${t.repo})${t.error ? ` — ${t.error.slice(0, 80)}` : ''}`
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const decisions: string[] = [];
|
|
103
|
+
if (pendingProposed.results.length > 0) {
|
|
104
|
+
decisions.push(`${pendingProposed.results.length} proposed task${pendingProposed.results.length > 1 ? 's' : ''} awaiting approval: ${pendingProposed.results.map(t => t.title.slice(0, 50)).join(', ')}`);
|
|
105
|
+
}
|
|
106
|
+
for (const item of staleAgenda.results) {
|
|
107
|
+
decisions.push(item.item.slice(0, 100));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const active: string[] = [];
|
|
111
|
+
for (const t of runningTasks.results) {
|
|
112
|
+
active.push(`Running: ${t.title} (${t.repo})`);
|
|
113
|
+
}
|
|
114
|
+
const queueDepth = pendingTasks?.cnt ?? 0;
|
|
115
|
+
if (queueDepth > 0) {
|
|
116
|
+
active.push(`${queueDepth} task${queueDepth > 1 ? 's' : ''} in queue`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const alerts: string[] = [];
|
|
120
|
+
for (const h of healthAlerts.results) {
|
|
121
|
+
alerts.push(`[${h.severity.toUpperCase()}] ${h.summary.slice(0, 100)}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
shipped,
|
|
126
|
+
failed,
|
|
127
|
+
decisions,
|
|
128
|
+
active,
|
|
129
|
+
alerts,
|
|
130
|
+
cognitiveScore,
|
|
131
|
+
eventCount24h: recentEvents?.cnt ?? 0,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Format briefing as concise text for injection into greeting response
|
|
136
|
+
export function formatBriefing(b: Briefing): string {
|
|
137
|
+
const sections: string[] = [];
|
|
138
|
+
|
|
139
|
+
// Header
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
142
|
+
const day = dayNames[now.getUTCDay()];
|
|
143
|
+
const scoreStr = b.cognitiveScore !== null ? ` | Cognitive: ${b.cognitiveScore}/100` : '';
|
|
144
|
+
sections.push(`**${day}** — ${b.eventCount24h} events in last 24h${scoreStr}`);
|
|
145
|
+
|
|
146
|
+
// What needs you (decisions first — this is what the operator looks for)
|
|
147
|
+
if (b.decisions.length > 0) {
|
|
148
|
+
sections.push(`\n**Needs you:**\n${b.decisions.map(d => `- ${d}`).join('\n')}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// What shipped
|
|
152
|
+
if (b.shipped.length > 0) {
|
|
153
|
+
sections.push(`\n**Shipped (24h):**\n${b.shipped.map(s => `- ${s}`).join('\n')}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// What failed
|
|
157
|
+
if (b.failed.length > 0) {
|
|
158
|
+
sections.push(`\n**Failed:**\n${b.failed.map(f => `- ${f}`).join('\n')}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Alerts
|
|
162
|
+
if (b.alerts.length > 0) {
|
|
163
|
+
sections.push(`\n**Alerts:**\n${b.alerts.map(a => `- ${a}`).join('\n')}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// What I'm working on
|
|
167
|
+
if (b.active.length > 0) {
|
|
168
|
+
sections.push(`\n**Active:**\n${b.active.map(a => `- ${a}`).join('\n')}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Nothing happening
|
|
172
|
+
if (b.shipped.length === 0 && b.failed.length === 0 && b.decisions.length === 0 && b.alerts.length === 0) {
|
|
173
|
+
sections.push('\nQuiet 24h. Nothing on fire, nothing shipped. What do you want to work on?');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sections.join('\n');
|
|
177
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Memory topic classification via TarotScript memory-topic-classify spread.
|
|
2
|
+
//
|
|
3
|
+
// The spread lives at Stackbilt-dev/tarotscript:spreads/memory-topic-classify.tarot
|
|
4
|
+
// and draws from decks/memory-topics (15 canonical topics for AEGIS semantic
|
|
5
|
+
// memory). See artifacts/tarotscript-classifier-migration.md in aegis-daemon
|
|
6
|
+
// for the broader design and rollout plan.
|
|
7
|
+
//
|
|
8
|
+
// Contract:
|
|
9
|
+
// classifyMemoryTopic(fetcher, fact) → {
|
|
10
|
+
// topic: canonical topic string,
|
|
11
|
+
// confidence: 'high' | 'moderate' | 'low',
|
|
12
|
+
// source: 'classifier' | 'fallback',
|
|
13
|
+
// element?: element of drawn card (debugging aid),
|
|
14
|
+
// }
|
|
15
|
+
//
|
|
16
|
+
// When the classifier returns confidence='low', OR when the classification
|
|
17
|
+
// lands on the 'general' fallback card, OR when the fetcher call fails for
|
|
18
|
+
// any reason, the helper falls through to { topic: 'general', source: 'fallback' }.
|
|
19
|
+
// Callers who want the raw classifier output can use runMemoryTopicClassification
|
|
20
|
+
// directly instead.
|
|
21
|
+
//
|
|
22
|
+
// Telemetry: the helper does not log or persist anything itself. Callers are
|
|
23
|
+
// responsible for recording divergence between operator-provided topics and
|
|
24
|
+
// classifier-inferred topics during the observation rollout.
|
|
25
|
+
|
|
26
|
+
// The set of canonical topics the classifier is trained to emit. Kept in sync
|
|
27
|
+
// with decks/memory-topics.deck in the tarotscript repo. If the deck adds a
|
|
28
|
+
// new card, also update this list so typecheck catches stale consumers.
|
|
29
|
+
export type CanonicalMemoryTopic =
|
|
30
|
+
| 'operator'
|
|
31
|
+
| 'feedback'
|
|
32
|
+
| 'business_ops'
|
|
33
|
+
| 'compliance'
|
|
34
|
+
| 'execution'
|
|
35
|
+
| 'content'
|
|
36
|
+
| 'research'
|
|
37
|
+
| 'reflection'
|
|
38
|
+
| 'cross_repo_intelligence'
|
|
39
|
+
| 'feed_intel'
|
|
40
|
+
| 'aegis'
|
|
41
|
+
| 'tarotscript'
|
|
42
|
+
| 'infrastructure'
|
|
43
|
+
| 'portfolio_project'
|
|
44
|
+
| 'general';
|
|
45
|
+
|
|
46
|
+
export const CANONICAL_MEMORY_TOPICS: readonly CanonicalMemoryTopic[] = [
|
|
47
|
+
'operator', 'feedback', 'business_ops', 'compliance', 'execution',
|
|
48
|
+
'content', 'research', 'reflection', 'cross_repo_intelligence',
|
|
49
|
+
'feed_intel', 'aegis', 'tarotscript', 'infrastructure',
|
|
50
|
+
'portfolio_project', 'general',
|
|
51
|
+
] as const;
|
|
52
|
+
|
|
53
|
+
export type ClassificationConfidence = 'high' | 'moderate' | 'low';
|
|
54
|
+
|
|
55
|
+
export interface MemoryTopicClassification {
|
|
56
|
+
topic: CanonicalMemoryTopic;
|
|
57
|
+
confidence: ClassificationConfidence;
|
|
58
|
+
source: 'classifier' | 'fallback';
|
|
59
|
+
element?: string;
|
|
60
|
+
receipt_hash?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Raw classification call. Invokes the memory-topic-classify spread on the
|
|
65
|
+
* tarotscript worker and returns the parsed facts object, or null if the
|
|
66
|
+
* call fails for any reason (network, auth, worker down, spread error).
|
|
67
|
+
* The helper wrapper `classifyMemoryTopic` applies fallback semantics on
|
|
68
|
+
* top of this; most callers should use the wrapper instead.
|
|
69
|
+
*/
|
|
70
|
+
export async function runMemoryTopicClassification(
|
|
71
|
+
fetcher: Fetcher,
|
|
72
|
+
fact: string,
|
|
73
|
+
opts: { seed?: number } = {},
|
|
74
|
+
): Promise<{
|
|
75
|
+
classification?: string;
|
|
76
|
+
classification_confidence?: string;
|
|
77
|
+
classification_element?: string;
|
|
78
|
+
receipt_hash?: string;
|
|
79
|
+
} | null> {
|
|
80
|
+
try {
|
|
81
|
+
const response = await fetcher.fetch('https://internal/run', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
spreadType: 'memory-topic-classify',
|
|
86
|
+
querent: {
|
|
87
|
+
id: 'aegis',
|
|
88
|
+
intention: fact,
|
|
89
|
+
},
|
|
90
|
+
seed: opts.seed,
|
|
91
|
+
inscribe: false, // classification is metadata, not a consultation
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
console.warn(`[classify-memory-topic] worker returned ${response.status}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const body = await response.json() as {
|
|
101
|
+
facts?: Record<string, string>;
|
|
102
|
+
receipt?: { hash?: string };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
classification: body.facts?.classification,
|
|
107
|
+
classification_confidence: body.facts?.classification_confidence,
|
|
108
|
+
classification_element: body.facts?.classification_element,
|
|
109
|
+
receipt_hash: body.receipt?.hash,
|
|
110
|
+
};
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.warn(
|
|
113
|
+
`[classify-memory-topic] fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isCanonicalTopic(value: string | undefined): value is CanonicalMemoryTopic {
|
|
120
|
+
return typeof value === 'string'
|
|
121
|
+
&& (CANONICAL_MEMORY_TOPICS as readonly string[]).includes(value);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* High-level classifier with fallback. Calls the memory-topic-classify spread,
|
|
126
|
+
* validates the output, and returns 'general' as a stable fallback when:
|
|
127
|
+
* - the fetcher call fails (network, auth, worker down)
|
|
128
|
+
* - the spread returns an unknown topic (deck drift — log + fall through)
|
|
129
|
+
* - the classification confidence is 'low' (deck scoring was ambiguous)
|
|
130
|
+
*
|
|
131
|
+
* The fallback is 'general' on purpose — that card exists in the deck for
|
|
132
|
+
* exactly this reason and callers can filter on `source === 'fallback'` to
|
|
133
|
+
* distinguish model output from fall-through behavior.
|
|
134
|
+
*/
|
|
135
|
+
export async function classifyMemoryTopic(
|
|
136
|
+
fetcher: Fetcher,
|
|
137
|
+
fact: string,
|
|
138
|
+
opts: { seed?: number } = {},
|
|
139
|
+
): Promise<MemoryTopicClassification> {
|
|
140
|
+
const raw = await runMemoryTopicClassification(fetcher, fact, opts);
|
|
141
|
+
|
|
142
|
+
if (!raw) {
|
|
143
|
+
return { topic: 'general', confidence: 'low', source: 'fallback' };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const confidence = (raw.classification_confidence ?? 'low') as ClassificationConfidence;
|
|
147
|
+
|
|
148
|
+
if (confidence === 'low') {
|
|
149
|
+
return { topic: 'general', confidence: 'low', source: 'fallback' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!isCanonicalTopic(raw.classification)) {
|
|
153
|
+
console.warn(
|
|
154
|
+
`[classify-memory-topic] unknown canonical topic from classifier: "${raw.classification}" — falling through to 'general'`,
|
|
155
|
+
);
|
|
156
|
+
return { topic: 'general', confidence, source: 'fallback' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
topic: raw.classification,
|
|
161
|
+
confidence,
|
|
162
|
+
source: 'classifier',
|
|
163
|
+
element: raw.classification_element,
|
|
164
|
+
receipt_hash: raw.receipt_hash,
|
|
165
|
+
};
|
|
166
|
+
}
|