opencodekit 0.20.2 → 0.20.4

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 (57) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/agent/build.md +4 -0
  3. package/dist/template/.opencode/agent/explore.md +4 -0
  4. package/dist/template/.opencode/agent/general.md +4 -0
  5. package/dist/template/.opencode/agent/plan.md +4 -0
  6. package/dist/template/.opencode/agent/review.md +4 -0
  7. package/dist/template/.opencode/agent/scout.md +4 -0
  8. package/dist/template/.opencode/command/create.md +119 -25
  9. package/dist/template/.opencode/command/design.md +1 -2
  10. package/dist/template/.opencode/command/health.md +234 -0
  11. package/dist/template/.opencode/command/init-user.md +15 -0
  12. package/dist/template/.opencode/command/plan.md +3 -4
  13. package/dist/template/.opencode/command/pr.md +13 -0
  14. package/dist/template/.opencode/command/research.md +15 -3
  15. package/dist/template/.opencode/command/review-codebase.md +11 -1
  16. package/dist/template/.opencode/command/ship.md +72 -8
  17. package/dist/template/.opencode/command/status.md +1 -1
  18. package/dist/template/.opencode/command/ui-review.md +0 -1
  19. package/dist/template/.opencode/command/ui-slop-check.md +1 -1
  20. package/dist/template/.opencode/command/verify.md +11 -1
  21. package/dist/template/.opencode/memory.db +0 -0
  22. package/dist/template/.opencode/memory.db-shm +0 -0
  23. package/dist/template/.opencode/memory.db-wal +0 -0
  24. package/dist/template/.opencode/opencode.json +1678 -1677
  25. package/dist/template/.opencode/plugin/README.md +1 -1
  26. package/dist/template/.opencode/plugin/lib/compact.ts +194 -0
  27. package/dist/template/.opencode/plugin/lib/compile.ts +253 -0
  28. package/dist/template/.opencode/plugin/lib/db/graph.ts +253 -0
  29. package/dist/template/.opencode/plugin/lib/db/observations.ts +8 -3
  30. package/dist/template/.opencode/plugin/lib/db/schema.ts +96 -5
  31. package/dist/template/.opencode/plugin/lib/db/types.ts +73 -0
  32. package/dist/template/.opencode/plugin/lib/index-generator.ts +170 -0
  33. package/dist/template/.opencode/plugin/lib/lint.ts +359 -0
  34. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +78 -4
  35. package/dist/template/.opencode/plugin/lib/memory-db.ts +19 -1
  36. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +30 -0
  37. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +10 -0
  38. package/dist/template/.opencode/plugin/lib/memory-tools.ts +167 -2
  39. package/dist/template/.opencode/plugin/lib/operation-log.ts +109 -0
  40. package/dist/template/.opencode/plugin/lib/validate.ts +243 -0
  41. package/dist/template/.opencode/plugin/memory.ts +2 -1
  42. package/dist/template/.opencode/skill/design-taste-frontend/SKILL.md +13 -1
  43. package/dist/template/.opencode/skill/figma-go/SKILL.md +1 -1
  44. package/dist/template/.opencode/skill/full-output-enforcement/SKILL.md +13 -0
  45. package/dist/template/.opencode/skill/high-end-visual-design/SKILL.md +13 -0
  46. package/dist/template/.opencode/skill/industrial-brutalist-ui/SKILL.md +13 -0
  47. package/dist/template/.opencode/skill/memory-system/SKILL.md +65 -1
  48. package/dist/template/.opencode/skill/minimalist-ui/SKILL.md +13 -0
  49. package/dist/template/.opencode/skill/redesign-existing-projects/SKILL.md +13 -0
  50. package/dist/template/.opencode/skill/requesting-code-review/SKILL.md +48 -2
  51. package/dist/template/.opencode/skill/requesting-code-review/references/specialist-profiles.md +108 -0
  52. package/dist/template/.opencode/skill/skill-creator/SKILL.md +25 -0
  53. package/dist/template/.opencode/skill/stitch-design-taste/SKILL.md +13 -0
  54. package/dist/template/.opencode/skill/verification-before-completion/SKILL.md +46 -0
  55. package/package.json +1 -1
  56. package/dist/template/.opencode/agent/runner.md +0 -79
  57. package/dist/template/.opencode/command/start.md +0 -156
@@ -28,10 +28,11 @@ export function storeObservation(input: ObservationInput): number {
28
28
  .query(
29
29
  `
30
30
  INSERT INTO observations (
31
- type, title, subtitle, facts, narrative, concepts,
31
+ type, title, subtitle, facts, narrative, raw_source, concepts,
32
32
  files_read, files_modified, confidence, bead_id,
33
- supersedes, markdown_file, source, created_at, created_at_epoch
34
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33
+ supersedes, markdown_file, source, wing, hall, room,
34
+ created_at, created_at_epoch
35
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
35
36
  `,
36
37
  )
37
38
  .run(
@@ -40,6 +41,7 @@ export function storeObservation(input: ObservationInput): number {
40
41
  input.subtitle ?? null,
41
42
  input.facts ? JSON.stringify(input.facts) : null,
42
43
  input.narrative ?? null,
44
+ input.raw_source ?? null,
43
45
  input.concepts ? JSON.stringify(input.concepts) : null,
44
46
  input.files_read ? JSON.stringify(input.files_read) : null,
45
47
  input.files_modified ? JSON.stringify(input.files_modified) : null,
@@ -48,6 +50,9 @@ export function storeObservation(input: ObservationInput): number {
48
50
  input.supersedes ?? null,
49
51
  input.markdown_file ?? null,
50
52
  input.source ?? "manual",
53
+ input.wing ?? null,
54
+ input.hall ?? null,
55
+ input.room ?? null,
51
56
  now.toISOString(),
52
57
  now.getTime(),
53
58
  );
@@ -28,10 +28,10 @@ function logRecovery(message: string): void {
28
28
  }
29
29
 
30
30
  // ============================================================================
31
- // Schema v2
31
+ // Schema v3 (v2 + navigation, entity graph, raw source, chunk type)
32
32
  // ============================================================================
33
33
 
34
- const SCHEMA_VERSION = 2;
34
+ const SCHEMA_VERSION = 3;
35
35
 
36
36
  const SCHEMA_SQL = `
37
37
  -- Schema versioning for migrations
@@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS schema_versions (
41
41
  applied_at TEXT NOT NULL
42
42
  );
43
43
 
44
- -- Observations table (v2: added source column)
44
+ -- Observations table (v3: added raw_source, wing, hall, room)
45
45
  CREATE TABLE IF NOT EXISTS observations (
46
46
  id INTEGER PRIMARY KEY AUTOINCREMENT,
47
47
  type TEXT NOT NULL CHECK(type IN ('decision','bugfix','feature','pattern','discovery','learning','warning')),
@@ -49,6 +49,7 @@ CREATE TABLE IF NOT EXISTS observations (
49
49
  subtitle TEXT,
50
50
  facts TEXT,
51
51
  narrative TEXT,
52
+ raw_source TEXT,
52
53
  concepts TEXT,
53
54
  files_read TEXT,
54
55
  files_modified TEXT,
@@ -59,6 +60,9 @@ CREATE TABLE IF NOT EXISTS observations (
59
60
  valid_until TEXT,
60
61
  markdown_file TEXT,
61
62
  source TEXT CHECK(source IN ('manual','curator','imported')) DEFAULT 'manual',
63
+ wing TEXT,
64
+ hall TEXT CHECK(hall IS NULL OR hall IN ('facts','events','discoveries','preferences','advice')),
65
+ room TEXT,
62
66
  created_at TEXT NOT NULL,
63
67
  created_at_epoch INTEGER NOT NULL,
64
68
  updated_at TEXT,
@@ -84,6 +88,10 @@ CREATE INDEX IF NOT EXISTS idx_observations_created ON observations(created_at_e
84
88
  CREATE INDEX IF NOT EXISTS idx_observations_bead_id ON observations(bead_id);
85
89
  CREATE INDEX IF NOT EXISTS idx_observations_superseded ON observations(superseded_by) WHERE superseded_by IS NOT NULL;
86
90
  CREATE INDEX IF NOT EXISTS idx_observations_source ON observations(source);
91
+ CREATE INDEX IF NOT EXISTS idx_observations_wing ON observations(wing) WHERE wing IS NOT NULL;
92
+ CREATE INDEX IF NOT EXISTS idx_observations_hall ON observations(hall) WHERE hall IS NOT NULL;
93
+ CREATE INDEX IF NOT EXISTS idx_observations_room ON observations(room) WHERE room IS NOT NULL;
94
+ CREATE INDEX IF NOT EXISTS idx_observations_navigation ON observations(wing, hall, room) WHERE wing IS NOT NULL;
87
95
 
88
96
  -- Memory files table
89
97
  CREATE TABLE IF NOT EXISTS memory_files (
@@ -102,7 +110,7 @@ CREATE INDEX IF NOT EXISTS idx_memory_files_path ON memory_files(file_path);
102
110
 
103
111
 
104
112
 
105
- -- Temporal messages table (v2: raw message capture)
113
+ -- Temporal messages table (v3: added chunk_type)
106
114
  CREATE TABLE IF NOT EXISTS temporal_messages (
107
115
  id INTEGER PRIMARY KEY AUTOINCREMENT,
108
116
  session_id TEXT NOT NULL,
@@ -112,6 +120,7 @@ CREATE TABLE IF NOT EXISTS temporal_messages (
112
120
  token_estimate INTEGER NOT NULL DEFAULT 0,
113
121
  time_created INTEGER NOT NULL,
114
122
  distillation_id INTEGER,
123
+ chunk_type TEXT,
115
124
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
116
125
  FOREIGN KEY(distillation_id) REFERENCES distillations(id) ON DELETE SET NULL
117
126
  );
@@ -139,6 +148,27 @@ CREATE TABLE IF NOT EXISTS distillations (
139
148
  CREATE INDEX IF NOT EXISTS idx_distillations_session ON distillations(session_id, time_created DESC);
140
149
  CREATE INDEX IF NOT EXISTS idx_distillations_time ON distillations(time_created DESC);
141
150
 
151
+ -- Entity triples table (v3: temporal knowledge graph)
152
+ CREATE TABLE IF NOT EXISTS entity_triples (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ subject TEXT NOT NULL,
155
+ predicate TEXT NOT NULL,
156
+ object TEXT NOT NULL,
157
+ valid_from TEXT NOT NULL,
158
+ valid_to TEXT,
159
+ confidence REAL NOT NULL DEFAULT 1.0,
160
+ source_observation_id INTEGER,
161
+ created_at TEXT NOT NULL,
162
+ created_at_epoch INTEGER NOT NULL,
163
+ FOREIGN KEY(source_observation_id) REFERENCES observations(id) ON DELETE SET NULL
164
+ );
165
+
166
+ CREATE INDEX IF NOT EXISTS idx_entity_subject ON entity_triples(subject);
167
+ CREATE INDEX IF NOT EXISTS idx_entity_object ON entity_triples(object);
168
+ CREATE INDEX IF NOT EXISTS idx_entity_predicate ON entity_triples(predicate);
169
+ CREATE INDEX IF NOT EXISTS idx_entity_valid ON entity_triples(valid_from, valid_to);
170
+ CREATE INDEX IF NOT EXISTS idx_entity_active ON entity_triples(subject, valid_to) WHERE valid_to IS NULL;
171
+
142
172
  -- FTS5 for distillations (v2)
143
173
  CREATE VIRTUAL TABLE IF NOT EXISTS distillations_fts USING fts5(
144
174
  content,
@@ -243,6 +273,47 @@ CREATE INDEX IF NOT EXISTS idx_temporal_undistilled ON temporal_messages(session
243
273
  CREATE INDEX IF NOT EXISTS idx_temporal_time ON temporal_messages(time_created DESC);
244
274
  `;
245
275
 
276
+ // Migration from v2 to v3
277
+ const MIGRATION_V2_TO_V3 = `
278
+ -- Add raw_source column to observations
279
+ ALTER TABLE observations ADD COLUMN raw_source TEXT;
280
+
281
+ -- Add navigation columns to observations
282
+ ALTER TABLE observations ADD COLUMN wing TEXT;
283
+ ALTER TABLE observations ADD COLUMN hall TEXT CHECK(hall IS NULL OR hall IN ('facts','events','discoveries','preferences','advice'));
284
+ ALTER TABLE observations ADD COLUMN room TEXT;
285
+
286
+ -- Navigation indexes
287
+ CREATE INDEX IF NOT EXISTS idx_observations_wing ON observations(wing) WHERE wing IS NOT NULL;
288
+ CREATE INDEX IF NOT EXISTS idx_observations_hall ON observations(hall) WHERE hall IS NOT NULL;
289
+ CREATE INDEX IF NOT EXISTS idx_observations_room ON observations(room) WHERE room IS NOT NULL;
290
+ CREATE INDEX IF NOT EXISTS idx_observations_navigation ON observations(wing, hall, room) WHERE wing IS NOT NULL;
291
+
292
+ -- Add chunk_type to temporal_messages
293
+ ALTER TABLE temporal_messages ADD COLUMN chunk_type TEXT;
294
+
295
+ -- Entity triples table (temporal knowledge graph)
296
+ CREATE TABLE IF NOT EXISTS entity_triples (
297
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
298
+ subject TEXT NOT NULL,
299
+ predicate TEXT NOT NULL,
300
+ object TEXT NOT NULL,
301
+ valid_from TEXT NOT NULL,
302
+ valid_to TEXT,
303
+ confidence REAL NOT NULL DEFAULT 1.0,
304
+ source_observation_id INTEGER,
305
+ created_at TEXT NOT NULL,
306
+ created_at_epoch INTEGER NOT NULL,
307
+ FOREIGN KEY(source_observation_id) REFERENCES observations(id) ON DELETE SET NULL
308
+ );
309
+
310
+ CREATE INDEX IF NOT EXISTS idx_entity_subject ON entity_triples(subject);
311
+ CREATE INDEX IF NOT EXISTS idx_entity_object ON entity_triples(object);
312
+ CREATE INDEX IF NOT EXISTS idx_entity_predicate ON entity_triples(predicate);
313
+ CREATE INDEX IF NOT EXISTS idx_entity_valid ON entity_triples(valid_from, valid_to);
314
+ CREATE INDEX IF NOT EXISTS idx_entity_active ON entity_triples(subject, valid_to) WHERE valid_to IS NULL;
315
+ `;
316
+
246
317
  // ============================================================================
247
318
  // Database Manager
248
319
  // ============================================================================
@@ -429,7 +500,7 @@ function initializeSchema(db: Database): void {
429
500
  }
430
501
 
431
502
  if (currentVersion === 0) {
432
- // Fresh install — run full v2 schema
503
+ // Fresh install — run full v3 schema
433
504
  db.exec(SCHEMA_SQL);
434
505
 
435
506
  // Run FTS triggers
@@ -443,6 +514,9 @@ function initializeSchema(db: Database): void {
443
514
  if (currentVersion < 2) {
444
515
  migrateV1ToV2(db);
445
516
  }
517
+ if (currentVersion < 3) {
518
+ migrateV2ToV3(db);
519
+ }
446
520
  }
447
521
 
448
522
  // Record schema version
@@ -501,3 +575,20 @@ function migrateV1ToV2(db: Database): void {
501
575
  // Triggers may already exist
502
576
  }
503
577
  }
578
+
579
+ /**
580
+ * Migrate from schema v2 to v3.
581
+ * Adds: raw_source, wing/hall/room navigation, chunk_type, entity_triples table.
582
+ */
583
+ function migrateV2ToV3(db: Database): void {
584
+ for (const stmt of MIGRATION_V2_TO_V3.split(";")) {
585
+ const trimmed = stmt.trim();
586
+ if (trimmed) {
587
+ try {
588
+ db.run(trimmed);
589
+ } catch {
590
+ // Statement may fail if already applied (e.g. column exists)
591
+ }
592
+ }
593
+ }
594
+ }
@@ -46,6 +46,69 @@ export const MEMORY_CONFIG = {
46
46
  },
47
47
  } as const;
48
48
 
49
+ // ============================================================================
50
+ // Navigation Types (v3: structured navigation)
51
+ // ============================================================================
52
+
53
+ export type HallType = "facts" | "events" | "discoveries" | "preferences" | "advice";
54
+
55
+ export const VALID_HALLS: HallType[] = ["facts", "events", "discoveries", "preferences", "advice"];
56
+
57
+ // ============================================================================
58
+ // Entity Graph Types (v3: temporal knowledge graph)
59
+ // ============================================================================
60
+
61
+ export interface EntityTripleInput {
62
+ subject: string;
63
+ predicate: string;
64
+ object: string;
65
+ valid_from?: string; // ISO date or epoch
66
+ valid_to?: string | null;
67
+ confidence?: number; // 0.0–1.0
68
+ source_observation_id?: number;
69
+ }
70
+
71
+ export interface EntityTripleRow {
72
+ id: number;
73
+ subject: string;
74
+ predicate: string;
75
+ object: string;
76
+ valid_from: string;
77
+ valid_to: string | null;
78
+ confidence: number;
79
+ source_observation_id: number | null;
80
+ created_at: string;
81
+ created_at_epoch: number;
82
+ }
83
+
84
+ export interface EntityQueryResult {
85
+ id: number;
86
+ subject: string;
87
+ predicate: string;
88
+ object: string;
89
+ valid_from: string;
90
+ valid_to: string | null;
91
+ confidence: number;
92
+ is_active: boolean;
93
+ }
94
+
95
+ // ============================================================================
96
+ // Compact Format Types (v3: AAAK-inspired compression)
97
+ // ============================================================================
98
+
99
+ export interface CompactEntry {
100
+ code: string; // 3-letter entity code
101
+ full: string; // Full name
102
+ role?: string; // Role or type
103
+ }
104
+
105
+ export interface CompactResult {
106
+ compressed: string;
107
+ token_estimate: number;
108
+ original_tokens: number;
109
+ compression_ratio: number;
110
+ }
111
+
49
112
  // ============================================================================
50
113
  // Observation Types
51
114
  // ============================================================================
@@ -68,6 +131,7 @@ export interface ObservationRow {
68
131
  subtitle: string | null;
69
132
  facts: string | null; // JSON array
70
133
  narrative: string | null;
134
+ raw_source: string | null; // v3: verbatim source text
71
135
  concepts: string | null; // JSON array
72
136
  files_read: string | null; // JSON array
73
137
  files_modified: string | null; // JSON array
@@ -78,6 +142,9 @@ export interface ObservationRow {
78
142
  valid_until: string | null;
79
143
  markdown_file: string | null;
80
144
  source: ObservationSource;
145
+ wing: string | null; // v3: navigation wing (project/person)
146
+ hall: HallType | null; // v3: navigation hall (facts/events/...)
147
+ room: string | null; // v3: navigation room (topic)
81
148
  created_at: string;
82
149
  created_at_epoch: number;
83
150
  updated_at: string | null;
@@ -89,6 +156,7 @@ export interface ObservationInput {
89
156
  subtitle?: string;
90
157
  facts?: string[];
91
158
  narrative?: string;
159
+ raw_source?: string; // v3: verbatim source text
92
160
  concepts?: string[];
93
161
  files_read?: string[];
94
162
  files_modified?: string[];
@@ -97,6 +165,9 @@ export interface ObservationInput {
97
165
  supersedes?: number;
98
166
  markdown_file?: string;
99
167
  source?: ObservationSource;
168
+ wing?: string; // v3: navigation wing
169
+ hall?: HallType; // v3: navigation hall
170
+ room?: string; // v3: navigation room
100
171
  }
101
172
 
102
173
  export interface SearchIndexResult {
@@ -136,6 +207,7 @@ export interface TemporalMessageRow {
136
207
  token_estimate: number;
137
208
  time_created: number;
138
209
  distillation_id: number | null;
210
+ chunk_type: string | null; // v3: exchange-pair | paragraph | sliding-window
139
211
  created_at: string;
140
212
  }
141
213
 
@@ -146,6 +218,7 @@ export interface TemporalMessageInput {
146
218
  content: string;
147
219
  token_estimate: number;
148
220
  time_created: number;
221
+ chunk_type?: string; // v3: exchange-pair | paragraph | sliding-window
149
222
  }
150
223
 
151
224
  // ============================================================================
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Memory Index Generator — Auto-Generated Knowledge Catalog
3
+ *
4
+ * Inspired by Karpathy's LLM Wiki index.md:
5
+ * generates a structured catalog of all observations, grouped by type,
6
+ * with cross-references and concept clusters.
7
+ *
8
+ * Stored in memory_files as "index" for injection or on-demand reading.
9
+ */
10
+
11
+ import { upsertMemoryFile } from "./db/maintenance.js";
12
+ import type { ObservationRow } from "./db/types.js";
13
+ import { getMemoryDB } from "./memory-db.js";
14
+ import { TYPE_ICONS, parseConcepts } from "./memory-helpers.js";
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ export interface IndexEntry {
21
+ id: number;
22
+ type: string;
23
+ title: string;
24
+ concepts: string[];
25
+ created_at: string;
26
+ }
27
+
28
+ export interface IndexResult {
29
+ entryCount: number;
30
+ conceptCount: number;
31
+ content: string;
32
+ }
33
+
34
+ // ============================================================================
35
+ // Index Generation
36
+ // ============================================================================
37
+
38
+ /**
39
+ * Generate a comprehensive index of all active observations.
40
+ * Groups by type, lists concept clusters, and writes to memory_files.
41
+ */
42
+ export function generateMemoryIndex(): IndexResult {
43
+ const db = getMemoryDB();
44
+
45
+ const observations = db
46
+ .query(
47
+ `SELECT id, type, title, concepts, created_at FROM observations
48
+ WHERE superseded_by IS NULL
49
+ ORDER BY type, created_at_epoch DESC`,
50
+ )
51
+ .all() as Pick<
52
+ ObservationRow,
53
+ "id" | "type" | "title" | "concepts" | "created_at"
54
+ >[];
55
+
56
+ // Group by type
57
+ const byType = new Map<string, IndexEntry[]>();
58
+ const allConcepts = new Map<string, number[]>();
59
+
60
+ for (const obs of observations) {
61
+ const entry: IndexEntry = {
62
+ id: obs.id,
63
+ type: obs.type,
64
+ title: obs.title,
65
+ concepts: parseConcepts(obs.concepts),
66
+ created_at: obs.created_at,
67
+ };
68
+
69
+ const group = byType.get(obs.type) ?? [];
70
+ group.push(entry);
71
+ byType.set(obs.type, group);
72
+
73
+ for (const concept of entry.concepts) {
74
+ const ids = allConcepts.get(concept) ?? [];
75
+ ids.push(obs.id);
76
+ allConcepts.set(concept, ids);
77
+ }
78
+ }
79
+
80
+ // Build markdown
81
+ const lines: string[] = [];
82
+ lines.push("# Memory Index");
83
+ lines.push("");
84
+ lines.push(
85
+ `> Auto-generated catalog of ${observations.length} active observations.`,
86
+ );
87
+ lines.push(`> Last updated: ${new Date().toISOString().slice(0, 19)}`);
88
+ lines.push("");
89
+
90
+ // Summary table
91
+ lines.push("## Summary");
92
+ lines.push("");
93
+ lines.push("| Type | Count |");
94
+ lines.push("|------|-------|");
95
+ for (const [type, entries] of byType) {
96
+ const icon = TYPE_ICONS[type] ?? "📌";
97
+ lines.push(`| ${icon} ${type} | ${entries.length} |`);
98
+ }
99
+ lines.push(`| **Total** | **${observations.length}** |`);
100
+ lines.push("");
101
+
102
+ // By type
103
+ for (const [type, entries] of byType) {
104
+ const icon = TYPE_ICONS[type] ?? "📌";
105
+ lines.push(
106
+ `## ${icon} ${type.charAt(0).toUpperCase() + type.slice(1)} (${entries.length})`,
107
+ );
108
+ lines.push("");
109
+ for (const entry of entries) {
110
+ const concepts =
111
+ entry.concepts.length > 0 ? ` [${entry.concepts.join(", ")}]` : "";
112
+ lines.push(
113
+ `- **#${entry.id}** ${entry.title}${concepts} _(${entry.created_at.slice(0, 10)})_`,
114
+ );
115
+ }
116
+ lines.push("");
117
+ }
118
+
119
+ // Concept clusters
120
+ const significantConcepts = [...allConcepts.entries()]
121
+ .filter(([, ids]) => ids.length >= 2)
122
+ .sort((a, b) => b[1].length - a[1].length);
123
+
124
+ if (significantConcepts.length > 0) {
125
+ lines.push("## Concept Clusters");
126
+ lines.push("");
127
+ lines.push("Concepts appearing in 2+ observations:");
128
+ lines.push("");
129
+ for (const [concept, ids] of significantConcepts.slice(0, 30)) {
130
+ lines.push(
131
+ `- **${concept}** (${ids.length}): ${ids.map((id) => `#${id}`).join(", ")}`,
132
+ );
133
+ }
134
+ lines.push("");
135
+ }
136
+
137
+ // Orphan concepts
138
+ const orphanConcepts = [...allConcepts.entries()].filter(
139
+ ([, ids]) => ids.length === 1,
140
+ );
141
+
142
+ if (orphanConcepts.length > 0) {
143
+ lines.push("## Orphan Concepts");
144
+ lines.push("");
145
+ lines.push(
146
+ `${orphanConcepts.length} concepts appear in only 1 observation:`,
147
+ );
148
+ lines.push("");
149
+ const orphanList = orphanConcepts
150
+ .slice(0, 20)
151
+ .map(([concept, ids]) => `${concept} (#${ids[0]})`);
152
+ lines.push(orphanList.join(", "));
153
+ if (orphanConcepts.length > 20) {
154
+ lines.push(`... and ${orphanConcepts.length - 20} more`);
155
+ }
156
+ lines.push("");
157
+ }
158
+
159
+ const content = lines.join("\n");
160
+
161
+ // Store in memory_files
162
+ upsertMemoryFile("index", content, "replace");
163
+
164
+ return {
165
+ entryCount: observations.length,
166
+ conceptCount: allConcepts.size,
167
+ content,
168
+ };
169
+ }
170
+