metame-cli 1.4.33 → 1.5.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 (44) hide show
  1. package/README.md +187 -48
  2. package/index.js +148 -9
  3. package/package.json +6 -3
  4. package/scripts/daemon-admin-commands.js +254 -9
  5. package/scripts/daemon-agent-commands.js +64 -6
  6. package/scripts/daemon-agent-tools.js +26 -5
  7. package/scripts/daemon-bridges.js +110 -20
  8. package/scripts/daemon-claude-engine.js +704 -268
  9. package/scripts/daemon-command-router.js +24 -8
  10. package/scripts/daemon-default.yaml +28 -4
  11. package/scripts/daemon-engine-runtime.js +275 -0
  12. package/scripts/daemon-exec-commands.js +10 -4
  13. package/scripts/daemon-notify.js +37 -1
  14. package/scripts/daemon-runtime-lifecycle.js +2 -1
  15. package/scripts/daemon-session-commands.js +52 -4
  16. package/scripts/daemon-session-store.js +2 -1
  17. package/scripts/daemon-task-scheduler.js +87 -28
  18. package/scripts/daemon-user-acl.js +26 -9
  19. package/scripts/daemon.js +81 -17
  20. package/scripts/distill.js +323 -18
  21. package/scripts/docs/agent-guide.md +12 -0
  22. package/scripts/docs/maintenance-manual.md +119 -0
  23. package/scripts/docs/pointer-map.md +88 -0
  24. package/scripts/feishu-adapter.js +6 -1
  25. package/scripts/hooks/stop-session-capture.js +243 -0
  26. package/scripts/memory-extract.js +100 -5
  27. package/scripts/memory-nightly-reflect.js +196 -11
  28. package/scripts/memory.js +134 -3
  29. package/scripts/mentor-engine.js +405 -0
  30. package/scripts/platform.js +2 -0
  31. package/scripts/providers.js +169 -21
  32. package/scripts/schema.js +12 -0
  33. package/scripts/session-analytics.js +245 -12
  34. package/scripts/skill-changelog.js +245 -0
  35. package/scripts/skill-evolution.js +288 -5
  36. package/scripts/usage-classifier.js +1 -1
  37. package/scripts/daemon-admin-commands.test.js +0 -333
  38. package/scripts/daemon-task-envelope.test.js +0 -59
  39. package/scripts/daemon-task-scheduler.test.js +0 -106
  40. package/scripts/reliability-core.test.js +0 -280
  41. package/scripts/skill-evolution.test.js +0 -113
  42. package/scripts/task-board.test.js +0 -83
  43. package/scripts/test_daemon.js +0 -1407
  44. package/scripts/utils.test.js +0 -192
@@ -3,7 +3,7 @@
3
3
  /**
4
4
  * MetaMe Passive Distiller
5
5
  *
6
- * Reads raw signal buffer, calls Claude (haiku, non-interactive)
6
+ * Reads raw signal buffer, calls Claude (configured distill model, non-interactive)
7
7
  * to extract persistent preferences/identity, merges into profile.
8
8
  *
9
9
  * Runs automatically before each MetaMe session launch.
@@ -15,11 +15,14 @@ const os = require('os');
15
15
  const { callHaiku, buildDistillEnv } = require('./providers');
16
16
 
17
17
  const HOME = os.homedir();
18
+ const METAME_DIR = path.join(HOME, '.metame');
18
19
  const BUFFER_FILE = path.join(HOME, '.metame', 'raw_signals.jsonl');
19
20
  const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
20
21
  const LOCK_FILE = path.join(HOME, '.metame', 'distill.lock');
22
+ const MEMORY_DIR = path.join(METAME_DIR, 'memory');
23
+ const POSTMORTEM_DIR = path.join(MEMORY_DIR, 'postmortems');
21
24
 
22
- const { hasKey, isLocked, getTier, getWritableKeysForPrompt, estimateTokens, TOKEN_BUDGET } = require('./schema');
25
+ const { hasKey, isLocked, getTier, getDefinition, getWritableKeysForPrompt, estimateTokens, TOKEN_BUDGET } = require('./schema');
23
26
  const { loadPending, savePending, upsertPending, getPromotable, removePromoted, expireStale } = require('./pending-traits');
24
27
  const { writeBrainFileSafe, normalizeProjectPath, deriveProjectInfo } = require('./utils');
25
28
 
@@ -37,6 +40,248 @@ try {
37
40
  distillEnv = buildDistillEnv();
38
41
  } catch { /* providers not configured — use defaults */ }
39
42
 
43
+ const COMPETENCE_SCORE = {
44
+ beginner: 1,
45
+ intermediate: 2,
46
+ expert: 3,
47
+ };
48
+
49
+ function normalizeCompetenceLevel(level) {
50
+ const v = String(level || '').trim().toLowerCase();
51
+ if (v === 'beginner' || v === 'intermediate' || v === 'expert') return v;
52
+ return null;
53
+ }
54
+
55
+ function normalizeDomainName(domain) {
56
+ const raw = String(domain || '').trim();
57
+ if (!raw) return '';
58
+ return raw.replace(/\s+/g, ' ').slice(0, 40);
59
+ }
60
+
61
+ function mergeCompetenceMap(currentMap, competenceSignals) {
62
+ const next = {
63
+ ...(currentMap && typeof currentMap === 'object' && !Array.isArray(currentMap) ? currentMap : {}),
64
+ };
65
+ const signals = Array.isArray(competenceSignals) ? competenceSignals : [];
66
+ let changed = false;
67
+
68
+ for (const signal of signals) {
69
+ const domain = normalizeDomainName(signal && signal.domain);
70
+ const level = normalizeCompetenceLevel(signal && signal.level);
71
+ if (!domain || !level) continue;
72
+
73
+ const oldLevel = normalizeCompetenceLevel(next[domain]);
74
+ if (!oldLevel) {
75
+ next[domain] = level;
76
+ changed = true;
77
+ continue;
78
+ }
79
+
80
+ const oldScore = COMPETENCE_SCORE[oldLevel] || 0;
81
+ const newScore = COMPETENCE_SCORE[level] || 0;
82
+ if (newScore >= oldScore) {
83
+ if (newScore > oldScore) {
84
+ next[domain] = level;
85
+ changed = true;
86
+ }
87
+ continue;
88
+ }
89
+
90
+ // Downgrade is only allowed with explicit downgrade evidence.
91
+ const downgradeEvidence = String(signal && signal.downgrade_evidence ? signal.downgrade_evidence : '').trim();
92
+ if (downgradeEvidence) {
93
+ next[domain] = level;
94
+ changed = true;
95
+ }
96
+ }
97
+
98
+ const entries = Object.entries(next);
99
+ if (entries.length <= 20) return { map: next, changed };
100
+
101
+ // Keep top-20 by level score to avoid unbounded growth.
102
+ entries.sort((a, b) => (COMPETENCE_SCORE[b[1]] || 0) - (COMPETENCE_SCORE[a[1]] || 0));
103
+ const compact = Object.fromEntries(entries.slice(0, 20));
104
+ return { map: compact, changed: true };
105
+ }
106
+
107
+ function sanitizeSlug(input, fallback = 'session') {
108
+ const v = String(input || '')
109
+ .trim()
110
+ .toLowerCase()
111
+ .replace(/[^a-z0-9\u4e00-\u9fff_-]+/g, '-')
112
+ .replace(/-+/g, '-')
113
+ .replace(/^-|-$/g, '');
114
+ if (!v) return fallback;
115
+ return v.slice(0, 48);
116
+ }
117
+
118
+ function stripMd(text) {
119
+ return String(text || '').replace(/[#*_`>\[\]\(\)]/g, ' ').replace(/\s+/g, ' ').trim();
120
+ }
121
+
122
+ function appendPostmortemSkillSignal(skillName, skeleton, reasons, filePath) {
123
+ let skillEvolution = null;
124
+ try { skillEvolution = require('./skill-evolution'); } catch { /* optional */ }
125
+ if (!skillEvolution || typeof skillEvolution.appendSkillSignal !== 'function') return;
126
+ const name = String(skillName || '').trim();
127
+ if (!name) return;
128
+ try {
129
+ skillEvolution.appendSkillSignal({
130
+ ts: new Date().toISOString(),
131
+ type: 'postmortem_lesson',
132
+ prompt: skeleton && skeleton.intent ? skeleton.intent : '',
133
+ output: `postmortem:${name}`,
134
+ error: null,
135
+ skills_invoked: [name],
136
+ has_tool_failure: Number(skeleton && skeleton.tool_error_count) > 0,
137
+ tool_usage: [],
138
+ files_modified: [],
139
+ cwd: skeleton && skeleton.project_path ? skeleton.project_path : '',
140
+ metadata: {
141
+ source: 'distill-postmortem',
142
+ reasons,
143
+ postmortem_file: filePath,
144
+ },
145
+ });
146
+ } catch { /* non-fatal */ }
147
+ }
148
+
149
+ async function maybeGeneratePostmortemArtifact(skeleton, sessionPath) {
150
+ if (!sessionAnalytics || !skeleton || !sessionPath) return null;
151
+ const detector = sessionAnalytics.detectSignificantSession;
152
+ if (typeof detector !== 'function') return null;
153
+
154
+ const sig = detector(skeleton);
155
+ if (!sig || !sig.significant) return null;
156
+
157
+ let evidence = null;
158
+ try {
159
+ if (typeof sessionAnalytics.extractEvidence === 'function') {
160
+ evidence = sessionAnalytics.extractEvidence(sessionPath, 3200);
161
+ }
162
+ } catch { /* non-fatal */ }
163
+
164
+ const promptInput = JSON.stringify({
165
+ skeleton: {
166
+ session_id: skeleton.session_id,
167
+ duration_min: skeleton.duration_min,
168
+ tool_error_count: skeleton.tool_error_count,
169
+ retry_sequences: skeleton.retry_sequences,
170
+ git_diff_lines: skeleton.git_diff_lines,
171
+ error_recovered: skeleton.error_recovered,
172
+ intent: skeleton.intent,
173
+ project: skeleton.project,
174
+ project_id: skeleton.project_id,
175
+ },
176
+ reasons: sig.reasons,
177
+ evidence,
178
+ }, null, 2).slice(0, 5200);
179
+
180
+ const pmPrompt = `你是工程复盘助手。根据会话骨架和证据生成一份简洁 postmortem。
181
+
182
+ 输入(JSON):
183
+ ${promptInput}
184
+
185
+ 输出 JSON:
186
+ {
187
+ "title":"简短标题",
188
+ "problem":"遇到的问题(1句)",
189
+ "root_cause":"根因(1句)",
190
+ "fix":"修复方案(1句)",
191
+ "lesson":"可复用教训(1句)",
192
+ "skill":"可选,若明显涉及某技能则写 skill 名,否则空字符串"
193
+ }
194
+
195
+ 规则:
196
+ - 只基于输入,不要虚构
197
+ - 每个字段 20-180 字
198
+ - 只输出 JSON`;
199
+
200
+ let raw = '';
201
+ try {
202
+ raw = await callHaiku(pmPrompt, distillEnv, 60000);
203
+ } catch {
204
+ return null;
205
+ }
206
+
207
+ let parsed = null;
208
+ try {
209
+ const cleaned = String(raw).replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
210
+ parsed = JSON.parse(cleaned);
211
+ } catch {
212
+ return null;
213
+ }
214
+
215
+ const title = String(parsed && parsed.title ? parsed.title : '').trim();
216
+ const problem = String(parsed && parsed.problem ? parsed.problem : '').trim();
217
+ const rootCause = String(parsed && parsed.root_cause ? parsed.root_cause : '').trim();
218
+ const fix = String(parsed && parsed.fix ? parsed.fix : '').trim();
219
+ const lesson = String(parsed && parsed.lesson ? parsed.lesson : '').trim();
220
+ if (!title || !problem || !rootCause || !fix || !lesson) return null;
221
+
222
+ fs.mkdirSync(POSTMORTEM_DIR, { recursive: true });
223
+ const day = new Date().toISOString().slice(0, 10);
224
+ const topicSlug = sanitizeSlug(skeleton.intent || title, `session-${String(skeleton.session_id || '').slice(0, 8)}`);
225
+ const filePath = path.join(POSTMORTEM_DIR, `${day}-${topicSlug}.md`);
226
+ const markdown = [
227
+ `# ${title}`,
228
+ '',
229
+ `- date: ${day}`,
230
+ `- session_id: ${skeleton.session_id || 'unknown'}`,
231
+ `- project: ${skeleton.project || 'unknown'}`,
232
+ `- project_id: ${skeleton.project_id || 'unknown'}`,
233
+ `- reasons: ${(sig.reasons || []).join(', ') || 'n/a'}`,
234
+ '',
235
+ '## 问题',
236
+ problem,
237
+ '',
238
+ '## 根因',
239
+ rootCause,
240
+ '',
241
+ '## 解法',
242
+ fix,
243
+ '',
244
+ '## 教训',
245
+ lesson,
246
+ '',
247
+ ].join('\n');
248
+ fs.writeFileSync(filePath, markdown, 'utf8');
249
+
250
+ let memory = null;
251
+ try {
252
+ memory = require('./memory');
253
+ } catch { /* optional */ }
254
+
255
+ if (memory && typeof memory.saveFacts === 'function') {
256
+ const value = stripMd(`问题:${problem} 根因:${rootCause} 解法:${fix} 教训:${lesson}`).slice(0, 280);
257
+ if (value.length >= 20) {
258
+ const entityProject = sanitizeSlug(skeleton.project || 'unknown', 'unknown');
259
+ const fallbackScope = skeleton.session_id
260
+ ? `sess_${String(skeleton.session_id).replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 24)}`
261
+ : null;
262
+ try {
263
+ memory.saveFacts(
264
+ skeleton.session_id || `pm-${Date.now()}`,
265
+ skeleton.project || 'unknown',
266
+ [{
267
+ entity: `postmortem.${entityProject}`,
268
+ relation: 'bug_lesson',
269
+ value,
270
+ confidence: 'high',
271
+ tags: ['postmortem'],
272
+ }],
273
+ { scope: skeleton.project_id || fallbackScope }
274
+ );
275
+ } catch { /* non-fatal */ }
276
+ }
277
+ try { memory.close(); } catch { /* non-fatal */ }
278
+ }
279
+
280
+ appendPostmortemSkillSignal(parsed && parsed.skill, skeleton, sig.reasons || [], filePath);
281
+
282
+ return { file: filePath, reasons: sig.reasons || [] };
283
+ }
284
+
40
285
  function selectSignalBatch(lines) {
41
286
  const parsed = [];
42
287
  for (const rawLine of lines) {
@@ -176,6 +421,7 @@ async function distill() {
176
421
  let sessionContext = '';
177
422
  let skeleton = null;
178
423
  let sessionSummary = null;
424
+ let targetSessionPath = null;
179
425
  if (sessionAnalytics) {
180
426
  try {
181
427
  let targetSession = null;
@@ -189,6 +435,7 @@ async function distill() {
189
435
  }
190
436
  if (targetSession) {
191
437
  skeleton = sessionAnalytics.extractSkeleton(targetSession.path);
438
+ targetSessionPath = targetSession.path;
192
439
  sessionContext = sessionAnalytics.formatForPrompt(skeleton);
193
440
  // For long sessions, extract pivot points
194
441
  sessionSummary = sessionAnalytics.summarizeSession(skeleton, targetSession.path);
@@ -364,11 +611,16 @@ BEHAVIORAL ANALYSIS — _behavior block (always output, use null if insufficient
364
611
  friction: [] # max 3 keywords describing pain points
365
612
  goal_alignment: aligned | partial | drifted | null
366
613
  drift_note: "max 30 char explanation" or null
614
+ competence_signals: # optional, max 5
615
+ - domain: "领域名"
616
+ level: beginner | intermediate | expert
617
+ evidence: "触发证据"
618
+ downgrade_evidence: "只有在明确降级时填写,否则省略"
367
619
  ${sessionContext ? '\nHint: high tool_calls + routine messages → zone likely higher. If DECLARED_GOALS exist, assess goal_alignment.' : ''}
368
620
  OUTPUT — respond with ONLY a YAML code block. If nothing worth saving AND no behavior: respond with exactly NO_UPDATE.
369
621
  Do NOT repeat existing unchanged values.`;
370
622
 
371
- // 6. Call Claude in print mode with haiku (+ provider env for relay support)
623
+ // 6. Call Claude in print mode with configured distill model (+ provider env for relay support)
372
624
  let result;
373
625
  try {
374
626
  result = await callHaiku(distillPrompt, distillEnv, 60000);
@@ -384,9 +636,23 @@ Do NOT repeat existing unchanged values.`;
384
636
 
385
637
  // 7. Parse result
386
638
  if (!result || result === 'NO_UPDATE') {
639
+ if (skeleton && sessionAnalytics) {
640
+ try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { /* non-fatal */ }
641
+ }
387
642
  ackSignals = true;
388
643
  finalize();
389
- return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — no persistent insights found.` };
644
+ let postmortem = null;
645
+ try {
646
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
647
+ } catch { /* non-fatal */ }
648
+ return {
649
+ updated: false,
650
+ behavior: null,
651
+ skeleton,
652
+ postmortem,
653
+ signalCount: signals.length,
654
+ summary: `Analyzed ${signals.length} messages — no persistent insights found.`,
655
+ };
390
656
  }
391
657
 
392
658
  // Extract YAML block from response — require explicit code block, no fallback
@@ -420,22 +686,13 @@ Do NOT repeat existing unchanged values.`;
420
686
 
421
687
  // Schema whitelist filter: drop any keys not in schema or locked
422
688
  const filtered = filterBySchema(updates);
423
- if (Object.keys(filtered).length === 0 && !behavior) {
689
+ const extractedFieldCount = Object.keys(filtered).length;
690
+ if (extractedFieldCount === 0 && !behavior) {
424
691
  ackSignals = true;
425
692
  finalize();
426
693
  return { updated: false, behavior: null, summary: `Analyzed ${signals.length} messages — all extracted fields rejected by schema.` };
427
694
  }
428
695
 
429
- // If only behavior detected but no profile updates
430
- if (Object.keys(filtered).length === 0 && behavior) {
431
- ackSignals = true;
432
- finalize();
433
- if (skeleton && sessionAnalytics) {
434
- try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { }
435
- }
436
- return { updated: false, behavior, skeleton, signalCount: signals.length, summary: `Analyzed ${signals.length} messages — behavior logged, no profile changes.` };
437
- }
438
-
439
696
  const profile = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};
440
697
 
441
698
  // Read raw content to find locked lines and comments
@@ -451,13 +708,44 @@ Do NOT repeat existing unchanged values.`;
451
708
  const merged = strategicMerge(profile, filtered, lockedKeys, pendingTraits, confidenceMap, sourceMap);
452
709
  savePending(pendingTraits);
453
710
 
711
+ // Merge competence signals into user_competence_map (upgrade by default).
712
+ const competenceSignals = Array.isArray(behavior && behavior.competence_signals)
713
+ ? behavior.competence_signals
714
+ : [];
715
+ const mergedCompetence = mergeCompetenceMap(merged.user_competence_map, competenceSignals);
716
+ if (mergedCompetence.changed) {
717
+ merged.user_competence_map = mergedCompetence.map;
718
+ }
719
+
720
+ if (extractedFieldCount === 0 && !mergedCompetence.changed) {
721
+ if (skeleton && sessionAnalytics) {
722
+ try { sessionAnalytics.markAnalyzed(skeleton.session_id); } catch { }
723
+ }
724
+ ackSignals = true;
725
+ finalize();
726
+ let postmortem = null;
727
+ try {
728
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
729
+ } catch { /* non-fatal */ }
730
+ return {
731
+ updated: false,
732
+ behavior,
733
+ skeleton,
734
+ postmortem,
735
+ signalCount: signals.length,
736
+ summary: `Analyzed ${signals.length} messages — behavior logged, no profile changes.`,
737
+ };
738
+ }
739
+
454
740
  // Add distillation log entry (keep last 10, compact format)
455
741
  if (!merged.evolution) merged.evolution = {};
456
742
  if (!merged.evolution.auto_distill) merged.evolution.auto_distill = [];
743
+ const changedFields = Object.keys(filtered);
744
+ if (mergedCompetence.changed) changedFields.push('user_competence_map');
457
745
  merged.evolution.auto_distill.push({
458
746
  ts: new Date().toISOString(),
459
747
  signals: signals.length,
460
- fields: Object.keys(filtered).join(', ')
748
+ fields: changedFields.join(', ')
461
749
  });
462
750
  // Cap at 10 entries
463
751
  if (merged.evolution.auto_distill.length > 10) {
@@ -507,13 +795,19 @@ Do NOT repeat existing unchanged values.`;
507
795
 
508
796
  ackSignals = true;
509
797
  finalize();
798
+ let postmortem = null;
799
+ try {
800
+ postmortem = await maybeGeneratePostmortemArtifact(skeleton, targetSessionPath);
801
+ } catch { /* non-fatal */ }
802
+ const absorbedCount = changedFields.length;
510
803
  return {
511
804
  updated: true,
512
805
  behavior,
513
806
  skeleton,
514
807
  sessionSummary,
808
+ postmortem,
515
809
  signalCount: signals.length,
516
- summary: `${Object.keys(filtered).length} new trait${Object.keys(filtered).length > 1 ? 's' : ''} absorbed. (${tokens} tokens)`
810
+ summary: `${absorbedCount} new trait${absorbedCount > 1 ? 's' : ''} absorbed. (${tokens} tokens)`
517
811
  };
518
812
 
519
813
  } catch (err) {
@@ -1077,7 +1371,18 @@ If no clear patterns found: respond with exactly NO_PATTERNS`;
1077
1371
  }
1078
1372
 
1079
1373
  // Export for use in index.js
1080
- module.exports = { distill, writeSessionLog, bootstrapSessionLog, detectPatterns };
1374
+ module.exports = {
1375
+ distill,
1376
+ writeSessionLog,
1377
+ bootstrapSessionLog,
1378
+ detectPatterns,
1379
+ _private: {
1380
+ mergeCompetenceMap,
1381
+ normalizeCompetenceLevel,
1382
+ normalizeDomainName,
1383
+ sanitizeSlug,
1384
+ },
1385
+ };
1081
1386
 
1082
1387
  // Also allow direct execution
1083
1388
  if (require.main === module) {
@@ -16,6 +16,15 @@
16
16
  在手机端(飞书/Telegram),直接说即可,daemon 会自动处理:
17
17
  > 创建一个 Agent,目录是 ~/projects/my-bot
18
18
 
19
+ 引擎选择(手机端自然语言):
20
+ - 默认不写引擎时,使用 Claude(配置里不落 `engine` 字段)
21
+ - 句子里带 `codex` 关键词时,自动写入 `engine: codex`
22
+
23
+ 示例:
24
+ > 创建一个 codex agent,目录是 ~/projects/reviewer
25
+
26
+ > 用 codex 建一个代码审查 agent,目录 ~/projects/pr-review
27
+
19
28
  在桌面 Claude Code 终端,需要手动操作:
20
29
  1. 创建项目目录和 CLAUDE.md(角色定义)
21
30
  2. 编辑 `~/.metame/daemon.yaml`,在 `projects` 下新增:
@@ -48,3 +57,6 @@
48
57
  ## 注意事项
49
58
  - 专属群(chat_agent_map 中的群)永远绑定同一个 Agent,不能通过昵称切换
50
59
  - 新群必须发 `/activate` 才能使用,未授权群会提示"此群未授权"
60
+ - Codex 当前限制(MVP):`/sessions` 列表暂只展示 Claude 本地会话,Codex 会话暂不可见
61
+ - Codex 当前限制(MVP):`/compact` 暂不支持,请继续在同一会话中对话
62
+ - 需要定位脚本入口、升级步骤或文件落点时,先看 `~/.metame/docs/pointer-map.md`
@@ -0,0 +1,119 @@
1
+ # MetaMe 维护手册(Claude/Codex 双引擎)
2
+
3
+ > 适用范围:`scripts/daemon.js` 后台 daemon 链路(飞书/Telegram 路由、会话执行、Agent 绑定)。
4
+
5
+ ## 1. 引擎路由规则
6
+
7
+ ### 手机端(daemon 生效)
8
+
9
+ - 路由入口:`chat_agent_map -> project -> project.engine`
10
+ - `project.engine` 可选值:`claude`(默认)/`codex`
11
+ - 未配置 `engine` 时等价 `claude`
12
+
13
+ 示例:
14
+
15
+ ```yaml
16
+ projects:
17
+ reviewer:
18
+ name: "Reviewer"
19
+ cwd: "~/AGI/MetaMe"
20
+ engine: codex
21
+
22
+ feishu:
23
+ chat_agent_map:
24
+ "oc_xxx": "reviewer"
25
+ ```
26
+
27
+ ### 电脑端(CLI)
28
+
29
+ - Claude 入口:`metame`(等价启动 Claude + MetaMe 初始化)
30
+ - Codex 入口:`metame codex [args]`
31
+ - 也可直接用原生命令:`claude` / `codex`
32
+
33
+ ## 2. Agent 创建与引擎写入
34
+
35
+ - 默认创建 Agent:不写 `engine` 字段(保持兼容)
36
+ - 创建语句中包含 `codex` 关键词:写入 `engine: codex`
37
+ - 绑定语句包含 `codex` 时同样支持写入 `engine: codex`
38
+
39
+ 示例:
40
+
41
+ - `创建一个 codex agent,目录是 ~/projects/reviewer`
42
+ - `用 codex 建个代码审查 agent,目录 ~/projects/pr-review`
43
+
44
+ ## 3. 会话与执行规则
45
+
46
+ - 引擎 runtime 工厂:`scripts/daemon-engine-runtime.js`
47
+ - 会话执行入口:`scripts/daemon-claude-engine.js`(Claude/Codex 共用)
48
+ - Session 回写:`patchSessionSerialized()` 串行化,避免 thread.started 竞态覆盖
49
+
50
+ ### Codex 会话策略
51
+
52
+ - 首轮:`codex exec --json -`
53
+ - 续轮:`codex exec resume <thread_id> --json -`
54
+ - `resume` 失败自动重试:同一 `chatId` 在 10 分钟内最多 1 次
55
+ - 收到新 `thread_id` 时自动迁移 session id
56
+
57
+ ## 4. 命令行为差异
58
+
59
+ - `/stop`:引擎中性,按 `activeProcesses.killSignal` 停止
60
+ - `/compact`:
61
+ - Claude 会话:正常压缩
62
+ - Codex 会话:返回“暂不支持,请继续同会话”
63
+ - `/engine`:
64
+ - 查询当前默认引擎:`/engine`
65
+ - 切换默认引擎:`/engine claude` 或 `/engine codex`
66
+ - `/distill-model`:
67
+ - 查询当前蒸馏模型:`/distill-model`
68
+ - 设置蒸馏模型:`/distill-model gpt-5.1-codex-mini`
69
+ - 也支持严格自然语言:`把蒸馏模型改成 5.1mini`
70
+ - `/doctor`:
71
+ - 同时检查 Claude/Codex CLI 可用性
72
+ - 仅在“当前默认引擎对应 CLI 不可用”时判为故障
73
+ - 自定义 provider 下允许任意合法模型名(不再强制 sonnet/opus/haiku)
74
+
75
+ ## 5. 运行时文件与状态
76
+
77
+ - 配置:`~/.metame/daemon.yaml`
78
+ - daemon 状态:`~/.metame/daemon_state.json`
79
+ - 活跃子进程:`~/.metame/active_agent_pids.json`
80
+
81
+ ## 6. 常见故障排查
82
+
83
+ ### Codex 认证失败
84
+
85
+ 症状:返回 `AUTH_REQUIRED`
86
+
87
+ 处理:
88
+
89
+ 1. 执行 `codex login`
90
+ 2. 或配置 `OPENAI_API_KEY`
91
+ 3. 重新发送同一条消息
92
+
93
+ ### Codex 频率限制
94
+
95
+ 症状:返回 `RATE_LIMIT`
96
+
97
+ 处理:
98
+
99
+ 1. 等待限流窗口恢复
100
+ 2. 降低并发或切回 Claude 路由
101
+
102
+ ### 会话续接异常
103
+
104
+ 症状:Codex `resume` 报错后反复失败
105
+
106
+ 处理:
107
+
108
+ 1. daemon 已自动做一次 fresh `exec` 重试
109
+ 2. 若仍失败,手动 `/new` 新开会话
110
+ 3. 检查 `~/.metame/active_agent_pids.json` 是否残留异常进程
111
+
112
+ ## 7. 变更后维护动作
113
+
114
+ 1. `npm test`
115
+ 2. `npm run sync:plugin`
116
+ 3. 更新文档:
117
+ - `scripts/docs/pointer-map.md`
118
+ - `README.md`
119
+ - `README中文版.md`
@@ -0,0 +1,88 @@
1
+ # MetaMe 脚本/文档指针地图
2
+
3
+ > 目的:回答“这段能力在哪个文件”“当前升级做到哪一步”“先看哪个脚本”。
4
+
5
+ ## 快速入口
6
+
7
+ - 主入口:`index.js`
8
+ - CLI 双入口:`metame`(Claude)/`metame codex [args]`(Codex)
9
+ - Daemon 主循环:`scripts/daemon.js`
10
+ - 多引擎 runtime 适配层:`scripts/daemon-engine-runtime.js`
11
+ - 会话执行引擎(Claude/Codex 共用入口):`scripts/daemon-claude-engine.js`
12
+ - 管理命令:`scripts/daemon-admin-commands.js`
13
+ - 命令路由:`scripts/daemon-command-router.js`
14
+ - 执行命令(`/stop`、`/compact` 等):`scripts/daemon-exec-commands.js`
15
+ - 会话存储:`scripts/daemon-session-store.js`
16
+ - 默认配置:`scripts/daemon-default.yaml`
17
+ - Provider/蒸馏模型配置:`scripts/providers.js`(`/provider`、`/distill-model`)
18
+ - 维护手册:`scripts/docs/maintenance-manual.md`
19
+
20
+ ## 多引擎(Claude/Codex)定位
21
+
22
+ - Runtime 工厂与事件归一化:
23
+ - `scripts/daemon-engine-runtime.js`
24
+ - 关键点:`normalizeEngineName()`、`buildClaudeArgs()`、`buildCodexArgs()`、`parseCodexStreamEvent()`
25
+
26
+ - 会话与引擎选择:
27
+ - `scripts/daemon-claude-engine.js`
28
+ - 关键点:`askClaude()` 按 `project.engine`/session 选择 runtime;`patchSessionSerialized()` 串行回写 session
29
+ - Codex 规则:`exec`/`resume`、10 分钟窗口内一次自动重试、`thread_id` 迁移回写
30
+
31
+ - 路由与 Agent 创建:
32
+ - `scripts/daemon-command-router.js`
33
+ - `scripts/daemon-agent-tools.js`
34
+ - 关键点:自然语言提取 `codex` 关键词;默认 `claude` 不写 `engine` 字段,仅 `codex` 持久化 `engine: codex`
35
+
36
+ - 会话命令与兼容边界:
37
+ - `scripts/daemon-exec-commands.js`
38
+ - 关键点:`/stop` 引擎中性;`/compact` 在 codex 会话返回“暂不支持”
39
+
40
+ - 运行时引擎切换与诊断:
41
+ - `scripts/daemon-admin-commands.js`
42
+ - 关键点:`/engine` 切换默认引擎;`/doctor` 按默认引擎检查 CLI 可用性(Claude/Codex)并兼容自定义 provider 模型名
43
+
44
+ ## Mentor Mode(Step 1-4)定位
45
+
46
+ - Step 1 数据基建:
47
+ - `scripts/session-analytics.js`
48
+ - 关键点:`extractSkeleton()` 新增数值指标、`detectSignificantSession()`
49
+ - `scripts/schema.js`:`growth.mentor_mode`、`growth.mentor_friction_level`、`growth.weekly_report_last`
50
+ - `scripts/memory.js`:`fact_labels` 表结构
51
+
52
+ - Step 2 决策引擎:
53
+ - `scripts/mentor-engine.js`
54
+ - 关键 API:`checkEmotionBreaker` / `buildMentorPrompt` / `computeZone` / `registerDebt` / `collectDebt` / `detectPatterns`
55
+ - 运行时状态文件:`~/.metame/mentor_runtime.json`
56
+
57
+ - Step 3 Hook 接入:
58
+ - `scripts/daemon-claude-engine.js`:Pre-flight / Context / Post-flight 三段 Hook
59
+ - `scripts/daemon-admin-commands.js`:`/mentor on|off|level|status`
60
+ - `scripts/daemon-default.yaml`:`daemon.mentor` 配置段
61
+
62
+ - Step 4 Distiller & Memory 闭环:
63
+ - `scripts/distill.js`:`competence_signals` 合并、significant session postmortem 产出、`bug_lesson` 回写
64
+ - `scripts/memory-extract.js`:消费 `saveFacts().savedFacts`,写入 `fact_labels`
65
+ - `scripts/memory.js`:`saveFactLabels()` 原子写入 API
66
+ - `scripts/memory-nightly-reflect.js`:`synthesized_insight` 回写、知识胶囊聚合与 `knowledge_capsule` 回写
67
+
68
+ ## 运行时数据位置
69
+
70
+ - 画像:`~/.claude_profile.yaml`
71
+ - 记忆数据库:`~/.metame/memory.db`
72
+ - 会话标签:`~/.metame/session_tags.json`
73
+ - 进程 PID 记录:`~/.metame/active_agent_pids.json`
74
+ - 夜间反思文档:`~/.metame/memory/decisions/`、`~/.metame/memory/lessons/`
75
+ - 知识胶囊:`~/.metame/memory/capsules/`
76
+ - 复盘文档:`~/.metame/memory/postmortems/`
77
+
78
+ ## 诊断顺序(推荐)
79
+
80
+ 1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
81
+ 2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
82
+ 3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
83
+ 4. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
84
+
85
+ ## 同步提示
86
+
87
+ - 每次改 `scripts/` 后执行:`npm run sync:plugin`
88
+ - plugin 镜像路径:`plugin/scripts/*`
@@ -416,10 +416,15 @@ function createBot(config) {
416
416
  const chatId = data.open_chat_id || data.chat_id
417
417
  || (data.context && data.context.open_chat_id)
418
418
  || (data.event && data.event.open_chat_id);
419
+ const senderId = (data.operator && data.operator.open_id)
420
+ || (data.open_id)
421
+ || (data.user && data.user.open_id)
422
+ || (data.context && data.context.open_id)
423
+ || null;
419
424
  if (action && chatId) {
420
425
  const cmd = action.value && action.value.cmd;
421
426
  if (cmd) {
422
- Promise.resolve().then(() => onMessage(chatId, cmd, data)).catch((err) => {
427
+ Promise.resolve().then(() => onMessage(chatId, cmd, data, null, senderId)).catch((err) => {
423
428
  try { console.error(`[feishu-adapter] card action error: ${err && err.message || err}`); } catch { }
424
429
  });
425
430
  }