wogiflow 1.0.12 → 1.0.13

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 (45) hide show
  1. package/.workflow/specs/architecture.md.template +24 -0
  2. package/.workflow/specs/stack.md.template +33 -0
  3. package/.workflow/specs/testing.md.template +36 -0
  4. package/README.md +90 -1
  5. package/package.json +1 -1
  6. package/scripts/MEMORY-ARCHITECTURE.md +150 -0
  7. package/scripts/flow +20 -19
  8. package/scripts/flow-auto-context.js +97 -3
  9. package/scripts/flow-conflict-resolver.js +735 -0
  10. package/scripts/flow-context-gatherer.js +520 -0
  11. package/scripts/flow-context-monitor.js +148 -19
  12. package/scripts/flow-damage-control.js +5 -1
  13. package/scripts/flow-export-profile +168 -1
  14. package/scripts/flow-import-profile +257 -6
  15. package/scripts/flow-instruction-richness.js +182 -18
  16. package/scripts/flow-knowledge-router.js +2 -0
  17. package/scripts/flow-knowledge-sync.js +2 -0
  18. package/scripts/{flow-transcript-chunking.js → flow-long-input-chunking.js} +4 -2
  19. package/scripts/{flow-transcript-parsing.js → flow-long-input-parsing.js} +35 -0
  20. package/scripts/{flow-transcript-stories.js → flow-long-input-stories.js} +86 -38
  21. package/scripts/{flow-transcript-digest.js → flow-long-input.js} +231 -15
  22. package/scripts/flow-memory-db.js +386 -1
  23. package/scripts/flow-memory-sync.js +2 -0
  24. package/scripts/flow-model-adapter.js +53 -29
  25. package/scripts/flow-model-router.js +246 -1
  26. package/scripts/flow-morning.js +94 -0
  27. package/scripts/flow-onboard +223 -10
  28. package/scripts/flow-orchestrate-validation.js +539 -0
  29. package/scripts/flow-orchestrate.js +16 -507
  30. package/scripts/flow-pattern-extractor.js +1265 -0
  31. package/scripts/flow-prompt-composer.js +222 -2
  32. package/scripts/flow-quality-guard.js +594 -0
  33. package/scripts/flow-section-index.js +713 -0
  34. package/scripts/flow-section-resolver.js +484 -0
  35. package/scripts/flow-session-end.js +188 -2
  36. package/scripts/flow-skill-create.js +19 -3
  37. package/scripts/flow-skill-matcher.js +122 -7
  38. package/scripts/flow-statusline-setup.js +218 -0
  39. package/scripts/flow-step-review.js +19 -0
  40. package/scripts/flow-tech-debt.js +734 -0
  41. package/scripts/flow-utils.js +2 -0
  42. package/scripts/hooks/core/long-input-gate.js +293 -0
  43. package/scripts/flow-parallel-detector.js +0 -399
  44. package/scripts/flow-parallel-dispatch.js +0 -987
  45. /package/scripts/{flow-transcript-language.js → flow-long-input-language.js} +0 -0
@@ -3,6 +3,8 @@
3
3
  /**
4
4
  * Wogi Flow - Memory Database Module
5
5
  *
6
+ * See MEMORY-ARCHITECTURE.md for how this fits with other memory/knowledge modules.
7
+ *
6
8
  * Shared database operations for memory storage.
7
9
  * Used by both MCP server and CLI tools.
8
10
  *
@@ -27,6 +29,37 @@ const WORKFLOW_DIR = path.join(PROJECT_ROOT, '.workflow');
27
29
  const MEMORY_DIR = path.join(WORKFLOW_DIR, 'memory');
28
30
  const DB_PATH = path.join(MEMORY_DIR, 'local.db');
29
31
 
32
+ // ============================================================
33
+ // Safe JSON Helpers
34
+ // ============================================================
35
+
36
+ /**
37
+ * Safely parse pins JSON with validation
38
+ * Prevents prototype pollution and validates structure
39
+ * @param {string} pinsJson - JSON string of pins array
40
+ * @returns {string[]} - Parsed pins array (empty on error)
41
+ */
42
+ function safeParsePins(pinsJson) {
43
+ if (!pinsJson || pinsJson === '[]') return [];
44
+
45
+ try {
46
+ // Check for prototype pollution attempts
47
+ if (/__proto__|constructor|prototype/i.test(pinsJson)) {
48
+ console.warn('[safeParsePins] Suspicious content detected in pins JSON');
49
+ return [];
50
+ }
51
+
52
+ const parsed = JSON.parse(pinsJson);
53
+
54
+ // Validate it's an array of strings
55
+ if (!Array.isArray(parsed)) return [];
56
+
57
+ return parsed.filter(p => typeof p === 'string');
58
+ } catch {
59
+ return [];
60
+ }
61
+ }
62
+
30
63
  // ============================================================
31
64
  // Database Singleton
32
65
  // ============================================================
@@ -162,6 +195,26 @@ async function initDatabase() {
162
195
  )
163
196
  `);
164
197
 
198
+ // Section index table for Smart Context System (Phase 1)
199
+ db.run(`
200
+ CREATE TABLE IF NOT EXISTS sections (
201
+ id TEXT PRIMARY KEY,
202
+ source TEXT NOT NULL,
203
+ category TEXT,
204
+ title TEXT NOT NULL,
205
+ pins TEXT,
206
+ content TEXT NOT NULL,
207
+ line_start INTEGER,
208
+ line_end INTEGER,
209
+ content_hash TEXT,
210
+ embedding TEXT,
211
+ access_count INTEGER DEFAULT 0,
212
+ last_accessed TEXT,
213
+ created_at TEXT DEFAULT (datetime('now')),
214
+ updated_at TEXT DEFAULT (datetime('now'))
215
+ )
216
+ `);
217
+
165
218
  // Migrate existing databases - add new columns if they don't exist
166
219
  const migrations = [
167
220
  'ALTER TABLE facts ADD COLUMN last_accessed TEXT',
@@ -183,6 +236,9 @@ async function initDatabase() {
183
236
  try { db.run('CREATE INDEX IF NOT EXISTS idx_facts_cold_archived ON facts_cold(archived_at)'); } catch {}
184
237
  try { db.run('CREATE INDEX IF NOT EXISTS idx_proposals_status ON proposals(status)'); } catch {}
185
238
  try { db.run('CREATE INDEX IF NOT EXISTS idx_prd_prd_id ON prd_chunks(prd_id)'); } catch {}
239
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_source ON sections(source)'); } catch {}
240
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_category ON sections(category)'); } catch {}
241
+ try { db.run('CREATE INDEX IF NOT EXISTS idx_sections_hash ON sections(content_hash)'); } catch {}
186
242
 
187
243
  saveDatabase();
188
244
  return db;
@@ -240,7 +296,7 @@ async function getEmbedder() {
240
296
  }
241
297
  return null;
242
298
  }
243
- throw e; // Re-throw other errors
299
+ throw err; // Re-throw other errors
244
300
  }
245
301
  }
246
302
  return embedder;
@@ -1051,6 +1107,326 @@ async function restoreFromColdStorage(factId) {
1051
1107
  return { restored: true };
1052
1108
  }
1053
1109
 
1110
+ // ============================================================
1111
+ // Section Index Operations (Smart Context System)
1112
+ // ============================================================
1113
+
1114
+ /**
1115
+ * Sync sections from section-index.json to database
1116
+ * @param {Object} index - Section index object from flow-section-index.js
1117
+ * @returns {Object} - { synced, updated, unchanged }
1118
+ */
1119
+ async function syncSectionsFromIndex(index) {
1120
+ await initDatabase();
1121
+ const results = { synced: 0, updated: 0, unchanged: 0, deleted: 0 };
1122
+
1123
+ if (!index || !index.sources) {
1124
+ return results;
1125
+ }
1126
+
1127
+ // Collect all section IDs from index
1128
+ const indexSectionIds = new Set();
1129
+
1130
+ for (const [sourceName, sourceData] of Object.entries(index.sources)) {
1131
+ const items = sourceData.sections || sourceData.rows || [];
1132
+
1133
+ for (const item of items) {
1134
+ indexSectionIds.add(item.id);
1135
+
1136
+ // Check if section exists
1137
+ const existing = db.exec('SELECT content_hash FROM sections WHERE id = ?', [item.id]);
1138
+ const existingRows = queryToRows(existing);
1139
+
1140
+ if (existingRows.length === 0) {
1141
+ // Insert new section
1142
+ db.run(`
1143
+ INSERT INTO sections (id, source, category, title, pins, content, line_start, line_end, content_hash)
1144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1145
+ `, [
1146
+ item.id,
1147
+ sourceName,
1148
+ item.category || null,
1149
+ item.title || item.name,
1150
+ JSON.stringify(item.pins || []),
1151
+ item.content || JSON.stringify(item.data || {}),
1152
+ item.lineStart || item.line || null,
1153
+ item.lineEnd || item.line || null,
1154
+ item.contentHash || null
1155
+ ]);
1156
+ results.synced++;
1157
+ } else if (existingRows[0].content_hash !== item.contentHash) {
1158
+ // Update existing section if content changed
1159
+ db.run(`
1160
+ UPDATE sections SET
1161
+ category = ?, title = ?, pins = ?, content = ?,
1162
+ line_start = ?, line_end = ?, content_hash = ?,
1163
+ updated_at = datetime('now')
1164
+ WHERE id = ?
1165
+ `, [
1166
+ item.category || null,
1167
+ item.title || item.name,
1168
+ JSON.stringify(item.pins || []),
1169
+ item.content || JSON.stringify(item.data || {}),
1170
+ item.lineStart || item.line || null,
1171
+ item.lineEnd || item.line || null,
1172
+ item.contentHash || null,
1173
+ item.id
1174
+ ]);
1175
+ results.updated++;
1176
+ } else {
1177
+ results.unchanged++;
1178
+ }
1179
+ }
1180
+ }
1181
+
1182
+ // Remove sections that are no longer in the index
1183
+ const existingResult = db.exec('SELECT id FROM sections');
1184
+ const existingIds = queryToRows(existingResult).map(r => r.id);
1185
+
1186
+ for (const existingId of existingIds) {
1187
+ if (!indexSectionIds.has(existingId)) {
1188
+ db.run('DELETE FROM sections WHERE id = ?', [existingId]);
1189
+ results.deleted++;
1190
+ }
1191
+ }
1192
+
1193
+ saveDatabase();
1194
+ return results;
1195
+ }
1196
+
1197
+ /**
1198
+ * Search sections by pins (keyword matching)
1199
+ * @param {string[]} pins - Pins to match
1200
+ * @param {Object} options - { limit, trackAccess }
1201
+ * @returns {Object[]} - Matching sections with match scores
1202
+ */
1203
+ async function searchSectionsByPins(pins, options = {}) {
1204
+ await initDatabase();
1205
+ const { limit = 20, trackAccess = true } = options;
1206
+
1207
+ const result = db.exec('SELECT * FROM sections');
1208
+ const sections = queryToRows(result);
1209
+
1210
+ if (sections.length === 0) return [];
1211
+
1212
+ const pinsLower = pins.map(p => p.toLowerCase());
1213
+
1214
+ // Score each section by pin matches
1215
+ const scored = sections.map(section => {
1216
+ const sectionPins = safeParsePins(section.pins).map(p => p.toLowerCase());
1217
+ const matchCount = pinsLower.filter(p => sectionPins.includes(p)).length;
1218
+ const matchScore = pinsLower.length > 0 ? matchCount / pinsLower.length : 0;
1219
+
1220
+ return {
1221
+ ...section,
1222
+ pins: safeParsePins(section.pins),
1223
+ matchCount,
1224
+ matchScore
1225
+ };
1226
+ });
1227
+
1228
+ // Filter and sort
1229
+ const matches = scored
1230
+ .filter(s => s.matchCount > 0)
1231
+ .sort((a, b) => b.matchScore - a.matchScore)
1232
+ .slice(0, limit);
1233
+
1234
+ // Track access
1235
+ if (trackAccess && matches.length > 0) {
1236
+ const ids = matches.map(m => m.id);
1237
+ db.run(`
1238
+ UPDATE sections SET
1239
+ access_count = access_count + 1,
1240
+ last_accessed = datetime('now')
1241
+ WHERE id IN (${ids.map(() => '?').join(',')})
1242
+ `, ids);
1243
+ saveDatabase();
1244
+ }
1245
+
1246
+ return matches;
1247
+ }
1248
+
1249
+ /**
1250
+ * Search sections by semantic similarity (with embedding)
1251
+ * Falls back to text search if embeddings unavailable
1252
+ * @param {string} query - Search query
1253
+ * @param {Object} options - { limit, category, trackAccess }
1254
+ * @returns {Object[]} - Matching sections with similarity scores
1255
+ */
1256
+ async function searchSectionsBySimilarity(query, options = {}) {
1257
+ await initDatabase();
1258
+ const { limit = 10, category = null, trackAccess = true } = options;
1259
+
1260
+ const queryEmbedding = await getEmbedding(query);
1261
+
1262
+ let sql = 'SELECT * FROM sections WHERE 1=1';
1263
+ const params = [];
1264
+
1265
+ if (category) {
1266
+ sql += ' AND category = ?';
1267
+ params.push(category);
1268
+ }
1269
+
1270
+ const result = db.exec(sql, params);
1271
+ const sections = queryToRows(result);
1272
+
1273
+ if (sections.length === 0) return [];
1274
+
1275
+ let scored;
1276
+ if (queryEmbedding) {
1277
+ // Semantic search with embeddings
1278
+ scored = sections.map(section => {
1279
+ const embedding = section.embedding ? jsonToEmbedding(section.embedding) : [];
1280
+ const similarity = embedding.length > 0 ? cosineSimilarity(queryEmbedding, embedding) : 0;
1281
+ return { ...section, pins: safeParsePins(section.pins), similarity };
1282
+ });
1283
+ } else {
1284
+ // Fallback: text matching
1285
+ const queryLower = query.toLowerCase();
1286
+ const queryWords = queryLower.split(/\s+/).filter(w => w.length > 2);
1287
+ scored = sections.map(section => {
1288
+ const contentLower = (section.content + ' ' + section.title).toLowerCase();
1289
+ const matches = queryWords.filter(w => contentLower.includes(w)).length;
1290
+ const similarity = queryWords.length > 0 ? matches / queryWords.length : 0;
1291
+ return { ...section, pins: safeParsePins(section.pins), similarity };
1292
+ });
1293
+ }
1294
+
1295
+ // Sort and limit
1296
+ const matches = scored
1297
+ .sort((a, b) => b.similarity - a.similarity)
1298
+ .slice(0, limit);
1299
+
1300
+ // Track access
1301
+ if (trackAccess && matches.length > 0) {
1302
+ const ids = matches.filter(m => m.similarity > 0.1).map(m => m.id);
1303
+ if (ids.length > 0) {
1304
+ db.run(`
1305
+ UPDATE sections SET
1306
+ access_count = access_count + 1,
1307
+ last_accessed = datetime('now')
1308
+ WHERE id IN (${ids.map(() => '?').join(',')})
1309
+ `, ids);
1310
+ saveDatabase();
1311
+ }
1312
+ }
1313
+
1314
+ return matches;
1315
+ }
1316
+
1317
+ /**
1318
+ * Get section by ID
1319
+ * @param {string} sectionId - Section ID
1320
+ * @param {boolean} trackAccess - Whether to track access
1321
+ * @returns {Object|null} - Section object or null
1322
+ */
1323
+ async function getSectionById(sectionId, trackAccess = true) {
1324
+ await initDatabase();
1325
+
1326
+ const result = db.exec('SELECT * FROM sections WHERE id = ?', [sectionId]);
1327
+ const rows = queryToRows(result);
1328
+
1329
+ if (rows.length === 0) return null;
1330
+
1331
+ const section = rows[0];
1332
+ section.pins = safeParsePins(section.pins);
1333
+
1334
+ // Track access
1335
+ if (trackAccess) {
1336
+ db.run(`
1337
+ UPDATE sections SET
1338
+ access_count = access_count + 1,
1339
+ last_accessed = datetime('now')
1340
+ WHERE id = ?
1341
+ `, [sectionId]);
1342
+ saveDatabase();
1343
+ }
1344
+
1345
+ return section;
1346
+ }
1347
+
1348
+ /**
1349
+ * Get all sections from a source
1350
+ * @param {string} source - Source file name (e.g., "decisions.md")
1351
+ * @returns {Object[]} - All sections from that source
1352
+ */
1353
+ async function getSectionsBySource(source) {
1354
+ await initDatabase();
1355
+
1356
+ const result = db.exec('SELECT * FROM sections WHERE source = ? ORDER BY line_start', [source]);
1357
+ const sections = queryToRows(result);
1358
+
1359
+ return sections.map(s => ({
1360
+ ...s,
1361
+ pins: safeParsePins(s.pins)
1362
+ }));
1363
+ }
1364
+
1365
+ /**
1366
+ * Get section statistics
1367
+ * @returns {Object} - Stats about sections
1368
+ */
1369
+ async function getSectionStats() {
1370
+ await initDatabase();
1371
+
1372
+ function count(sql, params = []) {
1373
+ const result = db.exec(sql, params);
1374
+ if (!result.length || !result[0].values.length) return 0;
1375
+ return result[0].values[0][0];
1376
+ }
1377
+
1378
+ function grouped(sql) {
1379
+ const result = db.exec(sql);
1380
+ if (!result.length) return {};
1381
+ return Object.fromEntries(result[0].values.map(row => [row[0] || 'unknown', row[1]]));
1382
+ }
1383
+
1384
+ return {
1385
+ total: count('SELECT COUNT(*) FROM sections'),
1386
+ bySource: grouped('SELECT source, COUNT(*) FROM sections GROUP BY source'),
1387
+ byCategory: grouped('SELECT category, COUNT(*) FROM sections GROUP BY category'),
1388
+ neverAccessed: count('SELECT COUNT(*) FROM sections WHERE access_count = 0'),
1389
+ topAccessed: queryToRows(db.exec(`
1390
+ SELECT id, title, source, access_count
1391
+ FROM sections
1392
+ WHERE access_count > 0
1393
+ ORDER BY access_count DESC
1394
+ LIMIT 5
1395
+ `))
1396
+ };
1397
+ }
1398
+
1399
+ /**
1400
+ * Generate embeddings for sections that don't have them
1401
+ * @returns {Object} - { generated, skipped, failed }
1402
+ */
1403
+ async function generateSectionEmbeddings() {
1404
+ await initDatabase();
1405
+ const results = { generated: 0, skipped: 0, failed: 0 };
1406
+
1407
+ const result = db.exec('SELECT id, content, title FROM sections WHERE embedding IS NULL');
1408
+ const sections = queryToRows(result);
1409
+
1410
+ for (const section of sections) {
1411
+ try {
1412
+ const text = `${section.title}\n${section.content}`;
1413
+ const embedding = await getEmbedding(text);
1414
+
1415
+ if (embedding) {
1416
+ db.run('UPDATE sections SET embedding = ? WHERE id = ?', [embeddingToJson(embedding), section.id]);
1417
+ results.generated++;
1418
+ } else {
1419
+ results.skipped++;
1420
+ }
1421
+ } catch (err) {
1422
+ results.failed++;
1423
+ }
1424
+ }
1425
+
1426
+ saveDatabase();
1427
+ return results;
1428
+ }
1429
+
1054
1430
  // ============================================================
1055
1431
  // Exports
1056
1432
  // ============================================================
@@ -1104,6 +1480,15 @@ module.exports = {
1104
1480
  getPromotionCandidates,
1105
1481
  restoreFromColdStorage,
1106
1482
 
1483
+ // Sections (Smart Context System)
1484
+ syncSectionsFromIndex,
1485
+ searchSectionsByPins,
1486
+ searchSectionsBySimilarity,
1487
+ getSectionById,
1488
+ getSectionsBySource,
1489
+ getSectionStats,
1490
+ generateSectionEmbeddings,
1491
+
1107
1492
  // Paths
1108
1493
  DB_PATH,
1109
1494
  MEMORY_DIR
@@ -3,6 +3,8 @@
3
3
  /**
4
4
  * Wogi Flow - Memory to Instructions Sync
5
5
  *
6
+ * See MEMORY-ARCHITECTURE.md for how this fits with other memory/knowledge modules.
7
+ *
6
8
  * Promotes high-relevance facts and patterns to decisions.md
7
9
  * This is the "self-editing core memory" feature.
8
10
  *
@@ -56,34 +56,40 @@ const MODEL_PATTERNS = {
56
56
 
57
57
  /**
58
58
  * Get current model from config or environment
59
+ *
60
+ * Note: This delegates to flow-models.js which is the single source of truth
61
+ * for model detection. This function returns just the normalized name string
62
+ * for backward compatibility with callers expecting a string.
59
63
  */
60
64
  function getCurrentModel() {
61
- const config = getConfig();
65
+ try {
66
+ // Use flow-models as the single source of truth
67
+ const { getCurrentModel: getModelFromRegistry } = require('./flow-models');
68
+ const result = getModelFromRegistry();
69
+ return normalizeModelName(result.name || 'claude-opus');
70
+ } catch {
71
+ // Fallback if flow-models not available
72
+ const config = getConfig();
62
73
 
63
- // Check hybrid mode config first
64
- if (config.hybrid?.enabled) {
65
- // New config structure: hybrid.executor.model
66
- if (config.hybrid.executor?.model) {
67
- return normalizeModelName(config.hybrid.executor.model);
74
+ if (config.hybrid?.enabled) {
75
+ if (config.hybrid.executor?.model) {
76
+ return normalizeModelName(config.hybrid.executor.model);
77
+ }
78
+ if (config.hybrid.model) {
79
+ return normalizeModelName(config.hybrid.model);
80
+ }
68
81
  }
69
- // Legacy config structure: hybrid.model directly
70
- if (config.hybrid.model) {
71
- return normalizeModelName(config.hybrid.model);
82
+
83
+ if (process.env.CLAUDE_MODEL) {
84
+ return normalizeModelName(process.env.CLAUDE_MODEL);
72
85
  }
73
- }
74
86
 
75
- // Check environment variable
76
- if (process.env.CLAUDE_MODEL) {
77
- return normalizeModelName(process.env.CLAUDE_MODEL);
78
- }
87
+ if (config.modelAdapters?.currentModel) {
88
+ return normalizeModelName(config.modelAdapters.currentModel);
89
+ }
79
90
 
80
- // Check modelAdapters config
81
- if (config.modelAdapters?.currentModel) {
82
- return normalizeModelName(config.modelAdapters.currentModel);
91
+ return 'claude-opus';
83
92
  }
84
-
85
- // Default to claude-opus (most capable)
86
- return 'claude-opus';
87
93
  }
88
94
 
89
95
  /**
@@ -130,6 +136,18 @@ function getAdapterPath(modelName) {
130
136
  return path.join(ADAPTERS_DIR, `${normalized}.md`);
131
137
  }
132
138
 
139
+ /**
140
+ * Safely read file content with try-catch
141
+ */
142
+ function safeReadFile(filePath) {
143
+ try {
144
+ return fs.readFileSync(filePath, 'utf-8');
145
+ } catch (err) {
146
+ // File may have been deleted/moved between existsSync and readFileSync (TOCTOU)
147
+ return null;
148
+ }
149
+ }
150
+
133
151
  /**
134
152
  * Load adapter file content
135
153
  */
@@ -142,9 +160,11 @@ function loadAdapter(modelName) {
142
160
  const familyPath = path.join(ADAPTERS_DIR, `${family}-default.md`);
143
161
 
144
162
  if (fs.existsSync(familyPath)) {
163
+ const content = safeReadFile(familyPath);
164
+ if (content === null) return null;
145
165
  return {
146
166
  path: familyPath,
147
- content: fs.readFileSync(familyPath, 'utf-8'),
167
+ content,
148
168
  isDefault: true
149
169
  };
150
170
  }
@@ -152,9 +172,11 @@ function loadAdapter(modelName) {
152
172
  // Load template as last resort
153
173
  const templatePath = path.join(ADAPTERS_DIR, '_template.md');
154
174
  if (fs.existsSync(templatePath)) {
175
+ const content = safeReadFile(templatePath);
176
+ if (content === null) return null;
155
177
  return {
156
178
  path: templatePath,
157
- content: fs.readFileSync(templatePath, 'utf-8'),
179
+ content,
158
180
  isTemplate: true
159
181
  };
160
182
  }
@@ -162,9 +184,11 @@ function loadAdapter(modelName) {
162
184
  return null;
163
185
  }
164
186
 
187
+ const content = safeReadFile(adapterPath);
188
+ if (content === null) return null;
165
189
  return {
166
190
  path: adapterPath,
167
- content: fs.readFileSync(adapterPath, 'utf-8'),
191
+ content,
168
192
  isDefault: false
169
193
  };
170
194
  }
@@ -476,7 +500,7 @@ function storeSingleLearning(modelName, learning, context = {}) {
476
500
  let content = '';
477
501
 
478
502
  if (fs.existsSync(adapterPath)) {
479
- content = fs.readFileSync(adapterPath, 'utf-8');
503
+ content = safeReadFile(adapterPath) || '';
480
504
  } else {
481
505
  // Ensure directory exists
482
506
  const dir = path.dirname(adapterPath);
@@ -487,8 +511,8 @@ function storeSingleLearning(modelName, learning, context = {}) {
487
511
  // Create from template or minimal
488
512
  const templatePath = path.join(ADAPTERS_DIR, '_template.md');
489
513
  if (fs.existsSync(templatePath)) {
490
- content = fs.readFileSync(templatePath, 'utf-8')
491
- .replace('{{MODEL_NAME}}', modelName);
514
+ const templateContent = safeReadFile(templatePath);
515
+ content = templateContent ? templateContent.replace('{{MODEL_NAME}}', modelName) : `# ${modelName} Adapter\n\n## Learnings\n`;
492
516
  } else {
493
517
  content = `# ${modelName} Adapter\n\n## Learnings\n`;
494
518
  }
@@ -539,13 +563,13 @@ function addLearningToAdapter(modelName, errors) {
539
563
  let content = '';
540
564
 
541
565
  if (fs.existsSync(adapterPath)) {
542
- content = fs.readFileSync(adapterPath, 'utf-8');
566
+ content = safeReadFile(adapterPath) || '';
543
567
  } else {
544
568
  // Create from template
545
569
  const templatePath = path.join(ADAPTERS_DIR, '_template.md');
546
570
  if (fs.existsSync(templatePath)) {
547
- content = fs.readFileSync(templatePath, 'utf-8')
548
- .replace('{{MODEL_NAME}}', modelName);
571
+ const templateContent = safeReadFile(templatePath);
572
+ content = templateContent ? templateContent.replace('{{MODEL_NAME}}', modelName) : `# ${modelName} Adapter\n\n## Learnings\n`;
549
573
  } else {
550
574
  content = `# ${modelName} Adapter\n\n## Learnings\n`;
551
575
  }