opencodekit 0.20.3 → 0.20.5

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 (32) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/AGENTS.md +14 -9
  3. package/dist/template/.opencode/agent/build.md +0 -32
  4. package/dist/template/.opencode/agent/plan.md +0 -14
  5. package/dist/template/.opencode/agent/review.md +0 -40
  6. package/dist/template/.opencode/command/create.md +11 -61
  7. package/dist/template/.opencode/command/plan.md +11 -12
  8. package/dist/template/.opencode/command/pr.md +4 -16
  9. package/dist/template/.opencode/command/research.md +7 -16
  10. package/dist/template/.opencode/command/resume.md +2 -11
  11. package/dist/template/.opencode/command/review-codebase.md +9 -15
  12. package/dist/template/.opencode/command/ship.md +12 -53
  13. package/dist/template/.opencode/memory/project/user.md +7 -0
  14. package/dist/template/.opencode/memory.db +0 -0
  15. package/dist/template/.opencode/memory.db-shm +0 -0
  16. package/dist/template/.opencode/memory.db-wal +0 -0
  17. package/dist/template/.opencode/opencode.json +54 -67
  18. package/dist/template/.opencode/package.json +1 -1
  19. package/dist/template/.opencode/plugin/README.md +1 -1
  20. package/dist/template/.opencode/plugin/lib/compact.ts +194 -0
  21. package/dist/template/.opencode/plugin/lib/db/graph.ts +253 -0
  22. package/dist/template/.opencode/plugin/lib/db/observations.ts +8 -3
  23. package/dist/template/.opencode/plugin/lib/db/schema.ts +96 -5
  24. package/dist/template/.opencode/plugin/lib/db/types.ts +73 -0
  25. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +36 -3
  26. package/dist/template/.opencode/plugin/lib/memory-db.ts +12 -1
  27. package/dist/template/.opencode/plugin/lib/memory-tools.ts +137 -1
  28. package/dist/template/.opencode/plugin/memory.ts +2 -1
  29. package/dist/template/.opencode/skill/memory-grounding/SKILL.md +68 -0
  30. package/dist/template/.opencode/skill/verification-gates/SKILL.md +63 -0
  31. package/dist/template/.opencode/skill/workspace-setup/SKILL.md +76 -0
  32. package/package.json +1 -1
@@ -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
  // ============================================================================
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Memory Plugin — Admin Tools
3
3
  *
4
- * memory-admin (9 operations).
4
+ * memory-admin (12 operations).
5
5
  *
6
6
  * Uses factory pattern: createAdminTools(deps) returns tool definitions.
7
7
  */
@@ -20,10 +20,13 @@ import {
20
20
  type ConfidenceLevel,
21
21
  checkFTS5Available,
22
22
  checkpointWAL,
23
+ findGraphContradictions,
23
24
  getCaptureStats,
24
25
  getDatabaseSizes,
25
26
  getDistillationStats,
27
+ getEntityGraphStats,
26
28
  getMarkdownFilesInSqlite,
29
+ getMemoryDB,
27
30
  getObservationStats,
28
31
  type ObservationType,
29
32
  rebuildFTS5,
@@ -73,11 +76,12 @@ export function createAdminTools(deps: AdminToolDeps) {
73
76
  });
74
77
  const captureStats = getCaptureStats();
75
78
  const distillStats = getDistillationStats();
79
+ const graphStats = getEntityGraphStats();
76
80
  return [
77
81
  "## Memory System Status\n",
78
82
  `**Database**: ${(sizes.total / 1024).toFixed(1)} KB`,
79
83
  `**FTS5**: ${checkFTS5Available() ? "Available (porter stemming)" : "Unavailable"}`,
80
- `**Schema**: v2 (4-tier storage)\n`,
84
+ `**Schema**: v3 (4-tier + entity graph)\n`,
81
85
  "### Observations",
82
86
  ...Object.entries(stats).map(([k, v]) => ` ${k}: ${v}`),
83
87
  ` Archivable (>${olderThanDays}d): ${archivable}\n`,
@@ -86,7 +90,11 @@ export function createAdminTools(deps: AdminToolDeps) {
86
90
  ` Sessions: ${captureStats.sessions}\n`,
87
91
  "### Distillations",
88
92
  ` Total: ${distillStats.total} (${distillStats.sessions} sessions)`,
89
- ` Avg compression: ${(distillStats.avgCompression * 100).toFixed(1)}%`,
93
+ ` Avg compression: ${(distillStats.avgCompression * 100).toFixed(1)}%\n`,
94
+ "### Entity Graph",
95
+ ` Triples: ${graphStats.total_triples} (active: ${graphStats.active_triples})`,
96
+ ` Entities: ${graphStats.unique_entities}`,
97
+ ` Predicates: ${graphStats.unique_predicates}`,
90
98
  ].join("\n");
91
99
  }
92
100
  case "full": {
@@ -132,6 +140,31 @@ export function createAdminTools(deps: AdminToolDeps) {
132
140
  }
133
141
  case "lint": {
134
142
  const result = lintMemory({ staleDays: olderThanDays });
143
+
144
+ // Entity graph contradiction scan
145
+ const graphStats = getEntityGraphStats();
146
+ if (graphStats.total_triples > 0) {
147
+ try {
148
+ // Check each active triple for contradictions
149
+ const db = getMemoryDB();
150
+ const activeTriples = db.query(
151
+ "SELECT DISTINCT subject, predicate, object FROM entity_triples WHERE valid_to IS NULL LIMIT 200"
152
+ ).all() as { subject: string; predicate: string; object: string }[];
153
+ for (const t of activeTriples) {
154
+ const contradictions = findGraphContradictions(t.subject, t.predicate, t.object);
155
+ if (contradictions.length > 0) {
156
+ result.issues.push({
157
+ severity: "medium" as const,
158
+ title: `Graph contradiction: ${t.subject} ↔ ${t.object}`,
159
+ detail: `Active triple "${t.subject} —[${t.predicate}]→ ${t.object}" has ${contradictions.length} conflicting predicate(s): ${contradictions.map(c => c.predicate).join(", ")}`,
160
+ suggestion: `Use memory-graph-invalidate to close outdated triples`,
161
+ type: "contradiction" as const,
162
+ observation_ids: contradictions.map(c => c.source_observation_id).filter((id): id is number => id != null),
163
+ });
164
+ }
165
+ }
166
+ } catch { /* Graph table may not exist yet */ }
167
+ }
135
168
  if (result.issues.length === 0) {
136
169
  return `Memory lint: clean (${result.stats.total_observations} observations, 0 issues).`;
137
170
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Memory Database Module v2 — Barrel Export
2
+ * Memory Database Module v3 — Barrel Export
3
3
  *
4
4
  * Re-exports all functions and types from sub-modules in ./db/.
5
5
  * This preserves backward compatibility for existing imports from "./lib/memory-db.js".
@@ -10,6 +10,7 @@
10
10
  * db/observations.ts — Observation CRUD, search, timeline, stats
11
11
  * db/pipeline.ts — Temporal messages, distillations, relevance scoring
12
12
  * db/maintenance.ts — Memory files, FTS5, DB maintenance
13
+ * db/graph.ts — Entity graph: temporal triples, queries, stats
13
14
  */
14
15
 
15
16
  // Memory Files, FTS5, and Maintenance
@@ -52,6 +53,16 @@ export {
52
53
  storeDistillation,
53
54
  storeTemporalMessage,
54
55
  } from "./db/pipeline.js";
56
+ // Entity Graph Operations (v3)
57
+ export {
58
+ addEntityTriple,
59
+ findContradictions as findGraphContradictions,
60
+ getEntityGraphStats,
61
+ getEntityTimeline,
62
+ getTripleById,
63
+ invalidateTriple,
64
+ queryEntity,
65
+ } from "./db/graph.js";
55
66
  // Database Manager
56
67
  export { closeMemoryDB, getMemoryDB } from "./db/schema.js";
57
68
  // Types & Configuration
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Memory Plugin — Core Tools
3
3
  *
4
- * observation, memory-search, memory-get, memory-read, memory-update, memory-timeline
4
+ * observation, memory-search, memory-get, memory-read, memory-update, memory-timeline,
5
+ * memory-graph-add, memory-graph-query, memory-graph-invalidate, memory-compact
5
6
  *
6
7
  * Uses factory pattern: createCoreTools(deps) returns tool definitions
7
8
  * that can be spread into plugin's tool:{} export.
@@ -11,17 +12,23 @@ import { readdir } from "node:fs/promises";
11
12
  import path from "node:path";
12
13
  import { tool } from "@opencode-ai/plugin/tool";
13
14
  import {
15
+ addEntityTriple,
14
16
  type ConfidenceLevel,
15
17
  checkFTS5Available,
18
+ getMemoryDB,
16
19
  getMemoryFile,
17
20
  getObservationsByIds,
18
21
  getTimelineAroundObservation,
22
+ invalidateTriple,
19
23
  type ObservationSource,
20
24
  type ObservationType,
25
+ queryEntity,
21
26
  searchDistillationsFTS,
22
27
  searchObservationsFTS,
23
28
  storeObservation,
24
29
  upsertMemoryFile,
30
+ type HallType,
31
+ VALID_HALLS,
25
32
  } from "./memory-db.js";
26
33
  import {
27
34
  autoDetectFiles,
@@ -32,6 +39,7 @@ import {
32
39
  VALID_TYPES,
33
40
  } from "./memory-helpers.js";
34
41
  import { validateObservation } from "./validate.js";
42
+ import { compactObservations } from "./compact.js";
35
43
 
36
44
  /**
37
45
  * Wrap a memory tool execute function with DB error handling.
@@ -118,6 +126,22 @@ export function createCoreTools(deps: CoreToolDeps) {
118
126
  .string()
119
127
  .optional()
120
128
  .describe("manual, curator, imported"),
129
+ wing: tool.schema
130
+ .string()
131
+ .optional()
132
+ .describe("Navigation wing (project or person name)"),
133
+ hall: tool.schema
134
+ .string()
135
+ .optional()
136
+ .describe("Navigation hall: facts, events, discoveries, preferences, advice"),
137
+ room: tool.schema
138
+ .string()
139
+ .optional()
140
+ .describe("Navigation room (topic name, e.g. auth-migration)"),
141
+ raw_source: tool.schema
142
+ .string()
143
+ .optional()
144
+ .describe("Verbatim source text to preserve losslessly alongside narrative"),
121
145
  },
122
146
  execute: withDBErrorHandling(async (args) => {
123
147
  const obsType = args.type as ObservationType;
@@ -157,6 +181,10 @@ export function createCoreTools(deps: CoreToolDeps) {
157
181
  }
158
182
 
159
183
  const source = (args.source ?? "manual") as ObservationSource;
184
+ const hall = args.hall as HallType | undefined;
185
+ if (hall && !VALID_HALLS.includes(hall)) {
186
+ return `Error: Invalid hall "${args.hall}". Valid: ${VALID_HALLS.join(", ")}`;
187
+ }
160
188
 
161
189
  // Validation gate: check for duplicates, contradictions, low quality
162
190
  const validation = validateObservation({
@@ -172,6 +200,9 @@ export function createCoreTools(deps: CoreToolDeps) {
172
200
  bead_id: args.bead_id,
173
201
  supersedes,
174
202
  source,
203
+ wing: args.wing,
204
+ hall,
205
+ room: args.room,
175
206
  });
176
207
 
177
208
  if (validation.verdict === "reject") {
@@ -188,6 +219,7 @@ export function createCoreTools(deps: CoreToolDeps) {
188
219
  subtitle: args.subtitle,
189
220
  facts,
190
221
  narrative,
222
+ raw_source: args.raw_source,
191
223
  concepts,
192
224
  files_read: filesRead,
193
225
  files_modified: filesModified,
@@ -195,6 +227,9 @@ export function createCoreTools(deps: CoreToolDeps) {
195
227
  bead_id: args.bead_id,
196
228
  supersedes,
197
229
  source,
230
+ wing: args.wing,
231
+ hall,
232
+ room: args.room,
198
233
  });
199
234
 
200
235
  const warnings = validation.issues.length > 0
@@ -395,5 +430,106 @@ export function createCoreTools(deps: CoreToolDeps) {
395
430
  return lines.join("\n");
396
431
  }),
397
432
  }),
433
+
434
+ "memory-graph-add": tool({
435
+ description: `Add a triple to the entity knowledge graph.\n\nStores a subject-predicate-object relationship with optional temporal bounds and confidence.\nUse for structured facts like "project uses typescript" or "user prefers dark-mode".\n\nExample:\nmemory-graph-add({ subject: "project", predicate: "uses", object: "typescript" })\nmemory-graph-add({ subject: "auth", predicate: "depends-on", object: "jwt", confidence: 0.9, source_observation_id: 42 })`,
436
+ args: {
437
+ subject: tool.schema.string().describe("Entity subject"),
438
+ predicate: tool.schema.string().describe("Relationship type (e.g. uses, depends-on, prefers)"),
439
+ object: tool.schema.string().describe("Entity object"),
440
+ valid_from: tool.schema.string().optional().describe("Start date (ISO, default: today)"),
441
+ valid_to: tool.schema.string().optional().describe("End date (ISO, null = still active)"),
442
+ confidence: tool.schema.number().optional().describe("Confidence 0.0-1.0 (default: 1.0)"),
443
+ source_observation_id: tool.schema.number().optional().describe("Link to source observation"),
444
+ },
445
+ execute: withDBErrorHandling(async (args) => {
446
+ if (!args.subject?.trim() || !args.predicate?.trim() || !args.object?.trim()) {
447
+ return "Error: subject, predicate, and object are required.";
448
+ }
449
+ const id = addEntityTriple({
450
+ subject: args.subject,
451
+ predicate: args.predicate,
452
+ object: args.object,
453
+ valid_from: args.valid_from,
454
+ valid_to: args.valid_to,
455
+ confidence: args.confidence,
456
+ source_observation_id: args.source_observation_id,
457
+ });
458
+ return `Triple #${id} added: ${args.subject} —[${args.predicate}]→ ${args.object}`;
459
+ }),
460
+ }),
461
+
462
+ "memory-graph-query": tool({
463
+ description: `Query the entity knowledge graph.\n\nFind relationships for an entity with optional time and direction filters.\nReturns triples where the entity appears as subject, object, or both.\n\nExample:\nmemory-graph-query({ entity: "typescript" })\nmemory-graph-query({ entity: "auth", direction: "out", active_only: true })\nmemory-graph-query({ entity: "project", as_of: "2025-06-01" })`,
464
+ args: {
465
+ entity: tool.schema.string().describe("Entity to query"),
466
+ direction: tool.schema.string().optional().describe("out (subject), in (object), both (default)"),
467
+ predicate: tool.schema.string().optional().describe("Filter by predicate"),
468
+ as_of: tool.schema.string().optional().describe("ISO date for point-in-time query"),
469
+ active_only: tool.schema.boolean().optional().describe("Only active triples (default: false)"),
470
+ limit: tool.schema.number().optional().describe("Max results (default: 50)"),
471
+ },
472
+ execute: withDBErrorHandling(async (args) => {
473
+ if (!args.entity?.trim()) return "Error: entity is required.";
474
+ const results = queryEntity(args.entity, {
475
+ direction: args.direction as "out" | "in" | "both" | undefined,
476
+ predicate: args.predicate,
477
+ as_of: args.as_of,
478
+ activeOnly: args.active_only,
479
+ limit: args.limit,
480
+ });
481
+ if (results.length === 0) return `No triples found for entity "${args.entity}".`;
482
+ const lines: string[] = [
483
+ `## Entity Graph: ${args.entity} (${results.length} triples)\n`,
484
+ "| ID | Subject | Predicate | Object | Active | Confidence |",
485
+ "|---|---|---|---|---|---|",
486
+ ];
487
+ for (const r of results) {
488
+ lines.push(`| ${r.id} | ${r.subject} | ${r.predicate} | ${r.object} | ${r.is_active ? "\u2705" : "\u274C"} | ${(r.confidence * 100).toFixed(0)}% |`);
489
+ }
490
+ return lines.join("\n");
491
+ }),
492
+ }),
493
+
494
+ "memory-graph-invalidate": tool({
495
+ description: `Invalidate (close) entity triples in the knowledge graph.\n\nMarks active triples matching subject+predicate+object as no longer valid by setting valid_to.\nUse when a fact is no longer true (e.g. project stopped using a library).\n\nExample:\nmemory-graph-invalidate({ subject: "project", predicate: "uses", object: "moment.js" })`,
496
+ args: {
497
+ subject: tool.schema.string().describe("Entity subject"),
498
+ predicate: tool.schema.string().describe("Relationship type"),
499
+ object: tool.schema.string().describe("Entity object"),
500
+ end_date: tool.schema.string().optional().describe("End date (ISO, default: today)"),
501
+ },
502
+ execute: withDBErrorHandling(async (args) => {
503
+ if (!args.subject?.trim() || !args.predicate?.trim() || !args.object?.trim()) {
504
+ return "Error: subject, predicate, and object are required.";
505
+ }
506
+ const count = invalidateTriple(args.subject, args.predicate, args.object, args.end_date);
507
+ return count > 0
508
+ ? `Invalidated ${count} triple(s): ${args.subject} —[${args.predicate}]→ ${args.object}`
509
+ : `No active triples found matching ${args.subject} —[${args.predicate}]→ ${args.object}.`;
510
+ }),
511
+ }),
512
+
513
+ "memory-compact": tool({
514
+ description: `Compact observations into a dense, token-efficient format.\n\nUses AAAK-inspired pipe-separated compression achieving ~3-5x token reduction.\nUseful for injecting memory into prompts with minimal token cost.\n\nExample:\nmemory-compact({})\nmemory-compact({ limit: 20 })`,
515
+ args: {
516
+ limit: tool.schema.number().optional().describe("Max observations to compact (default: all)"),
517
+ },
518
+ execute: withDBErrorHandling(async (args) => {
519
+ const db = getMemoryDB();
520
+ const limitClause = args.limit ? `LIMIT ${Number(args.limit)}` : "";
521
+ const observations = db.query(
522
+ `SELECT id, type, title, narrative, concepts, wing, hall, room, confidence, created_at
523
+ FROM observations
524
+ WHERE superseded_by IS NULL
525
+ ORDER BY created_at_epoch DESC ${limitClause}`
526
+ ).all() as { id: number; type: string; title: string; narrative?: string | null; concepts?: string | null; wing?: string | null; hall?: string | null; room?: string | null; confidence?: string | null; created_at?: string | null }[];
527
+ if (observations.length === 0) return "No observations to compact.";
528
+ const result = compactObservations(observations);
529
+ if (!result.compressed) return "No observations to compact.";
530
+ const ratio = result.compression_ratio > 0 ? ` (${(result.compression_ratio * 100).toFixed(0)}% of original)` : "";
531
+ return `## Compact Memory (${observations.length} observations, ~${result.token_estimate} tokens${ratio})\n\n${result.compressed}`;
532
+ }),
533
+ }),
398
534
  };
399
535
  }
@@ -11,7 +11,8 @@
11
11
  * 5. Context Management — messages.transform → token budget enforcement
12
12
  *
13
13
  * Tools: observation, memory-search, memory-get, memory-read,
14
- * memory-update, memory-timeline, memory-admin
14
+ * memory-update, memory-timeline, memory-graph-add, memory-graph-query,
15
+ * memory-graph-invalidate, memory-compact, memory-admin
15
16
  *
16
17
  * Module structure:
17
18
  * memory.ts — Plugin entry (this file)