opencodekit 0.16.4 → 0.16.6

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 (56) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/template/.opencode/AGENTS.md +106 -384
  3. package/dist/template/.opencode/README.md +170 -104
  4. package/dist/template/.opencode/agent/build.md +39 -32
  5. package/dist/template/.opencode/agent/explore.md +2 -0
  6. package/dist/template/.opencode/agent/review.md +3 -0
  7. package/dist/template/.opencode/agent/scout.md +22 -11
  8. package/dist/template/.opencode/command/create.md +164 -106
  9. package/dist/template/.opencode/command/design.md +5 -1
  10. package/dist/template/.opencode/command/handoff.md +6 -4
  11. package/dist/template/.opencode/command/init.md +1 -1
  12. package/dist/template/.opencode/command/plan.md +26 -23
  13. package/dist/template/.opencode/command/research.md +13 -6
  14. package/dist/template/.opencode/command/resume.md +8 -6
  15. package/dist/template/.opencode/command/ship.md +1 -1
  16. package/dist/template/.opencode/command/start.md +30 -25
  17. package/dist/template/.opencode/command/status.md +9 -42
  18. package/dist/template/.opencode/command/verify.md +11 -11
  19. package/dist/template/.opencode/memory/README.md +67 -37
  20. package/dist/template/.opencode/memory/_templates/prd.md +102 -18
  21. package/dist/template/.opencode/memory/project/gotchas.md +31 -0
  22. package/dist/template/.opencode/memory.db +0 -0
  23. package/dist/template/.opencode/memory.db-shm +0 -0
  24. package/dist/template/.opencode/memory.db-wal +0 -0
  25. package/dist/template/.opencode/opencode.json +0 -10
  26. package/dist/template/.opencode/package.json +1 -1
  27. package/dist/template/.opencode/skill/beads/SKILL.md +164 -380
  28. package/dist/template/.opencode/skill/beads/references/BOUNDARIES.md +23 -22
  29. package/dist/template/.opencode/skill/beads/references/DEPENDENCIES.md +23 -29
  30. package/dist/template/.opencode/skill/beads/references/RESUMABILITY.md +5 -8
  31. package/dist/template/.opencode/skill/beads/references/WORKFLOWS.md +43 -39
  32. package/dist/template/.opencode/skill/beads-bridge/SKILL.md +80 -53
  33. package/dist/template/.opencode/skill/brainstorming/SKILL.md +19 -5
  34. package/dist/template/.opencode/skill/context-engineering/SKILL.md +30 -63
  35. package/dist/template/.opencode/skill/context-management/SKILL.md +115 -0
  36. package/dist/template/.opencode/skill/deep-research/SKILL.md +4 -4
  37. package/dist/template/.opencode/skill/development-lifecycle/SKILL.md +305 -0
  38. package/dist/template/.opencode/skill/memory-system/SKILL.md +3 -3
  39. package/dist/template/.opencode/skill/prd/SKILL.md +47 -122
  40. package/dist/template/.opencode/skill/prd-task/SKILL.md +48 -4
  41. package/dist/template/.opencode/skill/prd-task/references/prd-schema.json +120 -24
  42. package/dist/template/.opencode/skill/swarm-coordination/SKILL.md +79 -61
  43. package/dist/template/.opencode/skill/tool-priority/SKILL.md +31 -22
  44. package/dist/template/.opencode/tool/context7.ts +183 -0
  45. package/dist/template/.opencode/tool/memory-admin.ts +445 -0
  46. package/dist/template/.opencode/tool/swarm.ts +572 -0
  47. package/package.json +1 -1
  48. package/dist/template/.opencode/memory/_templates/spec.md +0 -66
  49. package/dist/template/.opencode/tool/beads-sync.ts +0 -657
  50. package/dist/template/.opencode/tool/context7-query-docs.ts +0 -89
  51. package/dist/template/.opencode/tool/context7-resolve-library-id.ts +0 -113
  52. package/dist/template/.opencode/tool/memory-maintain.ts +0 -167
  53. package/dist/template/.opencode/tool/memory-migrate.ts +0 -319
  54. package/dist/template/.opencode/tool/swarm-delegate.ts +0 -180
  55. package/dist/template/.opencode/tool/swarm-monitor.ts +0 -388
  56. package/dist/template/.opencode/tool/swarm-plan.ts +0 -697
@@ -0,0 +1,183 @@
1
+ import { tool } from "@opencode-ai/plugin";
2
+
3
+ // Context7 API v2 - https://context7.com/docs/api-guide
4
+ const CONTEXT7_API = "https://context7.com/api/v2";
5
+
6
+ interface LibraryInfo {
7
+ id: string;
8
+ title: string;
9
+ description?: string;
10
+ totalSnippets?: number;
11
+ trustScore?: number;
12
+ benchmarkScore?: number;
13
+ versions?: string[];
14
+ }
15
+
16
+ interface SearchResponse {
17
+ results: LibraryInfo[];
18
+ }
19
+
20
+ /**
21
+ * Consolidated Context7 documentation lookup tool.
22
+ * Operations: resolve (find library ID), query (get docs)
23
+ */
24
+ export default tool({
25
+ description: `Context7 documentation lookup: resolve library IDs and query docs.
26
+
27
+ Operations:
28
+ - "resolve": Find library ID from name (e.g., "react" → "/reactjs/react.dev")
29
+ - "query": Get documentation for a library topic
30
+
31
+ Example:
32
+ context7({ operation: "resolve", libraryName: "react" })
33
+ context7({ operation: "query", libraryId: "/reactjs/react.dev", topic: "hooks" })`,
34
+ args: {
35
+ operation: tool.schema
36
+ .string()
37
+ .optional()
38
+ .default("resolve")
39
+ .describe("Operation: resolve or query"),
40
+ libraryName: tool.schema
41
+ .string()
42
+ .optional()
43
+ .describe("Library name to resolve (for resolve operation)"),
44
+ libraryId: tool.schema
45
+ .string()
46
+ .optional()
47
+ .describe("Library ID from resolve (for query operation)"),
48
+ topic: tool.schema
49
+ .string()
50
+ .optional()
51
+ .describe("Documentation topic (for query operation)"),
52
+ },
53
+ execute: async (args: {
54
+ operation?: string;
55
+ libraryName?: string;
56
+ libraryId?: string;
57
+ topic?: string;
58
+ }) => {
59
+ const operation = args.operation || "resolve";
60
+
61
+ // Add API key if available
62
+ const apiKey = process.env.CONTEXT7_API_KEY;
63
+ const headers: HeadersInit = {
64
+ Accept: "application/json",
65
+ "User-Agent": "OpenCode/1.0",
66
+ };
67
+
68
+ if (apiKey) {
69
+ headers.Authorization = `Bearer ${apiKey}`;
70
+ }
71
+
72
+ // ===== RESOLVE =====
73
+ if (operation === "resolve") {
74
+ const { libraryName } = args;
75
+
76
+ if (!libraryName || libraryName.trim() === "") {
77
+ return "Error: libraryName is required for resolve operation";
78
+ }
79
+
80
+ try {
81
+ const url = new URL(`${CONTEXT7_API}/libs/search`);
82
+ url.searchParams.set("libraryName", libraryName);
83
+ url.searchParams.set("query", "documentation");
84
+
85
+ const response = await fetch(url.toString(), { headers });
86
+
87
+ if (!response.ok) {
88
+ if (response.status === 401) {
89
+ return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
90
+ }
91
+ if (response.status === 429) {
92
+ return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
93
+ }
94
+ return `Error: Context7 API returned ${response.status}`;
95
+ }
96
+
97
+ const data = (await response.json()) as SearchResponse;
98
+ const libraries = data.results || [];
99
+
100
+ if (!libraries || libraries.length === 0) {
101
+ return `No libraries found matching: ${libraryName}\n\nTry:\n- Different library name\n- Check spelling\n- Use official package name`;
102
+ }
103
+
104
+ const formatted = libraries
105
+ .slice(0, 5)
106
+ .map((lib, i) => {
107
+ const desc = lib.description
108
+ ? `\n ${lib.description.slice(0, 100)}...`
109
+ : "";
110
+ const snippets = lib.totalSnippets
111
+ ? ` (${lib.totalSnippets} snippets)`
112
+ : "";
113
+ const score = lib.benchmarkScore
114
+ ? ` [score: ${lib.benchmarkScore}]`
115
+ : "";
116
+ return `${i + 1}. **${lib.title}** → \`${lib.id}\`${snippets}${score}${desc}`;
117
+ })
118
+ .join("\n\n");
119
+
120
+ return `Found ${libraries.length} libraries matching "${libraryName}":
121
+
122
+ ${formatted}
123
+
124
+ **Next step**: Use \`context7({ operation: "query", libraryId: "${libraries[0].id}", topic: "your topic" })\` to fetch documentation.`;
125
+ } catch (error: unknown) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ return `Error resolving library: ${message}`;
128
+ }
129
+ }
130
+
131
+ // ===== QUERY =====
132
+ if (operation === "query") {
133
+ const { libraryId, topic } = args;
134
+
135
+ if (!libraryId || libraryId.trim() === "") {
136
+ return 'Error: libraryId is required (use operation: "resolve" first)';
137
+ }
138
+
139
+ if (!topic || topic.trim() === "") {
140
+ return "Error: topic is required (e.g., 'hooks', 'setup', 'API reference')";
141
+ }
142
+
143
+ try {
144
+ const url = new URL(`${CONTEXT7_API}/context`);
145
+ url.searchParams.set("libraryId", libraryId);
146
+ url.searchParams.set("query", topic);
147
+
148
+ const queryHeaders = { ...headers, Accept: "text/plain" };
149
+ const response = await fetch(url.toString(), {
150
+ headers: queryHeaders,
151
+ });
152
+
153
+ if (!response.ok) {
154
+ if (response.status === 401) {
155
+ return `Error: Invalid CONTEXT7_API_KEY. Get a free key at https://context7.com/dashboard`;
156
+ }
157
+ if (response.status === 404) {
158
+ return `Error: Library not found: ${libraryId}\n\nUse operation: "resolve" first to find the correct ID.`;
159
+ }
160
+ if (response.status === 429) {
161
+ return `Error: Rate limit exceeded. Get a free API key at https://context7.com/dashboard for higher limits.`;
162
+ }
163
+ return `Error: Context7 API returned ${response.status}`;
164
+ }
165
+
166
+ const content = await response.text();
167
+
168
+ if (!content || content.trim() === "") {
169
+ return `No documentation found for "${topic}" in ${libraryId}.\n\nTry:\n- Simpler terms (e.g., "useState" instead of "state management")\n- Different topic spelling\n- Broader topics like "API reference" or "getting started"`;
170
+ }
171
+
172
+ return `# Documentation: ${topic} (${libraryId})
173
+
174
+ ${content}`;
175
+ } catch (error: unknown) {
176
+ const message = error instanceof Error ? error.message : String(error);
177
+ return `Error querying documentation: ${message}`;
178
+ }
179
+ }
180
+
181
+ return `Unknown operation: ${operation}. Use: resolve, query`;
182
+ },
183
+ });
@@ -0,0 +1,445 @@
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
+ }