metame-cli 1.5.3 → 1.5.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 (51) hide show
  1. package/README.md +60 -18
  2. package/index.js +352 -79
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +178 -90
  6. package/scripts/daemon-admin-commands.js +353 -105
  7. package/scripts/daemon-agent-commands.js +434 -66
  8. package/scripts/daemon-bridges.js +477 -68
  9. package/scripts/daemon-claude-engine.js +1267 -674
  10. package/scripts/daemon-command-router.js +205 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +7 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +108 -49
  15. package/scripts/daemon-file-browser.js +64 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +55 -1
  19. package/scripts/daemon-runtime-lifecycle.js +87 -0
  20. package/scripts/daemon-session-commands.js +102 -45
  21. package/scripts/daemon-session-store.js +497 -66
  22. package/scripts/daemon-siri-bridge.js +234 -0
  23. package/scripts/daemon-siri-imessage.js +209 -0
  24. package/scripts/daemon-task-scheduler.js +10 -2
  25. package/scripts/daemon.js +697 -179
  26. package/scripts/daemon.yaml +7 -0
  27. package/scripts/docs/agent-guide.md +36 -3
  28. package/scripts/docs/hook-config.md +134 -0
  29. package/scripts/docs/maintenance-manual.md +162 -5
  30. package/scripts/docs/pointer-map.md +60 -5
  31. package/scripts/feishu-adapter.js +7 -15
  32. package/scripts/hooks/doc-router.js +29 -0
  33. package/scripts/hooks/hook-utils.js +61 -0
  34. package/scripts/hooks/intent-doc-router.js +54 -0
  35. package/scripts/hooks/intent-engine.js +72 -0
  36. package/scripts/hooks/intent-file-transfer.js +51 -0
  37. package/scripts/hooks/intent-memory-recall.js +35 -0
  38. package/scripts/hooks/intent-ops-assist.js +54 -0
  39. package/scripts/hooks/intent-task-create.js +35 -0
  40. package/scripts/hooks/intent-team-dispatch.js +106 -0
  41. package/scripts/hooks/team-context.js +143 -0
  42. package/scripts/intent-registry.js +59 -0
  43. package/scripts/memory-extract.js +59 -0
  44. package/scripts/memory-nightly-reflect.js +109 -43
  45. package/scripts/memory.js +55 -17
  46. package/scripts/mentor-engine.js +6 -0
  47. package/scripts/schema.js +1 -0
  48. package/scripts/self-reflect.js +110 -12
  49. package/scripts/session-analytics.js +160 -0
  50. package/scripts/signal-capture.js +1 -1
  51. package/scripts/team-dispatch.js +315 -0
@@ -346,6 +346,65 @@ async function run() {
346
346
  }
347
347
  }
348
348
 
349
+ // ── Codex sessions ──────────────────────────────────────────────────────
350
+ // Same pipeline, different source: reads ~/.codex/sessions rollout files
351
+ // (first 2KB only) + history.jsonl for user messages.
352
+ const codexSessions = sessionAnalytics.findAllUnextractedCodexSessions(3);
353
+ if (codexSessions.length > 0) {
354
+ // Pass session IDs so loadCodexHistory only parses relevant entries
355
+ // (history.jsonl grows unbounded; no need to load the full file)
356
+ const historyMap = sessionAnalytics.loadCodexHistory(codexSessions.map(cs => cs.session_id));
357
+ for (const cs of codexSessions) {
358
+ try {
359
+ const { skeleton, evidence } = sessionAnalytics.buildCodexInput(cs.path, historyMap);
360
+
361
+ // Skip trivial sessions with no user messages
362
+ if (skeleton.message_count < 1) {
363
+ sessionAnalytics.markCodexFactsExtracted(cs.session_id);
364
+ continue;
365
+ }
366
+
367
+ const { ok, facts, session_name } = await extractFacts(skeleton, evidence, distillEnv);
368
+ if (!ok) {
369
+ console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)}: extraction failed, will retry later`);
370
+ continue;
371
+ }
372
+
373
+ if (facts.length > 0) {
374
+ const fallbackScope = `codex_${String(cs.session_id).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24)}`;
375
+ const { saved, skipped, superseded, savedFacts } = memory.saveFacts(
376
+ cs.session_id,
377
+ skeleton.project || 'unknown',
378
+ facts,
379
+ { scope: skeleton.project_id || fallbackScope, source_type: 'codex' }
380
+ );
381
+ let labelsSaved = 0;
382
+ if (typeof memory.saveFactLabels === 'function' && Array.isArray(savedFacts) && savedFacts.length > 0) {
383
+ const labelRows = buildFactLabelRows(facts, savedFacts);
384
+ if (labelRows.length > 0) {
385
+ const lr = memory.saveFactLabels(labelRows);
386
+ labelsSaved = Number(lr && lr.saved) || 0;
387
+ }
388
+ }
389
+ totalSaved += saved;
390
+ totalSkipped += skipped;
391
+ const superMsg = superseded > 0 ? `, ${superseded} superseded` : '';
392
+ const labelMsg = labelsSaved > 0 ? `, ${labelsSaved} labels` : '';
393
+ console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): ${saved} facts saved${superMsg}${labelMsg}`);
394
+ } else {
395
+ console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): no facts extracted`);
396
+ }
397
+
398
+ sessionAnalytics.markCodexFactsExtracted(cs.session_id);
399
+ saveSessionTag(cs.session_id, session_name, facts);
400
+ processed++;
401
+ } catch (e) {
402
+ console.log(`[memory-extract] Codex session error: ${e.message}`);
403
+ }
404
+ }
405
+ }
406
+ // ── end Codex ────────────────────────────────────────────────────────────
407
+
349
408
  memory.close();
350
409
  return { sessionsProcessed: processed, factsSaved: totalSaved, factsSkipped: totalSkipped };
351
410
  } finally {
@@ -108,13 +108,13 @@ function writeReflectLog(record) {
108
108
  * Query hot zone facts from memory.db.
109
109
  * Returns array of plain objects.
110
110
  */
111
- function queryHotFacts(db) {
111
+ function queryHotFacts(db, windowDays = WINDOW_DAYS) {
112
112
  const relationPlaceholders = EXCLUDED_RELATIONS.map(() => '?').join(', ');
113
113
  const stmt = db.prepare(`
114
114
  SELECT id, entity, relation, value, confidence, search_count, created_at
115
115
  FROM facts
116
116
  WHERE search_count >= ${MIN_SEARCH_COUNT}
117
- AND created_at >= datetime('now', '-${WINDOW_DAYS} days')
117
+ AND created_at >= datetime('now', '-${windowDays} days')
118
118
  AND superseded_by IS NULL
119
119
  AND (conflict_status IS NULL OR conflict_status = 'OK')
120
120
  AND relation NOT IN (${relationPlaceholders})
@@ -212,10 +212,8 @@ function parseJsonFromLlm(raw) {
212
212
  try { return JSON.parse(cleaned); } catch { return null; }
213
213
  }
214
214
 
215
- function writeCapsuleFile(filePath, capsule, facts, today, prefix) {
216
- const related = Array.isArray(capsule.related_concepts) ? capsule.related_concepts.slice(0, 8) : [];
217
- const supporting = Array.isArray(capsule.supporting_facts) ? capsule.supporting_facts.slice(0, 8) : [];
218
- const content = `---
215
+ function writeCapsuleFile(filePath, markdownContent, facts, today, prefix) {
216
+ const frontmatter = `---
219
217
  date: ${today}
220
218
  source: nightly-reflect
221
219
  type: knowledge-capsule
@@ -223,21 +221,30 @@ entity_prefix: ${prefix}
223
221
  facts_analyzed: ${Array.isArray(facts) ? facts.length : 0}
224
222
  ---
225
223
 
226
- # ${capsule.title}
227
-
228
- ## 核心结论
229
- ${capsule.core_conclusion}
230
-
231
- ## 适用场景
232
- ${capsule.applicable_scenarios}
233
-
234
- ## 关联概念
235
- ${related.length > 0 ? related.map(x => `- ${x}`).join('\n') : '- (none)'}
236
-
237
- ## 支撑事实
238
- ${supporting.length > 0 ? supporting.map(x => `- ${x}`).join('\n') : '- (none)'}
239
224
  `;
240
- fs.writeFileSync(filePath, content, 'utf8');
225
+ try {
226
+ fs.writeFileSync(filePath, frontmatter + markdownContent, 'utf8');
227
+ return true;
228
+ } catch (e) {
229
+ console.log(`[NIGHTLY-REFLECT] Warning: failed to write capsule ${filePath}: ${e.message}`);
230
+ return false;
231
+ }
232
+ }
233
+
234
+ function appendCapsuleUpdate(filePath, markdownContent, today) {
235
+ // Idempotency: skip if same-day update already appended
236
+ try {
237
+ const existing = fs.readFileSync(filePath, 'utf8');
238
+ if (existing.includes(`## 🔄 增量研判 (${today})`)) return false;
239
+ } catch { /* file may not be readable, proceed */ }
240
+ try {
241
+ const section = `\n\n## 🔄 增量研判 (${today})\n\n${markdownContent}\n`;
242
+ fs.appendFileSync(filePath, section, 'utf8');
243
+ return true;
244
+ } catch (e) {
245
+ console.log(`[NIGHTLY-REFLECT] Warning: failed to append capsule update ${filePath}: ${e.message}`);
246
+ return false;
247
+ }
241
248
  }
242
249
 
243
250
  /**
@@ -272,7 +279,10 @@ async function run() {
272
279
  db.exec('PRAGMA busy_timeout = 5000');
273
280
 
274
281
  const hotFacts = queryHotFacts(db);
275
- console.log(`[NIGHTLY-REFLECT] Found ${hotFacts.length} hot-zone facts.`);
282
+ // Recent facts (last 1 day) used exclusively for incremental capsule appends,
283
+ // preventing the 7-day rolling window from re-distilling the same facts repeatedly.
284
+ const recentFacts = queryHotFacts(db, 1);
285
+ console.log(`[NIGHTLY-REFLECT] Found ${hotFacts.length} hot-zone facts (${recentFacts.length} from last 24h).`);
276
286
 
277
287
  if (hotFacts.length < 3) {
278
288
  console.log('[NIGHTLY-REFLECT] Insufficient hot facts (< 3), skipping distillation.');
@@ -332,6 +342,7 @@ Rules:
332
342
  } catch (e) {
333
343
  console.log(`[NIGHTLY-REFLECT] Haiku call failed: ${e.message}`);
334
344
  writeReflectLog({ status: 'error', reason: 'haiku_failed', error: e.message, facts_found: hotFacts.length });
345
+ releaseLock();
335
346
  return;
336
347
  }
337
348
 
@@ -340,6 +351,7 @@ Rules:
340
351
  if (!parsed || typeof parsed !== 'object') {
341
352
  console.log('[NIGHTLY-REFLECT] Failed to parse Haiku output.');
342
353
  writeReflectLog({ status: 'error', reason: 'parse_failed', facts_found: hotFacts.length });
354
+ releaseLock();
343
355
  return;
344
356
  }
345
357
 
@@ -377,50 +389,104 @@ Rules:
377
389
  }
378
390
 
379
391
  // 3C: knowledge capsule aggregation by entity prefix.
392
+ // Cold start uses full hotFacts (7 days); incremental uses recentFacts (1 day)
393
+ // to prevent the same facts from being re-distilled every night.
380
394
  const capsuleGroups = collectCapsuleGroups(hotFacts, 3).slice(0, 3);
381
395
  for (const group of capsuleGroups) {
382
- const groupFacts = group.items.map(f => ({
396
+ const capsuleSlug = sanitizeSlug(group.prefix.replace(/\./g, '-'), 'capsule');
397
+ const capsuleFile = path.join(CAPSULES_DIR, `${capsuleSlug}-playbook.md`);
398
+ const playbookExists = fs.existsSync(capsuleFile);
399
+
400
+ // For incremental appends, only use facts from the last 24 hours
401
+ const factsForGroup = playbookExists
402
+ ? collectCapsuleGroups(recentFacts, 1).find(g => g.prefix === group.prefix)
403
+ : group;
404
+ // Skip append if no new facts in the last 24 hours
405
+ if (playbookExists && !factsForGroup) continue;
406
+
407
+ const sourceItems = factsForGroup ? factsForGroup.items : group.items;
408
+ const groupFacts = sourceItems.map(f => ({
383
409
  entity: f.entity,
384
410
  relation: f.relation,
385
411
  value: f.value,
386
412
  search_count: f.search_count,
387
413
  }));
388
- const capsulePrompt = `你是知识胶囊生成器。请将同一主题下的事实聚合成结构化胶囊。
414
+ const capsulePrompt = playbookExists
415
+ ? `你是知识胶囊维护者。以下是该主题近期新增的原始事实,请提炼成简洁的增量段落(不超过300字),直接追加到现有手册。不要重复旧内容,不要输出大标题。
389
416
 
390
417
  entity_prefix: ${group.prefix}
391
- facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0, 5000)}
418
+ 新增 facts(json): ${JSON.stringify(groupFacts, null, 2).slice(0, 3000)}
392
419
 
393
- 输出 JSON:
394
- {
395
- "title":"标题",
396
- "core_conclusion":"一句核心结论",
397
- "applicable_scenarios":"适用场景(1-2句)",
398
- "related_concepts":["概念1","概念2"],
399
- "supporting_facts":["支撑点1","支撑点2"]
400
- }
420
+ 输出格式(仅段落内容,Markdown 列表):
421
+ - **[具体要点/报错/红线]**:[原因与解法,含变量名/路径/报错原文]`
422
+ : `你是首席布道师与底层架构师。请将以下零散的开发者流水账,升维提炼成一本《硬核架构与避坑手册》(Playbook)。
401
423
 
402
- 规则:
403
- - 只基于输入事实,不虚构
404
- - 每个字段简洁具体
405
- - 仅输出 JSON`;
424
+ entity_prefix: ${group.prefix}
425
+ 输入事实(JSON): ${JSON.stringify(groupFacts, null, 2).slice(0, 5000)}
406
426
 
407
- let capsule = null;
427
+ 【输出信仰与戒律】
428
+ 1. 绝对的业务原子性:必须写具体的变量名、报错信息、文件路径,不要写"遇到了问题解决问题"。
429
+ 2. 倒金字塔结构:致命错误、架构红线必须写在最前面的 🩸 血泪避坑指南 中。
430
+ 3. 如果输入事实中包含报错原文(如 Exception 栈),必须保留在 🔍 历史报错指纹 中。
431
+ 4. 必须使用 Markdown 格式输出,不要有任何 JSON 包装。
432
+ 5. 去除一切废话:不要用"总而言之"、"这体现了"等水文词汇,全本必须干货。
433
+
434
+ 请按以下 Markdown 模板输出:
435
+ # 📕 Playbook: [你提炼的主题名称]
436
+
437
+ > **胶囊摘要**:[一句话核心]
438
+ > **覆盖实体**:${group.prefix}
439
+ > **核心标签**:[tag1, tag2, tag3]
440
+
441
+ ---
442
+
443
+ ## 1. 🩸 血泪避坑指南 (Critical Red Lines)
444
+ - **[问题名]**:[具体诱因与解法]
445
+
446
+ ## 2. 🏗️ 架构决议 (Architecture Decisions)
447
+ - **[为什么选X不选Y]**:[理由]
448
+
449
+ ## 3. 🔍 历史报错指纹 (Error Fingerprints)
450
+ - \`[报错原文]\`
451
+ - **诱因**:...
452
+ - **解法**:...
453
+
454
+ ## 4. 🔗 图谱扩展 (Related Concepts)
455
+ - [相关主题或文件引用]`;
456
+
457
+ let capsuleMarkdown = null;
408
458
  try {
409
459
  const rawCapsule = await Promise.race([
410
460
  callHaiku(capsulePrompt, distillEnv, 60000),
411
461
  new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
412
462
  ]);
413
- capsule = parseJsonFromLlm(rawCapsule);
463
+ capsuleMarkdown = typeof rawCapsule === 'string' ? rawCapsule.trim() : null;
414
464
  } catch { /* non-fatal */ }
415
- if (!capsule || !capsule.title || !capsule.core_conclusion || !capsule.applicable_scenarios) continue;
465
+ // Strip markdown code fences that Haiku may wrap around output
466
+ if (capsuleMarkdown) {
467
+ capsuleMarkdown = capsuleMarkdown
468
+ .replace(/^```(markdown)?\s*/i, '')
469
+ .replace(/```\s*$/i, '')
470
+ .trim();
471
+ }
472
+ if (!capsuleMarkdown || capsuleMarkdown.length < 50) continue;
416
473
 
417
- const capsuleSlug = sanitizeSlug(group.prefix.replace(/\./g, '-'), 'capsule');
418
- const capsuleFile = path.join(CAPSULES_DIR, `${capsuleSlug}-${today}.md`);
419
- writeCapsuleFile(capsuleFile, capsule, group.items, today, group.prefix);
474
+ if (playbookExists) {
475
+ appendCapsuleUpdate(capsuleFile, capsuleMarkdown, today);
476
+ } else {
477
+ writeCapsuleFile(capsuleFile, capsuleMarkdown, sourceItems, today, group.prefix);
478
+ }
420
479
  capsulesWritten++;
421
480
 
422
481
  if (memory && typeof memory.saveFacts === 'function') {
423
- const capsuleValue = stripMd(`${capsule.title}: ${capsule.core_conclusion}`).slice(0, 280);
482
+ const titleMatch = capsuleMarkdown.match(/^# 📕 Playbook:\s*(.+)$/m)
483
+ || capsuleMarkdown.match(/^## (.+)$/m);
484
+ const capsuleTitle = (titleMatch ? titleMatch[1].trim() : group.prefix).slice(0, 80);
485
+ const summaryMatch = capsuleMarkdown.match(/\*\*胶囊摘要\*\*[::]\s*(.+)/);
486
+ const capsuleSummary = summaryMatch
487
+ ? summaryMatch[1].trim()
488
+ : stripMd(capsuleMarkdown.slice(0, 120));
489
+ const capsuleValue = `${capsuleTitle}: ${capsuleSummary}`.slice(0, 280);
424
490
  if (capsuleValue.length >= 20) {
425
491
  const saveCapsule = memory.saveFacts(`capsule-${today}-${capsuleSlug}`, '*', [{
426
492
  entity: `capsule.${group.prefix.replace(/\./g, '_')}`,
package/scripts/memory.js CHANGED
@@ -99,8 +99,8 @@ function getDb() {
99
99
  }
100
100
 
101
101
  // Backward-compatible migration for old DBs without `scope`
102
- try { _db.exec('ALTER TABLE sessions ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
103
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope)'); } catch {}
102
+ try { _db.exec('ALTER TABLE sessions ADD COLUMN scope TEXT DEFAULT NULL'); } catch { }
103
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_sessions_scope ON sessions(scope)'); } catch { }
104
104
 
105
105
 
106
106
  // ── Facts table: atomic knowledge triples ──
@@ -167,27 +167,27 @@ function getDb() {
167
167
  }
168
168
 
169
169
  // Indexes
170
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch {}
171
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch {}
172
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch {}
173
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch {}
174
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch {}
170
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity ON facts(entity)'); } catch { }
171
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_entity_relation ON facts(entity, relation)'); } catch { }
172
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_project ON facts(project)'); } catch { }
173
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_label ON fact_labels(label)'); } catch { }
174
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_fact_labels_domain ON fact_labels(domain)'); } catch { }
175
175
 
176
176
  // Backward-compatible migration for old DBs without `scope`
177
- try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch {}
178
- try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch {}
177
+ try { _db.exec('ALTER TABLE facts ADD COLUMN scope TEXT DEFAULT NULL'); } catch { }
178
+ try { _db.exec('CREATE INDEX IF NOT EXISTS idx_facts_scope ON facts(scope)'); } catch { }
179
179
 
180
180
  // Search frequency tracking: counts how many times a fact appeared in search results.
181
181
  // This is a RELEVANCE PROXY, not a usefulness score — "searched" ≠ "actually helpful".
182
182
  // Renamed from recall_count (was ambiguous). Migration copies existing data forward.
183
- try { _db.exec('ALTER TABLE facts ADD COLUMN recall_count INTEGER DEFAULT 0'); } catch {}
184
- try { _db.exec('ALTER TABLE facts ADD COLUMN search_count INTEGER DEFAULT 0'); } catch {}
185
- try { _db.exec('ALTER TABLE facts ADD COLUMN last_searched_at TEXT'); } catch {}
183
+ try { _db.exec('ALTER TABLE facts ADD COLUMN recall_count INTEGER DEFAULT 0'); } catch { }
184
+ try { _db.exec('ALTER TABLE facts ADD COLUMN search_count INTEGER DEFAULT 0'); } catch { }
185
+ try { _db.exec('ALTER TABLE facts ADD COLUMN last_searched_at TEXT'); } catch { }
186
186
  // One-time migration: copy recall_count → search_count for existing rows
187
- try { _db.exec('UPDATE facts SET search_count = recall_count WHERE recall_count > 0 AND search_count = 0'); } catch {}
187
+ try { _db.exec('UPDATE facts SET search_count = recall_count WHERE recall_count > 0 AND search_count = 0'); } catch { }
188
188
 
189
189
  // conflict_status: 'OK' (default) | 'CONFLICT' — set by _detectConflict for non-stateful relations
190
- try { _db.exec("ALTER TABLE facts ADD COLUMN conflict_status TEXT NOT NULL DEFAULT 'OK'"); } catch {}
190
+ try { _db.exec("ALTER TABLE facts ADD COLUMN conflict_status TEXT NOT NULL DEFAULT 'OK'"); } catch { }
191
191
 
192
192
  return _db;
193
193
  }
@@ -317,8 +317,10 @@ function saveFacts(sessionId, project, facts, { scope = null } = {}) {
317
317
  insert.run(id, f.entity, f.relation, f.value.slice(0, 300),
318
318
  f.confidence || 'medium', sourceType, sessionId, normalizedProject, normalizedScope, tags);
319
319
  batchDedup.add(dedupKey);
320
- savedFacts.push({ id, entity: f.entity, relation: f.relation, value: f.value,
321
- project: normalizedProject, scope: normalizedScope, tags: f.tags || [], created_at: new Date().toISOString() });
320
+ savedFacts.push({
321
+ id, entity: f.entity, relation: f.relation, value: f.value,
322
+ project: normalizedProject, scope: normalizedScope, tags: f.tags || [], created_at: new Date().toISOString()
323
+ });
322
324
  saved++;
323
325
 
324
326
  // For stateful relations, mark older active facts with same entity::relation as superseded
@@ -440,7 +442,7 @@ function _trackSearch(ids) {
440
442
  }
441
443
 
442
444
  const SUPERSEDE_LOG = path.join(os.homedir(), '.metame', 'memory_supersede_log.jsonl');
443
- const CONFLICT_LOG = path.join(os.homedir(), '.metame', 'memory_conflict_log.jsonl');
445
+ const CONFLICT_LOG = path.join(os.homedir(), '.metame', 'memory_conflict_log.jsonl');
444
446
 
445
447
  /**
446
448
  * Append supersede operations to audit log (append-only, never mutated).
@@ -767,6 +769,42 @@ function searchFacts(query, { limit = 5, project = null, scope = null } = {}) {
767
769
  }
768
770
  } catch { /* fact_labels table may not exist yet */ }
769
771
  }
772
+ // Entity path expansion: supplement with entity hierarchy matches per query term.
773
+ // Handles multi-word queries like "daemon dispatch" that won't match dotted entity paths
774
+ // (e.g. "MetaMe.daemon.dispatch.cross-bot") via LIKE '%daemon dispatch%'.
775
+ if (likeResults.length < limit) {
776
+ const terms = query.trim().split(/\s+/).filter(t => t.length >= 3);
777
+ if (terms.length > 0) {
778
+ try {
779
+ const likeIds = new Set(likeResults.map(r => r.id));
780
+ const entityOrClauses = terms.map(() => `entity LIKE ?`).join(' OR ');
781
+ const entityParams = terms.map(t => `%${t}%`);
782
+ let entityExpSql = `SELECT id, entity, relation, value, confidence, project, scope, tags, created_at
783
+ FROM facts WHERE (${entityOrClauses})
784
+ AND superseded_by IS NULL
785
+ AND (conflict_status IS NULL OR conflict_status NOT IN ('ARCHIVED', 'CONFLICT'))`;
786
+ if (scope && project) {
787
+ entityExpSql += ` AND ((scope = ? OR scope = '*') OR (scope IS NULL AND (project = ? OR project = '*')))`;
788
+ entityParams.push(scope, project);
789
+ } else if (scope) {
790
+ entityExpSql += ` AND (scope = ? OR scope = '*')`;
791
+ entityParams.push(scope);
792
+ } else if (project) {
793
+ entityExpSql += ` AND (project = ? OR project = '*')`;
794
+ entityParams.push(project);
795
+ }
796
+ entityExpSql += ` ORDER BY created_at DESC LIMIT ?`;
797
+ entityParams.push(limit);
798
+ const entityRows = db.prepare(entityExpSql).all(...entityParams);
799
+ for (const row of entityRows) {
800
+ if (!likeIds.has(row.id) && likeResults.length < limit) {
801
+ likeIds.add(row.id);
802
+ likeResults.push(row);
803
+ }
804
+ }
805
+ } catch { /* entity expansion failed */ }
806
+ }
807
+ }
770
808
  if (likeResults.length > 0) _trackSearch(likeResults.map(r => r.id));
771
809
  return likeResults;
772
810
  }
@@ -54,6 +54,11 @@ function saveRuntime(runtime) {
54
54
  fs.renameSync(tmp, file);
55
55
  }
56
56
 
57
+ function clearRuntime() {
58
+ saveRuntime(defaultRuntime());
59
+ return defaultRuntime();
60
+ }
61
+
57
62
  function normalizeText(input) {
58
63
  return String(input || '').trim();
59
64
  }
@@ -394,6 +399,7 @@ module.exports = {
394
399
  gcExpiredDebts,
395
400
  detectPatterns,
396
401
  getRuntimeStatus,
402
+ clearRuntime,
397
403
  _private: {
398
404
  runtimeFilePath,
399
405
  loadRuntime,
package/scripts/schema.js CHANGED
@@ -103,6 +103,7 @@ const SCHEMA = {
103
103
 
104
104
  // === T5: Growth (metacognition, system-managed) ===
105
105
  'growth.patterns': { tier: 'T5', type: 'array', maxItems: 3 },
106
+ 'growth.self_reflection_patterns': { tier: 'T5', type: 'array', maxItems: 3 },
106
107
  'growth.zone_history': { tier: 'T5', type: 'array', maxItems: 10 },
107
108
  'growth.reflections_answered': { tier: 'T5', type: 'number' },
108
109
  'growth.reflections_skipped': { tier: 'T5', type: 'number' },
@@ -5,7 +5,8 @@
5
5
  *
6
6
  * Scans correction/metacognitive signals from the past 7 days,
7
7
  * aggregates "where did the AI get it wrong", and writes a brief
8
- * self-critique pattern into growth.patterns in ~/.claude_profile.yaml.
8
+ * self-critique pattern into growth.self_reflection_patterns
9
+ * in ~/.claude_profile.yaml.
9
10
  *
10
11
  * Also distills correction signals into lessons/ SOP markdown files.
11
12
  *
@@ -27,6 +28,81 @@ const LOCK_FILE = path.join(HOME, '.metame', 'self-reflect.lock');
27
28
  const LESSONS_DIR = path.join(HOME, '.metame', 'memory', 'lessons');
28
29
  const WINDOW_DAYS = 7;
29
30
 
31
+ function normalizeReflectionEntry(entry) {
32
+ if (!entry) return null;
33
+ if (typeof entry === 'string') {
34
+ const summary = entry.trim();
35
+ if (!summary) return null;
36
+ return {
37
+ summary,
38
+ detected: new Date().toISOString().slice(0, 10),
39
+ };
40
+ }
41
+ if (typeof entry === 'object' && typeof entry.summary === 'string' && entry.summary.trim()) {
42
+ return {
43
+ summary: entry.summary.trim(),
44
+ detected: entry.detected || new Date().toISOString().slice(0, 10),
45
+ };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ function mergeReflectionEntries(entries) {
51
+ const merged = new Map();
52
+ for (const entry of entries) {
53
+ const normalized = normalizeReflectionEntry(entry);
54
+ if (!normalized) continue;
55
+ const existing = merged.get(normalized.summary);
56
+ if (!existing) {
57
+ merged.set(normalized.summary, normalized);
58
+ continue;
59
+ }
60
+
61
+ const existingDetected = existing.detected ? new Date(existing.detected).getTime() : 0;
62
+ const normalizedDetected = normalized.detected ? new Date(normalized.detected).getTime() : 0;
63
+ const shouldReplace =
64
+ normalizedDetected > 0 && (
65
+ existingDetected === 0
66
+ || normalizedDetected < existingDetected
67
+ );
68
+ if (shouldReplace) merged.set(normalized.summary, { ...existing, ...normalized });
69
+ }
70
+ return [...merged.values()];
71
+ }
72
+
73
+ function normalizeSelfReflectionPatterns(profile) {
74
+ if (!profile.growth) profile.growth = {};
75
+
76
+ const current = Array.isArray(profile.growth.self_reflection_patterns)
77
+ ? profile.growth.self_reflection_patterns
78
+ : [];
79
+ const legacy = Array.isArray(profile.growth.patterns)
80
+ ? profile.growth.patterns.filter(p => typeof p === 'string')
81
+ : [];
82
+
83
+ const normalized = mergeReflectionEntries([...current, ...legacy])
84
+ .slice(-3);
85
+
86
+ profile.growth.self_reflection_patterns = normalized;
87
+ if (Array.isArray(profile.growth.patterns)) {
88
+ profile.growth.patterns = profile.growth.patterns.filter(p => typeof p === 'object' && p && typeof p.summary === 'string');
89
+ }
90
+
91
+ return normalized;
92
+ }
93
+
94
+ function migrateLegacySelfReflectionPatterns(profile) {
95
+ const beforePatterns = JSON.stringify((profile.growth && profile.growth.patterns) || null);
96
+ const beforeReflections = JSON.stringify((profile.growth && profile.growth.self_reflection_patterns) || null);
97
+ const normalized = normalizeSelfReflectionPatterns(profile);
98
+ const afterPatterns = JSON.stringify((profile.growth && profile.growth.patterns) || null);
99
+ const afterReflections = JSON.stringify((profile.growth && profile.growth.self_reflection_patterns) || null);
100
+ return {
101
+ changed: beforePatterns !== afterPatterns || beforeReflections !== afterReflections,
102
+ normalized,
103
+ };
104
+ }
105
+
30
106
  /**
31
107
  * Distill correction signals into reusable SOP markdown files.
32
108
  * Each run produces at most one lesson file per unique slug.
@@ -148,6 +224,22 @@ async function run() {
148
224
  }
149
225
 
150
226
  try {
227
+ // Persist legacy string-pattern migration even when this run produces no new reflections.
228
+ if (fs.existsSync(BRAIN_FILE)) {
229
+ try {
230
+ const yaml = require('js-yaml');
231
+ const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
232
+ const migrated = migrateLegacySelfReflectionPatterns(profile);
233
+ if (migrated.changed) {
234
+ const dumped = yaml.dump(profile, { lineWidth: -1 });
235
+ await writeBrainFileSafe(dumped);
236
+ console.log('[self-reflect] Migrated legacy growth.patterns strings into growth.self_reflection_patterns.');
237
+ }
238
+ } catch (e) {
239
+ console.log(`[self-reflect] Legacy pattern migration failed (non-fatal): ${e.message}`);
240
+ }
241
+ }
242
+
151
243
  // Read signals from last WINDOW_DAYS days
152
244
  if (!fs.existsSync(SIGNAL_FILE)) {
153
245
  console.log('[self-reflect] No signal file, skipping.');
@@ -175,9 +267,9 @@ async function run() {
175
267
  try {
176
268
  const yaml = require('js-yaml');
177
269
  const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
178
- const existing = (profile.growth && profile.growth.patterns) || [];
270
+ const existing = migrateLegacySelfReflectionPatterns(profile).normalized;
179
271
  if (existing.length > 0) {
180
- currentPatterns = `Current growth.patterns (avoid repeating):\n${existing.map(p => `- ${p}`).join('\n')}\n\n`;
272
+ currentPatterns = `Current growth.self_reflection_patterns (avoid repeating):\n${existing.map(p => `- ${p.summary}`).join('\n')}\n\n`;
181
273
  }
182
274
  } catch { /* non-fatal */ }
183
275
 
@@ -247,24 +339,24 @@ ${signalText}
247
339
  return;
248
340
  }
249
341
 
250
- // Merge into growth.patterns (cap at 3, keep newest)
342
+ // Merge into growth.self_reflection_patterns (cap at 3, keep newest)
251
343
  try {
252
344
  const yaml = require('js-yaml');
253
345
  const raw = fs.readFileSync(BRAIN_FILE, 'utf8');
254
346
  const profile = yaml.load(raw) || {};
255
347
  if (!profile.growth) profile.growth = {};
256
- const existing = Array.isArray(profile.growth.patterns) ? profile.growth.patterns : [];
257
- // Add new patterns, deduplicate, cap at 3 newest
258
- const merged = [...existing, ...patterns]
259
- .filter((p, i, arr) => arr.indexOf(p) === i)
348
+ const existing = migrateLegacySelfReflectionPatterns(profile).normalized;
349
+ const merged = mergeReflectionEntries([
350
+ ...existing,
351
+ ...patterns.map(summary => ({ summary, detected: new Date().toISOString().slice(0, 10) })),
352
+ ])
260
353
  .slice(-3);
261
- profile.growth.patterns = merged;
354
+ profile.growth.self_reflection_patterns = merged;
262
355
  profile.growth.last_reflection = new Date().toISOString().slice(0, 10);
263
356
 
264
- // Preserve locked lines (simple approach: only update growth section)
265
357
  const dumped = yaml.dump(profile, { lineWidth: -1 });
266
358
  await writeBrainFileSafe(dumped);
267
- console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.patterns: ${patterns.join(' | ')}`);
359
+ console.log(`[self-reflect] ${patterns.length} pattern(s) written to growth.self_reflection_patterns: ${patterns.join(' | ')}`);
268
360
  } catch (e) {
269
361
  console.log(`[self-reflect] Failed to write profile: ${e.message}`);
270
362
  }
@@ -283,4 +375,10 @@ if (require.main === module) {
283
375
  });
284
376
  }
285
377
 
286
- module.exports = { run };
378
+ module.exports = {
379
+ run,
380
+ mergeReflectionEntries,
381
+ normalizeReflectionEntry,
382
+ normalizeSelfReflectionPatterns,
383
+ migrateLegacySelfReflectionPatterns,
384
+ };