opencodekit 0.17.13 → 0.18.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 (36) hide show
  1. package/dist/index.js +4 -6
  2. package/dist/template/.opencode/dcp.jsonc +81 -81
  3. package/dist/template/.opencode/memory/memory.db +0 -0
  4. package/dist/template/.opencode/memory.db +0 -0
  5. package/dist/template/.opencode/memory.db-shm +0 -0
  6. package/dist/template/.opencode/memory.db-wal +0 -0
  7. package/dist/template/.opencode/opencode.json +199 -23
  8. package/dist/template/.opencode/opencode.json.tui-migration.bak +1380 -0
  9. package/dist/template/.opencode/package.json +1 -1
  10. package/dist/template/.opencode/plugin/lib/capture.ts +177 -0
  11. package/dist/template/.opencode/plugin/lib/context.ts +194 -0
  12. package/dist/template/.opencode/plugin/lib/curator.ts +234 -0
  13. package/dist/template/.opencode/plugin/lib/db/maintenance.ts +312 -0
  14. package/dist/template/.opencode/plugin/lib/db/observations.ts +299 -0
  15. package/dist/template/.opencode/plugin/lib/db/pipeline.ts +520 -0
  16. package/dist/template/.opencode/plugin/lib/db/schema.ts +356 -0
  17. package/dist/template/.opencode/plugin/lib/db/types.ts +211 -0
  18. package/dist/template/.opencode/plugin/lib/distill.ts +376 -0
  19. package/dist/template/.opencode/plugin/lib/inject.ts +126 -0
  20. package/dist/template/.opencode/plugin/lib/memory-admin-tools.ts +188 -0
  21. package/dist/template/.opencode/plugin/lib/memory-db.ts +54 -936
  22. package/dist/template/.opencode/plugin/lib/memory-helpers.ts +202 -0
  23. package/dist/template/.opencode/plugin/lib/memory-hooks.ts +240 -0
  24. package/dist/template/.opencode/plugin/lib/memory-tools.ts +341 -0
  25. package/dist/template/.opencode/plugin/memory.ts +56 -60
  26. package/dist/template/.opencode/plugin/sessions.ts +372 -93
  27. package/dist/template/.opencode/tui.json +15 -0
  28. package/package.json +1 -1
  29. package/dist/template/.opencode/tool/action-queue.ts +0 -313
  30. package/dist/template/.opencode/tool/memory-admin.ts +0 -445
  31. package/dist/template/.opencode/tool/memory-get.ts +0 -143
  32. package/dist/template/.opencode/tool/memory-read.ts +0 -45
  33. package/dist/template/.opencode/tool/memory-search.ts +0 -264
  34. package/dist/template/.opencode/tool/memory-timeline.ts +0 -105
  35. package/dist/template/.opencode/tool/memory-update.ts +0 -63
  36. package/dist/template/.opencode/tool/observation.ts +0 -357
@@ -1,445 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { tool } from "@opencode-ai/plugin";
4
- import {
5
- type ConfidenceLevel,
6
- type ObservationInput,
7
- type ObservationType,
8
- archiveOldObservations,
9
- checkpointWAL,
10
- getDatabaseSizes,
11
- getMemoryDB,
12
- getObservationStats,
13
- rebuildFTS5,
14
- runFullMaintenance,
15
- storeObservation,
16
- vacuumDatabase,
17
- } from "../plugin/lib/memory-db.js";
18
-
19
- /**
20
- * Consolidated memory administration tool.
21
- * Operations: status, full, archive, checkpoint, vacuum, migrate
22
- */
23
- export default tool({
24
- description: `Memory system administration: maintenance and migration.
25
-
26
- Operations:
27
- - "status": Storage stats and recommendations
28
- - "full": Full maintenance cycle (archive + checkpoint + vacuum)
29
- - "archive": Archive old observations (>90 days default)
30
- - "checkpoint": Checkpoint WAL file
31
- - "vacuum": Vacuum database
32
- - "migrate": Import .opencode/memory/observations/*.md into SQLite
33
-
34
- Example:
35
- memory-admin({ operation: "status" })
36
- memory-admin({ operation: "migrate", dry_run: true })`,
37
- args: {
38
- operation: tool.schema
39
- .string()
40
- .optional()
41
- .default("status")
42
- .describe(
43
- "Operation: status, full, archive, checkpoint, vacuum, migrate",
44
- ),
45
- older_than_days: tool.schema
46
- .number()
47
- .optional()
48
- .default(90)
49
- .describe("Archive threshold in days (default: 90)"),
50
- dry_run: tool.schema
51
- .boolean()
52
- .optional()
53
- .default(false)
54
- .describe("Preview changes without executing"),
55
- force: tool.schema
56
- .boolean()
57
- .optional()
58
- .describe("Force re-migration of all files"),
59
- },
60
- execute: async (args: {
61
- operation?: string;
62
- older_than_days?: number;
63
- dry_run?: boolean;
64
- force?: boolean;
65
- }) => {
66
- const operation = args.operation || "status";
67
- const olderThanDays = args.older_than_days ?? 90;
68
- const dryRun = args.dry_run ?? false;
69
-
70
- // Helper to format bytes
71
- const formatBytes = (bytes: number): string => {
72
- if (bytes < 1024) return `${bytes} B`;
73
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
74
- return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
75
- };
76
-
77
- // ===== MIGRATE =====
78
- if (operation === "migrate") {
79
- return await runMigration(args.dry_run, args.force);
80
- }
81
-
82
- const results: string[] = [];
83
-
84
- // ===== STATUS =====
85
- if (operation === "status") {
86
- const sizes = getDatabaseSizes();
87
- const stats = getObservationStats();
88
-
89
- results.push("## Memory System Status\n");
90
- results.push("### Database Size");
91
- results.push(`- Main DB: ${formatBytes(sizes.mainDb)}`);
92
- results.push(`- WAL file: ${formatBytes(sizes.wal)}`);
93
- results.push(`- **Total: ${formatBytes(sizes.total)}**\n`);
94
-
95
- results.push("### Observations");
96
- results.push(`- Total: ${stats.total}`);
97
- for (const [type, count] of Object.entries(stats)) {
98
- if (type !== "total") {
99
- results.push(`- ${type}: ${count}`);
100
- }
101
- }
102
-
103
- const archiveCandidates = archiveOldObservations({
104
- olderThanDays,
105
- includeSuperseded: true,
106
- dryRun: true,
107
- });
108
- results.push(`\n### Maintenance Recommendations`);
109
- results.push(
110
- `- Archive candidates (>${olderThanDays} days): ${archiveCandidates}`,
111
- );
112
- if (sizes.wal > 1024 * 1024) {
113
- results.push(`- WAL checkpoint recommended (WAL > 1MB)`);
114
- }
115
-
116
- return results.join("\n");
117
- }
118
-
119
- // ===== FULL MAINTENANCE =====
120
- if (operation === "full") {
121
- results.push(
122
- dryRun ? "## Full Maintenance (DRY RUN)\n" : "## Full Maintenance\n",
123
- );
124
-
125
- const stats = runFullMaintenance({
126
- olderThanDays,
127
- includeSuperseded: true,
128
- dryRun,
129
- });
130
-
131
- results.push(`### Results`);
132
- results.push(`- Archived observations: ${stats.archived}`);
133
- results.push(`- WAL checkpointed: ${stats.checkpointed ? "Yes" : "No"}`);
134
- results.push(`- Database vacuumed: ${stats.vacuumed ? "Yes" : "No"}`);
135
- results.push(`- Space before: ${formatBytes(stats.dbSizeBefore)}`);
136
- results.push(`- Space after: ${formatBytes(stats.dbSizeAfter)}`);
137
- results.push(`- **Freed: ${formatBytes(stats.freedBytes)}**`);
138
-
139
- return results.join("\n");
140
- }
141
-
142
- // ===== ARCHIVE ONLY =====
143
- if (operation === "archive") {
144
- const archived = archiveOldObservations({
145
- olderThanDays,
146
- includeSuperseded: true,
147
- dryRun,
148
- });
149
-
150
- if (dryRun) {
151
- return `## Archive Preview\n\nWould archive ${archived} observations older than ${olderThanDays} days.\n\nRun without dry_run to execute.`;
152
- }
153
-
154
- return `## Archive Complete\n\nArchived ${archived} observations to observations_archive table.`;
155
- }
156
-
157
- // ===== CHECKPOINT ONLY =====
158
- if (operation === "checkpoint") {
159
- if (dryRun) {
160
- const sizes = getDatabaseSizes();
161
- return `## Checkpoint Preview\n\nWAL size: ${formatBytes(sizes.wal)}\n\nRun without dry_run to checkpoint.`;
162
- }
163
-
164
- const result = checkpointWAL();
165
- return `## Checkpoint Complete\n\nCheckpointed: ${result.checkpointed ? "Yes" : "No"}\nWAL pages processed: ${result.walSize}`;
166
- }
167
-
168
- // ===== VACUUM ONLY =====
169
- if (operation === "vacuum") {
170
- if (dryRun) {
171
- const sizes = getDatabaseSizes();
172
- return `## Vacuum Preview\n\nCurrent size: ${formatBytes(sizes.total)}\n\nRun without dry_run to vacuum.`;
173
- }
174
-
175
- const before = getDatabaseSizes();
176
- const success = vacuumDatabase();
177
- const after = getDatabaseSizes();
178
-
179
- return `## Vacuum Complete\n\nSuccess: ${success ? "Yes" : "No"}\nBefore: ${formatBytes(before.total)}\nAfter: ${formatBytes(after.total)}\nFreed: ${formatBytes(before.total - after.total)}`;
180
- }
181
-
182
- return `Unknown operation: ${operation}. Use: status, full, archive, checkpoint, vacuum, migrate`;
183
- },
184
- });
185
-
186
- // ===== MIGRATION HELPERS =====
187
-
188
- interface ParsedObservation {
189
- type: ObservationType;
190
- title: string;
191
- subtitle?: string;
192
- facts: string[];
193
- narrative: string;
194
- concepts: string[];
195
- files_read: string[];
196
- files_modified: string[];
197
- confidence: ConfidenceLevel;
198
- bead_id?: string;
199
- supersedes?: string;
200
- markdown_file: string;
201
- created_at: string;
202
- created_at_epoch: number;
203
- }
204
-
205
- function parseYAML(yamlContent: string): Record<string, unknown> {
206
- const result: Record<string, unknown> = {};
207
-
208
- for (const line of yamlContent.split("\n")) {
209
- const match = line.match(/^(\w+):\s*(.*)$/);
210
- if (match) {
211
- const [, key, value] = match;
212
- if (value.startsWith("[")) {
213
- try {
214
- result[key] = JSON.parse(value);
215
- } catch {
216
- result[key] = value;
217
- }
218
- } else if (value === "null" || value === "") {
219
- result[key] = null;
220
- } else if (value.startsWith('"') && value.endsWith('"')) {
221
- result[key] = value.slice(1, -1);
222
- } else {
223
- result[key] = value;
224
- }
225
- }
226
- }
227
-
228
- return result;
229
- }
230
-
231
- function extractFacts(narrative: string): string[] {
232
- const facts: string[] = [];
233
- const lines = narrative.split("\n");
234
-
235
- for (const line of lines) {
236
- const bulletMatch = line.match(/^[-*]\s+(.+)$/);
237
- if (bulletMatch) {
238
- facts.push(bulletMatch[1].trim());
239
- }
240
- }
241
-
242
- return facts;
243
- }
244
-
245
- function parseMarkdownObservation(
246
- content: string,
247
- filename: string,
248
- ): ParsedObservation {
249
- const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
250
- if (!frontmatterMatch) {
251
- throw new Error(`Invalid format: ${filename} - no YAML frontmatter`);
252
- }
253
-
254
- const yaml = parseYAML(frontmatterMatch[1]);
255
- const narrative = frontmatterMatch[2].trim();
256
-
257
- const titleMatch = narrative.match(/^#\s+.+?\s+(.+)$/m);
258
- const title = titleMatch
259
- ? titleMatch[1]
260
- : (yaml.title as string) || "Untitled";
261
-
262
- const validTypes: ObservationType[] = [
263
- "decision",
264
- "bugfix",
265
- "feature",
266
- "pattern",
267
- "discovery",
268
- "learning",
269
- "warning",
270
- ];
271
- const type = (yaml.type as string)?.toLowerCase() as ObservationType;
272
- if (!validTypes.includes(type)) {
273
- throw new Error(`Invalid type '${yaml.type}' in ${filename}`);
274
- }
275
-
276
- const validConfidence: ConfidenceLevel[] = ["high", "medium", "low"];
277
- const confidence = ((yaml.confidence as string)?.toLowerCase() ||
278
- "high") as ConfidenceLevel;
279
- if (!validConfidence.includes(confidence)) {
280
- throw new Error(`Invalid confidence '${yaml.confidence}' in ${filename}`);
281
- }
282
-
283
- const createdStr = yaml.created as string;
284
- if (!createdStr) {
285
- throw new Error(`Missing created date in ${filename}`);
286
- }
287
- const createdAt = new Date(createdStr);
288
- if (Number.isNaN(createdAt.getTime())) {
289
- throw new Error(`Invalid created date '${createdStr}' in ${filename}`);
290
- }
291
-
292
- const facts = extractFacts(narrative);
293
- const files = (yaml.files as string[]) || [];
294
-
295
- return {
296
- type,
297
- title,
298
- subtitle: yaml.subtitle as string | undefined,
299
- facts,
300
- narrative,
301
- concepts: (yaml.concepts as string[]) || [],
302
- files_read: files,
303
- files_modified: files,
304
- confidence,
305
- bead_id: yaml.bead_id as string | undefined,
306
- supersedes: yaml.supersedes as string | undefined,
307
- markdown_file: filename,
308
- created_at: createdAt.toISOString(),
309
- created_at_epoch: createdAt.getTime(),
310
- };
311
- }
312
-
313
- async function runMigration(
314
- dryRun?: boolean,
315
- force?: boolean,
316
- ): Promise<string> {
317
- const obsDir = path.join(process.cwd(), ".opencode/memory/observations");
318
- const migrationMarker = path.join(obsDir, ".migrated");
319
-
320
- if (!force) {
321
- try {
322
- await fs.access(migrationMarker);
323
- const markerContent = await fs.readFile(migrationMarker, "utf-8");
324
- return `Migration already complete.\n\n${markerContent}\n\nUse force: true to re-migrate.`;
325
- } catch {
326
- // No marker, proceed with migration
327
- }
328
- }
329
-
330
- let files: string[];
331
- try {
332
- const entries = await fs.readdir(obsDir);
333
- files = entries.filter((f) => f.endsWith(".md") && !f.startsWith("."));
334
- } catch {
335
- return "No observations directory found at .opencode/memory/observations/";
336
- }
337
-
338
- if (files.length === 0) {
339
- return "No markdown files found to migrate.";
340
- }
341
-
342
- const db = getMemoryDB();
343
- const results: {
344
- migrated: string[];
345
- skipped: string[];
346
- errors: { file: string; error: string }[];
347
- } = {
348
- migrated: [],
349
- skipped: [],
350
- errors: [],
351
- };
352
-
353
- files.sort();
354
-
355
- for (const file of files) {
356
- try {
357
- const content = await fs.readFile(path.join(obsDir, file), "utf-8");
358
- const parsed = parseMarkdownObservation(content, file);
359
-
360
- if (dryRun) {
361
- results.migrated.push(
362
- `${file} → ${parsed.type}: ${parsed.title.substring(0, 50)}`,
363
- );
364
- continue;
365
- }
366
-
367
- const existing = db
368
- .query("SELECT id FROM observations WHERE markdown_file = ?")
369
- .get(file);
370
-
371
- if (existing && !force) {
372
- results.skipped.push(file);
373
- continue;
374
- }
375
-
376
- const input: ObservationInput = {
377
- type: parsed.type,
378
- title: parsed.title,
379
- subtitle: parsed.subtitle,
380
- facts: parsed.facts,
381
- narrative: parsed.narrative,
382
- concepts: parsed.concepts,
383
- files_read: parsed.files_read,
384
- files_modified: parsed.files_modified,
385
- confidence: parsed.confidence,
386
- bead_id: parsed.bead_id,
387
- markdown_file: file,
388
- };
389
-
390
- storeObservation(input);
391
- results.migrated.push(file);
392
- } catch (e) {
393
- results.errors.push({
394
- file,
395
- error: e instanceof Error ? e.message : String(e),
396
- });
397
- }
398
- }
399
-
400
- if (!dryRun && results.migrated.length > 0) {
401
- try {
402
- rebuildFTS5();
403
- } catch {
404
- // FTS5 rebuild failed, continue
405
- }
406
- }
407
-
408
- if (!dryRun) {
409
- const markerContent = [
410
- `Migrated ${results.migrated.length} observations on ${new Date().toISOString()}`,
411
- `Skipped: ${results.skipped.length}`,
412
- `Errors: ${results.errors.length}`,
413
- ].join("\n");
414
- await fs.writeFile(migrationMarker, markerContent, "utf-8");
415
- }
416
-
417
- let output = dryRun
418
- ? "# Migration Preview (Dry Run)\n\n"
419
- : "# Migration Complete\n\n";
420
-
421
- output += `**Total files**: ${files.length}\n`;
422
- output += `**Migrated**: ${results.migrated.length}\n`;
423
- output += `**Skipped**: ${results.skipped.length}\n`;
424
- output += `**Errors**: ${results.errors.length}\n\n`;
425
-
426
- if (results.errors.length > 0) {
427
- output += "## Errors\n\n";
428
- for (const { file, error } of results.errors) {
429
- output += `- **${file}**: ${error}\n`;
430
- }
431
- output += "\n";
432
- }
433
-
434
- if (dryRun && results.migrated.length > 0) {
435
- output += "## Files to migrate\n\n";
436
- for (const item of results.migrated.slice(0, 20)) {
437
- output += `- ${item}\n`;
438
- }
439
- if (results.migrated.length > 20) {
440
- output += `- ... and ${results.migrated.length - 20} more\n`;
441
- }
442
- }
443
-
444
- return output;
445
- }
@@ -1,143 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import {
3
- type ObservationRow,
4
- getObservationsByIds,
5
- } from "../plugin/lib/memory-db";
6
-
7
- const TYPE_ICONS: Record<string, string> = {
8
- decision: "🎯",
9
- bugfix: "🐛",
10
- feature: "✨",
11
- pattern: "🔄",
12
- discovery: "💡",
13
- learning: "📚",
14
- warning: "⚠️",
15
- };
16
-
17
- const CONFIDENCE_ICONS: Record<string, string> = {
18
- high: "🟢",
19
- medium: "🟡",
20
- low: "🔴",
21
- };
22
-
23
- function parseJsonArray(jsonStr: string | null): string[] {
24
- if (!jsonStr) return [];
25
- try {
26
- return JSON.parse(jsonStr);
27
- } catch {
28
- return [];
29
- }
30
- }
31
-
32
- function formatFullObservation(obs: ObservationRow): string {
33
- const icon = TYPE_ICONS[obs.type] || "📝";
34
- const confIcon = CONFIDENCE_ICONS[obs.confidence] || "🟢";
35
- const date = obs.created_at.split("T")[0];
36
-
37
- let output = `# ${icon} #${obs.id}: ${obs.title}\n\n`;
38
-
39
- // Metadata
40
- output += `**Type**: ${obs.type} | **Confidence**: ${confIcon} ${obs.confidence} | **Created**: ${date}\n\n`;
41
-
42
- if (obs.subtitle) {
43
- output += `*${obs.subtitle}*\n\n`;
44
- }
45
-
46
- // Concepts
47
- const concepts = parseJsonArray(obs.concepts);
48
- if (concepts.length > 0) {
49
- output += `**Concepts**: ${concepts.join(", ")}\n\n`;
50
- }
51
-
52
- // Files
53
- const filesRead = parseJsonArray(obs.files_read);
54
- const filesModified = parseJsonArray(obs.files_modified);
55
- if (filesRead.length > 0) {
56
- output += `**Files Read**: ${filesRead.join(", ")}\n`;
57
- }
58
- if (filesModified.length > 0) {
59
- output += `**Files Modified**: ${filesModified.join(", ")}\n`;
60
- }
61
- if (filesRead.length > 0 || filesModified.length > 0) {
62
- output += "\n";
63
- }
64
-
65
- // Facts
66
- const facts = parseJsonArray(obs.facts);
67
- if (facts.length > 0) {
68
- output += "## Key Facts\n\n";
69
- for (const fact of facts) {
70
- output += `- ${fact}\n`;
71
- }
72
- output += "\n";
73
- }
74
-
75
- // Narrative
76
- if (obs.narrative) {
77
- output += "## Content\n\n";
78
- output += obs.narrative;
79
- output += "\n\n";
80
- }
81
-
82
- // Relationships
83
- if (obs.bead_id) {
84
- output += `**Linked Bead**: ${obs.bead_id}\n`;
85
- }
86
- if (obs.supersedes) {
87
- output += `**Supersedes**: #${obs.supersedes}\n`;
88
- }
89
- if (obs.superseded_by) {
90
- output += `⚠️ **Superseded by**: #${obs.superseded_by}\n`;
91
- }
92
- if (obs.valid_until) {
93
- output += `**Valid until**: ${obs.valid_until}\n`;
94
- }
95
- if (obs.markdown_file) {
96
- output += `**Source file**: ${obs.markdown_file}\n`;
97
- }
98
-
99
- return output;
100
- }
101
-
102
- export default tool({
103
- description: `Get full observation details by ID.
104
-
105
- Purpose:
106
- - Progressive disclosure: fetch full details after identifying relevant observations via search
107
- - Get complete narrative, facts, and metadata
108
- - Supports multiple IDs for batch retrieval
109
-
110
- Example:
111
- memory-get({ ids: "42" }) // Single observation
112
- memory-get({ ids: "1,5,10" }) // Multiple observations`,
113
- args: {
114
- ids: tool.schema
115
- .string()
116
- .describe("Comma-separated observation IDs to retrieve"),
117
- },
118
- execute: async (args: { ids: string }) => {
119
- const ids = args.ids
120
- .split(",")
121
- .map((id) => Number.parseInt(id.trim(), 10))
122
- .filter((id) => !Number.isNaN(id));
123
-
124
- if (ids.length === 0) {
125
- return "No valid observation IDs provided.";
126
- }
127
-
128
- const observations = getObservationsByIds(ids);
129
-
130
- if (observations.length === 0) {
131
- return `No observations found for IDs: ${args.ids}`;
132
- }
133
-
134
- let output = `# Retrieved ${observations.length} Observation(s)\n\n`;
135
-
136
- for (const obs of observations) {
137
- output += formatFullObservation(obs);
138
- output += "\n---\n\n";
139
- }
140
-
141
- return output;
142
- },
143
- });
@@ -1,45 +0,0 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { getMemoryFile } from "../plugin/lib/memory-db.js";
3
-
4
- export default tool({
5
- description: `Read memory files for persistent cross-session context.
6
-
7
- Purpose:
8
- - Retrieve project state, learnings, and active tasks
9
- - Reads from SQLite database
10
- - Supports subdirectories: handoffs/, research/
11
-
12
- Example:
13
- memory-read({ file: "handoffs/2024-01-20-phase-1" })
14
- memory-read({ file: "research/2024-01-topic" })`,
15
- args: {
16
- file: tool.schema
17
- .string()
18
- .optional()
19
- .describe(
20
- "Memory file to read: handoffs/YYYY-MM-DD-phase, research/YYYY-MM-DD-topic",
21
- ),
22
- },
23
- execute: async (args: { file?: string }) => {
24
- const fileName = args.file || "memory";
25
-
26
- // Normalize: strip .md extension if present
27
- const normalizedFile = fileName.replace(/\.md$/i, "");
28
-
29
- try {
30
- const dbRecord = getMemoryFile(normalizedFile);
31
- if (dbRecord) {
32
- const updatedInfo = dbRecord.updated_at
33
- ? ` (updated: ${dbRecord.updated_at})`
34
- : "";
35
- return `[${normalizedFile}${updatedInfo}]\n\n${dbRecord.content}`;
36
- }
37
- } catch (error) {
38
- if (error instanceof Error) {
39
- return `Error reading memory: ${error.message}`;
40
- }
41
- }
42
-
43
- return `Memory file '${normalizedFile}' not found.\n\nStructure:\n- handoffs/YYYY-MM-DD-phase (phase transitions)\n- research/YYYY-MM-DD-topic (research findings)`;
44
- },
45
- });