metame-cli 1.3.20 โ†’ 1.3.22

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.
package/scripts/daemon.js CHANGED
@@ -26,6 +26,10 @@ const PID_FILE = path.join(METAME_DIR, 'daemon.pid');
26
26
  const LOG_FILE = path.join(METAME_DIR, 'daemon.log');
27
27
  const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
28
28
 
29
+ // Skill evolution module (hot path + cold path)
30
+ let skillEvolution = null;
31
+ try { skillEvolution = require('./skill-evolution'); } catch { /* graceful fallback */ }
32
+
29
33
  // ---------------------------------------------------------
30
34
  // SKILL ROUTING (keyword โ†’ /skillname prefix, like metame-desktop)
31
35
  // ---------------------------------------------------------
@@ -121,7 +125,7 @@ function loadConfig() {
121
125
 
122
126
  function backupConfig() {
123
127
  const bak = CONFIG_FILE + '.bak';
124
- try { fs.copyFileSync(CONFIG_FILE, bak); } catch {}
128
+ try { fs.copyFileSync(CONFIG_FILE, bak); } catch { }
125
129
  }
126
130
 
127
131
  function restoreConfig() {
@@ -132,7 +136,7 @@ function restoreConfig() {
132
136
  // Preserve security-critical fields from current config (chat IDs, agent map)
133
137
  // so a /fix never loses manually-added channels
134
138
  let curCfg = {};
135
- try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch {}
139
+ try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch { }
136
140
  for (const adapter of ['feishu', 'telegram']) {
137
141
  if (curCfg[adapter] && bakCfg[adapter]) {
138
142
  const curIds = curCfg[adapter].allowed_chat_ids || [];
@@ -559,6 +563,25 @@ function startHeartbeat(config, notifyFn) {
559
563
  }
560
564
  }
561
565
  }
566
+
567
+ // Skill evolution: check queue and notify user of actionable items
568
+ if (skillEvolution) {
569
+ try {
570
+ const notifications = skillEvolution.checkEvolutionQueue();
571
+ for (const item of notifications) {
572
+ let msg = '';
573
+ if (item.type === 'skill_gap') {
574
+ msg = `๐Ÿงฌ *ๆŠ€่ƒฝ็ผบๅฃๆฃ€ๆต‹*\n${item.reason}`;
575
+ if (item.search_hint) msg += `\nๆœ็ดขๅปบ่ฎฎ: \`${item.search_hint}\``;
576
+ } else if (item.type === 'skill_fix') {
577
+ msg = `๐Ÿ”ง *ๆŠ€่ƒฝ้œ€่ฆไฟฎๅค*\nๆŠ€่ƒฝ \`${item.skill_name}\` ${item.reason}`;
578
+ } else if (item.type === 'user_complaint') {
579
+ msg = `โš ๏ธ *ๆŠ€่ƒฝๅ้ฆˆ*\nๆŠ€่ƒฝ \`${item.skill_name}\` ๆ”ถๅˆฐ็”จๆˆทๅ้ฆˆ\n${item.reason}`;
580
+ }
581
+ if (msg && notifyFn) notifyFn(msg);
582
+ }
583
+ } catch (e) { log('WARN', `Skill evolution queue check failed: ${e.message}`); }
584
+ }
562
585
  }, checkIntervalSec * 1000);
563
586
 
564
587
  return timer;
@@ -601,7 +624,7 @@ async function startTelegramBridge(config, executeTaskByName) {
601
624
  if (update.callback_query) {
602
625
  const cb = update.callback_query;
603
626
  const chatId = cb.message && cb.message.chat.id;
604
- bot.answerCallback(cb.id).catch(() => {});
627
+ bot.answerCallback(cb.id).catch(() => { });
605
628
  if (chatId && cb.data) {
606
629
  const allowedIds = (loadConfig().telegram && loadConfig().telegram.allowed_chat_ids) || [];
607
630
  if (!allowedIds.includes(chatId)) continue;
@@ -619,9 +642,10 @@ async function startTelegramBridge(config, executeTaskByName) {
619
642
  const chatId = msg.chat.id;
620
643
 
621
644
  // Security: check whitelist (empty = deny all) โ€” read live config to support hot-reload
622
- // Exception: /bind is allowed from any chat so users can self-register new groups
645
+ // Exception: /bind and /agent bind/new are allowed from any chat so users can self-register new groups
623
646
  const allowedIds = (loadConfig().telegram && loadConfig().telegram.allowed_chat_ids) || [];
624
- const isBindCmd = msg.text && msg.text.trim().startsWith('/bind');
647
+ const trimmedText = msg.text && msg.text.trim();
648
+ const isBindCmd = trimmedText && (trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
625
649
  if (!allowedIds.includes(chatId) && !isBindCmd) {
626
650
  log('WARN', `Rejected message from unauthorized chat: ${chatId}`);
627
651
  continue;
@@ -697,10 +721,10 @@ async function startTelegramBridge(config, executeTaskByName) {
697
721
  }
698
722
 
699
723
  // โ”€โ”€ Timing constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
700
- const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
701
- const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
724
+ const CLAUDE_COOLDOWN_MS = 10000; // 10s between Claude calls per chat
725
+ const STATUS_THROTTLE_MS = 3000; // Min 3s between streaming status updates
702
726
  const FALLBACK_THROTTLE_MS = 8000; // 8s between fallback status updates
703
- const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
727
+ const DEDUP_TTL_MS = 60000; // Feishu message dedup window (60s)
704
728
  // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
705
729
 
706
730
  // Rate limiter for /ask and /run โ€” prevents rapid-fire Claude calls
@@ -795,7 +819,7 @@ async function sendDirPicker(bot, chatId, mode, title) {
795
819
  * - Shows up to 12 subdirs per page with pagination
796
820
  */
797
821
  async function sendBrowse(bot, chatId, mode, dirPath, title, page = 0) {
798
- const cmd = mode === 'new' ? '/new' : mode === 'bind' ? '/bind-dir' : '/cd';
822
+ const cmd = mode === 'new' ? '/new' : mode === 'bind' ? '/bind-dir' : mode === 'agent-new' ? '/agent-dir' : '/cd';
799
823
  const PAGE_SIZE = 10;
800
824
  try {
801
825
  const entries = fs.readdirSync(dirPath, { withFileTypes: true });
@@ -943,6 +967,52 @@ async function sendDirListing(bot, chatId, baseDir, arg) {
943
967
  }
944
968
  }
945
969
 
970
+ /**
971
+ * ๆ™บ่ƒฝๅˆๅนถ Agent ่ง’่‰ฒๆ่ฟฐๅˆฐ CLAUDE.md
972
+ * ๅฆ‚ๆžœ็›ฎๅฝ•ไธญๆฒกๆœ‰ CLAUDE.md๏ผŒ็›ดๆŽฅๅˆ›ๅปบ๏ผ›ๅฆๅˆ™่ฐƒ็”จ Claude ๅˆๅนถใ€‚
973
+ */
974
+ async function mergeAgentRole(cwd, description) {
975
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
976
+ if (!fs.existsSync(claudeMdPath)) {
977
+ // ็›ดๆŽฅๅˆ›ๅปบ๏ผŒๆ— ้œ€่ฐƒ Claude
978
+ const content = `## Agent ่ง’่‰ฒ\n\n${description}\n`;
979
+ fs.writeFileSync(claudeMdPath, content, 'utf8');
980
+ return { created: true };
981
+ }
982
+
983
+ const existing = fs.readFileSync(claudeMdPath, 'utf8');
984
+ const prompt = `็Žฐๆœ‰ CLAUDE.md ๅ†…ๅฎน๏ผš
985
+ ---
986
+ ${existing}
987
+ ---
988
+
989
+ ็”จๆˆทไธบ่ฟ™ไธช Agent ๅฎšไน‰็š„่ง’่‰ฒๅ’Œ่Œ่ดฃ๏ผš
990
+ "${description}"
991
+
992
+ ่ฏทๅฐ†็”จๆˆทๆ„ๅ›พๅˆๅนถ่ฟ› CLAUDE.md๏ผš
993
+ 1. ๆ‰พๅˆฐ็Žฐๆœ‰่ง’่‰ฒ/่Œ่ดฃ็›ธๅ…ณ็ซ ่Š‚ โ†’ ๆ›ดๆ–ฐๆ›ฟๆข
994
+ 2. ๆฒกๆœ‰ไธ“ๅฑž็ซ ่Š‚ไฝ†ๆœ‰็›ธๅ…ณๅ†…ๅฎน โ†’ ๅˆๅนถ่ฟ›ๅŽป
995
+ 3. ๅฎŒๅ…จๆฒกๆœ‰็›ธๅ…ณๅ†…ๅฎน โ†’ ๅœจๆ–‡ไปถๆœ€้กถ้ƒจๆ–ฐๅขž ## Agent ่ง’่‰ฒ section
996
+ 4. ่พ“ๅ‡บๅฎŒๆ•ด CLAUDE.md ๅ†…ๅฎน๏ผŒไฟๆŒๅŽŸๆœ‰ๅ…ถไป–ๅ†…ๅฎนไธๅ˜
997
+ 5. ไฟๆŒ็ฎ€ๆด๏ผŒ็ฆๆญข้‡ๅค
998
+
999
+ ็›ดๆŽฅ่พ“ๅ‡บๅฎŒๆ•ด CLAUDE.md ๅ†…ๅฎน๏ผŒไธ่ฆๅŠ ไปปไฝ•่งฃ้‡Šๆˆ–ไปฃ็ ๅ—ๆ ‡่ฎฐใ€‚`;
1000
+
1001
+ const claudeArgs = ['-p', '--output-format', 'text', '--max-turns', '1'];
1002
+ const { output, error } = await spawnClaudeAsync(claudeArgs, prompt, HOME, 60000);
1003
+ if (error || !output) {
1004
+ return { error: error || 'ๅˆๅนถๅคฑ่ดฅ' };
1005
+ }
1006
+
1007
+ let cleanOutput = output.trim();
1008
+ if (cleanOutput.startsWith('```')) {
1009
+ cleanOutput = cleanOutput.replace(/^```[a-zA-Z]*\n/, '').replace(/\n```$/, '');
1010
+ }
1011
+
1012
+ fs.writeFileSync(claudeMdPath, cleanOutput, 'utf8');
1013
+ return { merged: true };
1014
+ }
1015
+
946
1016
  /**
947
1017
  * Unified command handler โ€” shared by Telegram & Feishu
948
1018
  */
@@ -1009,48 +1079,11 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
1009
1079
  }
1010
1080
 
1011
1081
  // --- /bind <name> [cwd]: register this chat as a dedicated agent channel ---
1012
- // With cwd: /bind ๅฐ็พŽ ~/ โ†’ bind immediately
1013
- // Without cwd: /bind ๆ•™ๆŽˆ โ†’ show directory picker
1014
- if (text.startsWith('/bind ') || text === '/bind') {
1015
- const args = text.slice(5).trim();
1016
- const parts = args.split(/\s+/);
1017
- const agentName = parts[0];
1018
- const agentCwd = parts.slice(1).join(' ');
1019
-
1020
- if (!agentName) {
1021
- await bot.sendMessage(chatId, '็”จๆณ•: /bind <ๅ็งฐ> [ๅทฅไฝœ็›ฎๅฝ•]\nไพ‹: /bind ๅฐ็พŽ ~/\nๆˆ–: /bind ๆ•™ๆŽˆ (ๅผนๅ‡บ็›ฎๅฝ•้€‰ๆ‹ฉ)');
1022
- return;
1023
- }
1024
-
1025
- if (!agentCwd) {
1026
- // No cwd given โ€” show directory picker
1027
- pendingBinds.set(String(chatId), agentName);
1028
- await sendDirPicker(bot, chatId, 'bind', `ไธบใ€Œ${agentName}ใ€้€‰ๆ‹ฉๅทฅไฝœ็›ฎๅฝ•:`);
1029
- return;
1030
- }
1031
-
1032
- await doBindAgent(bot, chatId, agentName, agentCwd);
1033
- return;
1034
- }
1035
-
1036
- // --- /bind-dir <path>: called by directory picker to complete a pending bind ---
1037
- if (text.startsWith('/bind-dir ')) {
1038
- const dirPath = expandPath(text.slice(10).trim());
1039
- const agentName = pendingBinds.get(String(chatId));
1040
- if (!agentName) {
1041
- await bot.sendMessage(chatId, 'โŒ ๆฒกๆœ‰ๅพ…ๅฎŒๆˆ็š„ /bind๏ผŒ่ฏท้‡ๆ–ฐๅ‘้€ /bind <ๅ็งฐ>');
1042
- return;
1043
- }
1044
- pendingBinds.delete(String(chatId));
1045
- await doBindAgent(bot, chatId, agentName, dirPath);
1046
- return;
1047
- }
1048
1082
 
1049
1083
  // --- chat_agent_map: auto-switch agent based on dedicated chatId ---
1050
1084
  // Configure in daemon.yaml: feishu.chat_agent_map or telegram.chat_agent_map
1051
1085
  // e.g. chat_agent_map: { "oc_xxx": "personal", "oc_yyy": "metame" }
1052
- const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) ||
1053
- (config.telegram && config.telegram.chat_agent_map) || {};
1086
+ const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
1054
1087
  const mappedKey = chatAgentMap[String(chatId)];
1055
1088
  if (mappedKey && config.projects && config.projects[mappedKey]) {
1056
1089
  const proj = config.projects[mappedKey];
@@ -1088,8 +1121,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
1088
1121
  if (!arg) {
1089
1122
  // In a dedicated agent group, use the agent's bound cwd directly
1090
1123
  const newCfg = loadConfig();
1091
- const agentMap = (newCfg.feishu && newCfg.feishu.chat_agent_map) ||
1092
- (newCfg.telegram && newCfg.telegram.chat_agent_map) || {};
1124
+ const agentMap = { ...(newCfg.telegram ? newCfg.telegram.chat_agent_map : {}), ...(newCfg.feishu ? newCfg.feishu.chat_agent_map : {}) };
1093
1125
  const boundKey = agentMap[String(chatId)];
1094
1126
  const boundProj = boundKey && newCfg.projects && newCfg.projects[boundKey];
1095
1127
  if (boundProj && boundProj.cwd) {
@@ -1342,21 +1374,224 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
1342
1374
  return;
1343
1375
  }
1344
1376
 
1345
- if (text === '/agent') {
1346
- const projects = config.projects || {};
1347
- const entries = Object.entries(projects).filter(([, p]) => p.cwd);
1348
- if (entries.length === 0) {
1349
- await bot.sendMessage(chatId, 'No projects configured. Add projects with cwd to daemon.yaml.');
1377
+ // โ”€โ”€โ”€ /agent ๅ‘ฝไปคไฝ“็ณป โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1378
+ // /agent bind <ๅ็งฐ> [็›ฎๅฝ•] โ€” ๆŠŠๅฝ“ๅ‰็พค็ป‘ๅฎšไธบไธ“ๅฑž agent ้ข‘้“
1379
+ // /agent list โ€” ๆŸฅ็œ‹ๆ‰€ๆœ‰ๅทฒ้…็ฝฎ็š„ agent
1380
+ // /agent new โ€” ๅคšๆญฅๅ‘ๅฏผๆ–ฐๅปบ agent
1381
+ // /agent edit โ€” ็ผ–่พ‘ๅฝ“ๅ‰ agent ็š„ CLAUDE.md ่ง’่‰ฒๅฎšไน‰
1382
+ // /agent reset โ€” ๅˆ ้™คๅฝ“ๅ‰ agent ็š„่ง’่‰ฒ section
1383
+ // /agent โ€” ๅผนๅ‡บ agent ๅˆ‡ๆข้€‰ๆ‹ฉๅ™จ๏ผˆๆ— ๅ‚ๆ•ฐ๏ผ‰
1384
+ // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1385
+
1386
+ // ๅค„็† /agent new ๅคšๆญฅๅ‘ๅฏผ็Šถๆ€ๆœบไธญ็š„ๆ–‡ๆœฌ่พ“ๅ…ฅ๏ผˆname/desc ๆญฅ้ชค๏ผ‰
1387
+ {
1388
+ const flow = pendingAgentFlows.get(String(chatId));
1389
+ if (flow && flow.step === 'name' && text && !text.startsWith('/')) {
1390
+ // ๆญฅ้ชค2: ็”จๆˆทๅ›žๅคไบ† Agent ๅ็งฐ
1391
+ flow.name = text.trim();
1392
+ flow.step = 'desc';
1393
+ pendingAgentFlows.set(String(chatId), flow);
1394
+ await bot.sendMessage(chatId, `ๅฅฝ็š„๏ผŒAgent ๅ็งฐๆ˜ฏใ€Œ${flow.name}ใ€\n\n่ฏทๆ่ฟฐ่ฟ™ไธช Agent ็š„่ง’่‰ฒๅ’Œ่Œ่ดฃ๏ผˆ็”จ่‡ช็„ถ่ฏญ่จ€๏ผ‰๏ผš`);
1350
1395
  return;
1351
1396
  }
1352
- const currentSession = getSession(chatId);
1353
- const currentCwd = currentSession?.cwd ? path.resolve(expandPath(currentSession.cwd)) : null;
1354
- const buttons = entries.map(([key, p]) => {
1355
- const projCwd = normalizeCwd(p.cwd);
1356
- const active = currentCwd && path.resolve(projCwd) === currentCwd ? ' โ—€' : '';
1357
- return [{ text: `${p.icon || '๐Ÿค–'} ${p.name || key}${active}`, callback_data: `/cd ${projCwd}` }];
1358
- });
1359
- await bot.sendButtons(chatId, 'ๅˆ‡ๆขๅฏน่ฏๅฏน่ฑก', buttons);
1397
+ if (flow && flow.step === 'desc' && text && !text.startsWith('/')) {
1398
+ // ๆญฅ้ชค3: ็”จๆˆทๅ›žๅคไบ†่ง’่‰ฒๆ่ฟฐ
1399
+ pendingAgentFlows.delete(String(chatId));
1400
+ const { dir, name } = flow;
1401
+ const description = text.trim();
1402
+ await bot.sendMessage(chatId, `โณ ๆญฃๅœจ้…็ฝฎ Agentใ€Œ${name}ใ€๏ผŒ็จ็ญ‰...`);
1403
+ try {
1404
+ // a. ๅ†™ๅ…ฅ config๏ผˆprojects ้‡Œๆ–ฐๅขžๆก็›ฎ๏ผ‰ๅนถ็ป‘ๅฎšๅฝ“ๅ‰็พค
1405
+ await doBindAgent(bot, chatId, name, dir);
1406
+ // b. ๆ™บ่ƒฝๅˆๅนถ CLAUDE.md
1407
+ const mergeResult = await mergeAgentRole(dir, description);
1408
+ if (mergeResult.error) {
1409
+ await bot.sendMessage(chatId, `โš ๏ธ CLAUDE.md ๅˆๅนถๅคฑ่ดฅ: ${mergeResult.error}๏ผŒๅ…ถไป–้…็ฝฎๅทฒไฟๅญ˜`);
1410
+ } else if (mergeResult.created) {
1411
+ await bot.sendMessage(chatId, `๐Ÿ“ ๅทฒๅˆ›ๅปบ CLAUDE.md ๅนถๅ†™ๅ…ฅ่ง’่‰ฒๅฎšไน‰`);
1412
+ } else {
1413
+ await bot.sendMessage(chatId, `๐Ÿ“ ๅทฒๅฐ†่ง’่‰ฒๅฎšไน‰ๅˆๅนถ่ฟ›็Žฐๆœ‰ CLAUDE.md`);
1414
+ }
1415
+ } catch (e) {
1416
+ await bot.sendMessage(chatId, `โŒ ๅˆ›ๅปบ Agent ๅคฑ่ดฅ: ${e.message}`);
1417
+ }
1418
+ return;
1419
+ }
1420
+ }
1421
+
1422
+ // /agent edit ็Šถๆ€ๆœบ๏ผš็ญ‰ๅพ…็”จๆˆท่พ“ๅ…ฅไฟฎๆ”นๆ„ๅ›พ
1423
+ {
1424
+ const editFlow = pendingAgentFlows.get(String(chatId) + ':edit');
1425
+ if (editFlow && text && !text.startsWith('/')) {
1426
+ pendingAgentFlows.delete(String(chatId) + ':edit');
1427
+ const { cwd } = editFlow;
1428
+ await bot.sendMessage(chatId, 'โณ ๆญฃๅœจๆ›ดๆ–ฐ CLAUDE.md...');
1429
+ const mergeResult = await mergeAgentRole(cwd, text.trim());
1430
+ if (mergeResult.error) {
1431
+ await bot.sendMessage(chatId, `โŒ ๆ›ดๆ–ฐๅคฑ่ดฅ: ${mergeResult.error}`);
1432
+ } else {
1433
+ await bot.sendMessage(chatId, 'โœ… CLAUDE.md ๅทฒๆ›ดๆ–ฐ');
1434
+ }
1435
+ return;
1436
+ }
1437
+ }
1438
+
1439
+ if (text === '/agent' || text.startsWith('/agent ')) {
1440
+ const agentArg = text === '/agent' ? '' : text.slice(7).trim();
1441
+ const agentParts = agentArg.split(/\s+/);
1442
+ const agentSub = agentParts[0]; // bind / list / new / edit / reset / ''
1443
+
1444
+ // /agent bind <ๅ็งฐ> [็›ฎๅฝ•] โ€” ๆ›ฟไปฃๆ—ง็š„ /bind
1445
+ if (agentSub === 'bind') {
1446
+ const bindName = agentParts[1];
1447
+ const bindCwd = agentParts.slice(2).join(' ');
1448
+ if (!bindName) {
1449
+ await bot.sendMessage(chatId, '็”จๆณ•: /agent bind <ๅ็งฐ> [ๅทฅไฝœ็›ฎๅฝ•]\nไพ‹: /agent bind ๅฐ็พŽ ~/\nๆˆ–: /agent bind ๆ•™ๆŽˆ (ๅผนๅ‡บ็›ฎๅฝ•้€‰ๆ‹ฉ)');
1450
+ return;
1451
+ }
1452
+ if (!bindCwd) {
1453
+ pendingBinds.set(String(chatId), bindName);
1454
+ await sendDirPicker(bot, chatId, 'bind', `ไธบใ€Œ${bindName}ใ€้€‰ๆ‹ฉๅทฅไฝœ็›ฎๅฝ•:`);
1455
+ return;
1456
+ }
1457
+ await doBindAgent(bot, chatId, bindName, expandPath(bindCwd));
1458
+ return;
1459
+ }
1460
+
1461
+ // /agent list โ€” ๆŸฅ็œ‹ๆ‰€ๆœ‰ๅทฒ้…็ฝฎ็š„ agent
1462
+ if (agentSub === 'list') {
1463
+ const cfg = loadConfig();
1464
+ const projects = cfg.projects || {};
1465
+ const entries = Object.entries(projects).filter(([, p]) => p.cwd);
1466
+ if (entries.length === 0) {
1467
+ await bot.sendMessage(chatId, 'ๆš‚ๆ— ๅทฒ้…็ฝฎ็š„ Agentใ€‚\nไฝฟ็”จ /agent new ๅˆ›ๅปบ๏ผŒๆˆ– /agent bind <ๅ็งฐ> ็ป‘ๅฎš็›ฎๅฝ•ใ€‚');
1468
+ return;
1469
+ }
1470
+ // ๆ‰พๅ‡บๅฝ“ๅ‰็พค็ป‘ๅฎš็š„ agent
1471
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
1472
+ const boundKey = agentMap[String(chatId)];
1473
+ const lines = ['๐Ÿ“‹ ๅทฒ้…็ฝฎ็š„ Agent๏ผš', ''];
1474
+ for (const [key, p] of entries) {
1475
+ const icon = p.icon || '๐Ÿค–';
1476
+ const name = p.name || key;
1477
+ const displayCwd = (p.cwd || '').replace(HOME, '~');
1478
+ const bound = key === boundKey ? ' โ—€ ๅฝ“ๅ‰' : '';
1479
+ lines.push(`${icon} ${name}${bound}`);
1480
+ lines.push(` ็›ฎๅฝ•: ${displayCwd}`);
1481
+ lines.push(` Key: ${key}`);
1482
+ lines.push('');
1483
+ }
1484
+ await bot.sendMessage(chatId, lines.join('\n').trimEnd());
1485
+ return;
1486
+ }
1487
+
1488
+ // /agent new โ€” ๅคšๆญฅๅ‘ๅฏผๆ–ฐๅปบ agent
1489
+ if (agentSub === 'new') {
1490
+ pendingAgentFlows.set(String(chatId), { step: 'dir' });
1491
+ await sendBrowse(bot, chatId, 'agent-new', HOME, 'ๆญฅ้ชค1/3๏ผš้€‰ๆ‹ฉ่ฟ™ไธช Agent ็š„ๅทฅไฝœ็›ฎๅฝ•');
1492
+ return;
1493
+ }
1494
+
1495
+ // /agent edit โ€” ็ผ–่พ‘ๅฝ“ๅ‰ agent ็š„ CLAUDE.md ่ง’่‰ฒๅฎšไน‰
1496
+ if (agentSub === 'edit') {
1497
+ const cfg = loadConfig();
1498
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
1499
+ const boundKey = agentMap[String(chatId)];
1500
+ const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
1501
+ if (!boundProj || !boundProj.cwd) {
1502
+ await bot.sendMessage(chatId, 'โŒ ๅฝ“ๅ‰็พคๆœช็ป‘ๅฎš Agent๏ผŒ่ฏทๅ…ˆไฝฟ็”จ /agent bind ๆˆ– /agent new');
1503
+ return;
1504
+ }
1505
+ const cwd = normalizeCwd(boundProj.cwd);
1506
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
1507
+ let currentContent = '๏ผˆCLAUDE.md ไธๅญ˜ๅœจ๏ผ‰';
1508
+ if (fs.existsSync(claudeMdPath)) {
1509
+ currentContent = fs.readFileSync(claudeMdPath, 'utf8');
1510
+ // ๅชๅฑ•็คบๅ‰ 500 ๅญ—็ฌฆ
1511
+ if (currentContent.length > 500) {
1512
+ currentContent = currentContent.slice(0, 500) + '\n...(ๅทฒๆˆชๆ–ญ)';
1513
+ }
1514
+ }
1515
+ pendingAgentFlows.set(String(chatId) + ':edit', { cwd });
1516
+ await bot.sendMessage(chatId, `๐Ÿ“„ ๅฝ“ๅ‰ CLAUDE.md ๅ†…ๅฎน:\n\`\`\`\n${currentContent}\n\`\`\`\n\n่ฏทๆ่ฟฐไฝ ๆƒณๅš็š„ไฟฎๆ”น๏ผˆ็”จ่‡ช็„ถ่ฏญ่จ€๏ผŒไพ‹ๅฆ‚๏ผšใ€ŒๆŠŠ่ง’่‰ฒๆ”นๆˆๅŽ็ซฏๅทฅ็จ‹ๅธˆ๏ผŒไธ“ๆณจ Pythonใ€๏ผ‰๏ผš`);
1517
+ return;
1518
+ }
1519
+
1520
+ // /agent reset โ€” ๅˆ ้™ค CLAUDE.md ้‡Œ็š„่ง’่‰ฒ section
1521
+ if (agentSub === 'reset') {
1522
+ const cfg = loadConfig();
1523
+ const agentMap = { ...(cfg.telegram ? cfg.telegram.chat_agent_map : {}), ...(cfg.feishu ? cfg.feishu.chat_agent_map : {}) };
1524
+ const boundKey = agentMap[String(chatId)];
1525
+ const boundProj = boundKey && cfg.projects && cfg.projects[boundKey];
1526
+ if (!boundProj || !boundProj.cwd) {
1527
+ await bot.sendMessage(chatId, 'โŒ ๅฝ“ๅ‰็พคๆœช็ป‘ๅฎš Agent๏ผŒ่ฏทๅ…ˆไฝฟ็”จ /agent bind ๆˆ– /agent new');
1528
+ return;
1529
+ }
1530
+ const cwd = normalizeCwd(boundProj.cwd);
1531
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
1532
+ if (!fs.existsSync(claudeMdPath)) {
1533
+ await bot.sendMessage(chatId, 'โš ๏ธ CLAUDE.md ไธๅญ˜ๅœจ๏ผŒๆ— ้œ€้‡็ฝฎ');
1534
+ return;
1535
+ }
1536
+ let content = fs.readFileSync(claudeMdPath, 'utf8');
1537
+ // ็”จๆญฃๅˆ™ๅˆ ้™ค ## Agent ่ง’่‰ฒ section๏ผˆๅˆฐไธ‹ไธ€ไธช ## ๆˆ–ๆ–‡ไปถๆœซๅฐพ๏ผ‰
1538
+ content = content.replace(/(?:^|\n)## Agent ่ง’่‰ฒ\n[\s\S]*?(?=\n## |$)/, '').trimStart();
1539
+ // ๅฆ‚ๆžœๆฒกๅŒน้…ๅˆฐ๏ผŒ็ป™ๅ‡บๆ็คบ
1540
+ if (content === fs.readFileSync(claudeMdPath, 'utf8').trimStart()) {
1541
+ await bot.sendMessage(chatId, 'โš ๏ธ ๆœชๆ‰พๅˆฐใ€Œ## Agent ่ง’่‰ฒใ€section๏ผŒCLAUDE.md ๆœชไฟฎๆ”น');
1542
+ return;
1543
+ }
1544
+ fs.writeFileSync(claudeMdPath, content, 'utf8');
1545
+ await bot.sendMessage(chatId, 'โœ… ๅทฒๅˆ ้™ค่ง’่‰ฒ section๏ผŒ่ฏท้‡ๆ–ฐๅ‘้€่ง’่‰ฒๆ่ฟฐ๏ผˆ/agent edit ๆˆ– /agent new๏ผ‰');
1546
+ return;
1547
+ }
1548
+
1549
+ // /agent๏ผˆๆ— ๅ‚ๆ•ฐ๏ผ‰โ€” ๅผนๅ‡บ agent ๅˆ‡ๆข้€‰ๆ‹ฉๅ™จ
1550
+ {
1551
+ const projects = config.projects || {};
1552
+ const entries = Object.entries(projects).filter(([, p]) => p.cwd);
1553
+ if (entries.length === 0) {
1554
+ await bot.sendMessage(chatId, 'ๆš‚ๆ— ๅทฒ้…็ฝฎ็š„ Agentใ€‚\nไฝฟ็”จ /agent new ๆ–ฐๅปบ๏ผŒๆˆ– /agent bind <ๅ็งฐ> ็ป‘ๅฎš็›ฎๅฝ•ใ€‚');
1555
+ return;
1556
+ }
1557
+ const currentSession = getSession(chatId);
1558
+ const currentCwd = currentSession?.cwd ? path.resolve(expandPath(currentSession.cwd)) : null;
1559
+ const buttons = entries.map(([key, p]) => {
1560
+ const projCwd = normalizeCwd(p.cwd);
1561
+ const active = currentCwd && path.resolve(projCwd) === currentCwd ? ' โ—€' : '';
1562
+ return [{ text: `${p.icon || '๐Ÿค–'} ${p.name || key}${active}`, callback_data: `/cd ${projCwd}` }];
1563
+ });
1564
+ await bot.sendButtons(chatId, 'ๅˆ‡ๆขๅฏน่ฏๅฏน่ฑก', buttons);
1565
+ return;
1566
+ }
1567
+ }
1568
+
1569
+ // --- /bind-dir <path>: /agent bind ็›ฎๅฝ•้€‰ๆ‹ฉๅ™จ็š„ๅ†…้ƒจๅ›ž่ฐƒ ---
1570
+ if (text.startsWith('/bind-dir ')) {
1571
+ const dirPath = expandPath(text.slice(10).trim());
1572
+ const agentName = pendingBinds.get(String(chatId));
1573
+ if (!agentName) {
1574
+ await bot.sendMessage(chatId, 'โŒ ๆฒกๆœ‰ๅพ…ๅฎŒๆˆ็š„ /agent bind๏ผŒ่ฏท้‡ๆ–ฐๅ‘้€');
1575
+ return;
1576
+ }
1577
+ pendingBinds.delete(String(chatId));
1578
+ await doBindAgent(bot, chatId, agentName, dirPath);
1579
+ return;
1580
+ }
1581
+
1582
+ // --- /agent-dir <path>: /agent new ๅ‘ๅฏผ็š„็›ฎๅฝ•้€‰ๆ‹ฉๅ›ž่ฐƒ ---
1583
+ if (text.startsWith('/agent-dir ')) {
1584
+ const dirPath = expandPath(text.slice(11).trim());
1585
+ const flow = pendingAgentFlows.get(String(chatId));
1586
+ if (!flow || flow.step !== 'dir') {
1587
+ await bot.sendMessage(chatId, 'โŒ ๆฒกๆœ‰ๅพ…ๅฎŒๆˆ็š„ /agent new๏ผŒ่ฏท้‡ๆ–ฐๅ‘้€ /agent new');
1588
+ return;
1589
+ }
1590
+ flow.dir = dirPath;
1591
+ flow.step = 'name';
1592
+ pendingAgentFlows.set(String(chatId), flow);
1593
+ const displayPath = dirPath.replace(HOME, '~');
1594
+ await bot.sendMessage(chatId, `โœ“ ๅทฒ้€‰ๆ‹ฉ็›ฎๅฝ•๏ผš${displayPath}\n\nๆญฅ้ชค2/3๏ผš็ป™่ฟ™ไธช Agent ่ตทไธชๅๅญ—๏ผŸ`);
1360
1595
  return;
1361
1596
  }
1362
1597
 
@@ -1583,7 +1818,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
1583
1818
  if (text === '/budget') {
1584
1819
  const limit = (config.budget && config.budget.daily_limit) || 50000;
1585
1820
  const used = state.budget.tokens_used;
1586
- await bot.sendMessage(chatId, `Budget: ${used}/${limit} tokens (${((used/limit)*100).toFixed(1)}%)`);
1821
+ await bot.sendMessage(chatId, `Budget: ${used}/${limit} tokens (${((used / limit) * 100).toFixed(1)}%)`);
1587
1822
  return;
1588
1823
  }
1589
1824
 
@@ -1890,7 +2125,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
1890
2125
  break; // Found a message before checkpoint, stop
1891
2126
  }
1892
2127
  }
1893
- } catch {}
2128
+ } catch { }
1894
2129
  }
1895
2130
  if (cutIdx > 0) {
1896
2131
  const kept = lines.slice(0, cutIdx);
@@ -2131,8 +2366,13 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
2131
2366
  '/last โ€” ็ปง็ปญ็”ต่„‘ไธŠๆœ€่ฟ‘็š„ๅฏน่ฏ',
2132
2367
  '/cd last โ€” ๅˆ‡ๅˆฐ็”ต่„‘ๆœ€่ฟ‘็š„้กน็›ฎ็›ฎๅฝ•',
2133
2368
  '',
2134
- '๐Ÿค– Agent ๅˆ‡ๆข:',
2135
- '/agent โ€” ้€‰ๆ‹ฉๅฏน่ฏ็š„้กน็›ฎ/Agent',
2369
+ '๐Ÿค– Agent ็ฎก็†:',
2370
+ '/agent โ€” ๅˆ‡ๆข Agent',
2371
+ '/agent new โ€” ๅ‘ๅฏผๆ–ฐๅปบ Agent',
2372
+ '/agent bind <ๅ็งฐ> [็›ฎๅฝ•] โ€” ็ป‘ๅฎšๅฝ“ๅ‰็พค',
2373
+ '/agent list โ€” ๆŸฅ็œ‹ๆ‰€ๆœ‰ Agent',
2374
+ '/agent edit โ€” ็ผ–่พ‘ๅฝ“ๅ‰ Agent ่ง’่‰ฒ',
2375
+ '/agent reset โ€” ้‡็ฝฎๅฝ“ๅ‰ Agent ่ง’่‰ฒ',
2136
2376
  '',
2137
2377
  '๐Ÿ“‚ Session ็ฎก็†:',
2138
2378
  '/new [path] [name] โ€” ๆ–ฐๅปบไผš่ฏ',
@@ -2209,7 +2449,7 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName, sende
2209
2449
  await bot.sendMessage(chatId, 'Daily token budget exceeded.');
2210
2450
  return;
2211
2451
  }
2212
- await askClaude(bot, chatId, text, config);
2452
+ await askClaude(bot, chatId, text, config, readOnly);
2213
2453
  }
2214
2454
 
2215
2455
  // ---------------------------------------------------------
@@ -2520,6 +2760,8 @@ function spawnClaudeAsync(args, input, cwd, timeoutMs = 300000) {
2520
2760
  const timer = setTimeout(() => {
2521
2761
  killed = true;
2522
2762
  child.kill('SIGTERM');
2763
+ // Fix: escalate to SIGKILL if SIGTERM is ignored
2764
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch { } }, 5000);
2523
2765
  }, timeoutMs);
2524
2766
 
2525
2767
  child.stdout.on('data', (data) => { stdout += data.toString(); });
@@ -2580,9 +2822,49 @@ const CONTENT_EXTENSIONS = new Set([
2580
2822
  // Active Claude processes per chat (for /stop)
2581
2823
  const activeProcesses = new Map(); // chatId -> { child, aborted }
2582
2824
 
2825
+ // Fix3: persist child PIDs so next daemon startup can kill orphans
2826
+ const ACTIVE_PIDS_FILE = path.join(HOME, '.metame', 'active_claude_pids.json');
2827
+ function saveActivePids() {
2828
+ try {
2829
+ const pids = {};
2830
+ for (const [chatId, proc] of activeProcesses) {
2831
+ if (proc.child && proc.child.pid) pids[chatId] = proc.child.pid;
2832
+ }
2833
+ fs.writeFileSync(ACTIVE_PIDS_FILE, JSON.stringify(pids), 'utf8');
2834
+ } catch { }
2835
+ }
2836
+ function getProcessName(pid) {
2837
+ try {
2838
+ return execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf8', timeout: 2000 }).trim();
2839
+ } catch { return null; }
2840
+ }
2841
+ function killOrphanPids() {
2842
+ try {
2843
+ if (!fs.existsSync(ACTIVE_PIDS_FILE)) return;
2844
+ const pids = JSON.parse(fs.readFileSync(ACTIVE_PIDS_FILE, 'utf8'));
2845
+ for (const [chatId, pid] of Object.entries(pids)) {
2846
+ try {
2847
+ // Safety: only kill if PID still belongs to a claude process (prevent PID reuse accidents)
2848
+ const comm = getProcessName(pid);
2849
+ if (!comm || !comm.includes('claude')) {
2850
+ log('WARN', `Skipping PID ${pid} (chatId: ${chatId}): process is "${comm}", not claude`);
2851
+ continue;
2852
+ }
2853
+ process.kill(pid, 'SIGKILL');
2854
+ log('INFO', `Killed orphan claude PID ${pid} (chatId: ${chatId})`);
2855
+ } catch { }
2856
+ }
2857
+ fs.unlinkSync(ACTIVE_PIDS_FILE);
2858
+ } catch { }
2859
+ }
2860
+
2583
2861
  // Pending /bind flows: waiting for user to pick a directory
2584
2862
  const pendingBinds = new Map(); // chatId -> agentName
2585
2863
 
2864
+ // Pending /agent new ๅคšๆญฅๅ‘ๅฏผ็Šถๆ€ๆœบ
2865
+ // chatId -> { step: 'dir'|'name'|'desc', dir: string, name: string }
2866
+ const pendingAgentFlows = new Map();
2867
+
2586
2868
  // Message queue for messages received while a task is running
2587
2869
  const messageQueue = new Map(); // chatId -> { messages: string[], notified: false }
2588
2870
 
@@ -2694,6 +2976,7 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
2694
2976
  // Track active process for /stop
2695
2977
  if (chatId) {
2696
2978
  activeProcesses.set(chatId, { child, aborted: false });
2979
+ saveActivePids(); // Fix3: persist PID to disk
2697
2980
  }
2698
2981
 
2699
2982
  let buffer = '';
@@ -2703,10 +2986,13 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
2703
2986
  let lastStatusTime = 0;
2704
2987
  const STATUS_THROTTLE = STATUS_THROTTLE_MS;
2705
2988
  const writtenFiles = []; // Track files created/modified by Write tool
2989
+ const toolUsageLog = []; // Track all tool invocations for skill evolution
2706
2990
 
2707
2991
  const timer = setTimeout(() => {
2708
2992
  killed = true;
2709
2993
  child.kill('SIGTERM');
2994
+ // Fix: escalate to SIGKILL if SIGTERM is ignored
2995
+ setTimeout(() => { try { child.kill('SIGKILL'); } catch { } }, 5000);
2710
2996
  }, timeoutMs);
2711
2997
 
2712
2998
  child.stdout.on('data', (data) => {
@@ -2735,6 +3021,13 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
2735
3021
  if (block.type === 'tool_use') {
2736
3022
  const toolName = block.name || 'Tool';
2737
3023
 
3024
+ // Track tool usage for skill evolution
3025
+ const toolEntry = { tool: toolName };
3026
+ if (toolName === 'Skill' && block.input?.skill) toolEntry.skill = block.input.skill;
3027
+ else if (block.input?.command) toolEntry.context = block.input.command.slice(0, 50);
3028
+ else if (block.input?.file_path) toolEntry.context = path.basename(block.input.file_path);
3029
+ if (toolUsageLog.length < 50) toolUsageLog.push(toolEntry);
3030
+
2738
3031
  // Track files written by Write tool
2739
3032
  if (toolName === 'Write' && block.input?.file_path) {
2740
3033
  const filePath = block.input.file_path;
@@ -2799,7 +3092,7 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
2799
3092
  : `${displayEmoji} ${displayName}...`;
2800
3093
 
2801
3094
  if (onStatus) {
2802
- onStatus(status).catch(() => {});
3095
+ onStatus(status).catch(() => { });
2803
3096
  }
2804
3097
  }
2805
3098
  }
@@ -2834,23 +3127,23 @@ function spawnClaudeStreaming(args, input, cwd, onStatus, timeoutMs = 600000, ch
2834
3127
  // Clean up active process tracking
2835
3128
  const proc = chatId ? activeProcesses.get(chatId) : null;
2836
3129
  const wasAborted = proc && proc.aborted;
2837
- if (chatId) activeProcesses.delete(chatId);
3130
+ if (chatId) { activeProcesses.delete(chatId); saveActivePids(); } // Fix3
2838
3131
 
2839
3132
  if (wasAborted) {
2840
- resolve({ output: finalResult || null, error: 'Stopped by user', files: writtenFiles });
3133
+ resolve({ output: finalResult || null, error: 'Stopped by user', files: writtenFiles, toolUsageLog });
2841
3134
  } else if (killed) {
2842
- resolve({ output: finalResult || null, error: 'Timeout: Claude took too long', files: writtenFiles });
3135
+ resolve({ output: finalResult || null, error: 'Timeout: Claude took too long', files: writtenFiles, toolUsageLog });
2843
3136
  } else if (code !== 0) {
2844
- resolve({ output: finalResult || null, error: stderr || `Exit code ${code}`, files: writtenFiles });
3137
+ resolve({ output: finalResult || null, error: stderr || `Exit code ${code}`, files: writtenFiles, toolUsageLog });
2845
3138
  } else {
2846
- resolve({ output: finalResult || '', error: null, files: writtenFiles });
3139
+ resolve({ output: finalResult || '', error: null, files: writtenFiles, toolUsageLog });
2847
3140
  }
2848
3141
  });
2849
3142
 
2850
3143
  child.on('error', (err) => {
2851
3144
  clearTimeout(timer);
2852
- if (chatId) activeProcesses.delete(chatId);
2853
- resolve({ output: null, error: err.message, files: [] });
3145
+ if (chatId) { activeProcesses.delete(chatId); saveActivePids(); } // Fix3
3146
+ resolve({ output: null, error: err.message, files: [], toolUsageLog: [] });
2854
3147
  });
2855
3148
 
2856
3149
  // Write input and close stdin
@@ -2897,7 +3190,7 @@ function lazyDistill() {
2897
3190
  * Shared ask logic โ€” full Claude Code session (stateful, with tools)
2898
3191
  * Now uses spawn (async) instead of execSync to allow parallel requests.
2899
3192
  */
2900
- async function askClaude(bot, chatId, prompt, config) {
3193
+ async function askClaude(bot, chatId, prompt, config, readOnly = false) {
2901
3194
  log('INFO', `askClaude for ${chatId}: ${prompt.slice(0, 50)}`);
2902
3195
  // Trigger background distill on first message / every 4h
2903
3196
  try { lazyDistill(); } catch { /* non-fatal */ }
@@ -2909,9 +3202,9 @@ async function askClaude(bot, chatId, prompt, config) {
2909
3202
  } catch (e) {
2910
3203
  log('ERROR', `Failed to send ack to ${chatId}: ${e.message}`);
2911
3204
  }
2912
- await bot.sendTyping(chatId).catch(() => {});
3205
+ await bot.sendTyping(chatId).catch(() => { });
2913
3206
  const typingTimer = setInterval(() => {
2914
- bot.sendTyping(chatId).catch(() => {});
3207
+ bot.sendTyping(chatId).catch(() => { });
2915
3208
  }, 4000);
2916
3209
 
2917
3210
  // Agent nickname routing: "่ดพ็ปดๆ–ฏ" / "ๅฐ็พŽ๏ผŒๅธฎๆˆ‘..." โ†’ switch project session
@@ -3037,11 +3330,23 @@ async function askClaude(bot, chatId, prompt, config) {
3037
3330
  } catch { /* ignore status update failures */ }
3038
3331
  };
3039
3332
 
3040
- const { output, error, files } = await spawnClaudeStreaming(args, fullPrompt, session.cwd, onStatus, 600000, chatId);
3333
+ const { output, error, files, toolUsageLog } = await spawnClaudeStreaming(args, fullPrompt, session.cwd, onStatus, 600000, chatId);
3041
3334
  clearInterval(typingTimer);
3335
+
3336
+ // Skill evolution: capture signal + hot path heuristic check
3337
+ if (skillEvolution) {
3338
+ try {
3339
+ const signal = skillEvolution.extractSkillSignal(fullPrompt, output, error, files, session.cwd, toolUsageLog);
3340
+ if (signal) {
3341
+ skillEvolution.appendSkillSignal(signal);
3342
+ skillEvolution.checkHotEvolution(signal);
3343
+ }
3344
+ } catch (e) { log('WARN', `Skill evolution signal capture failed: ${e.message}`); }
3345
+ }
3346
+
3042
3347
  // Clean up status message
3043
3348
  if (statusMsgId && bot.deleteMessage) {
3044
- bot.deleteMessage(chatId, statusMsgId).catch(() => {});
3349
+ bot.deleteMessage(chatId, statusMsgId).catch(() => { });
3045
3350
  }
3046
3351
 
3047
3352
  // When Claude completes with no text output (pure tool work), send a done notice
@@ -3113,7 +3418,7 @@ async function askClaude(bot, chatId, prompt, config) {
3113
3418
 
3114
3419
  // Auto-name: if this was the first message and session has no name, generate one
3115
3420
  if (wasNew && !getSessionName(session.id)) {
3116
- autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => {});
3421
+ autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
3117
3422
  }
3118
3423
  } else {
3119
3424
  const errMsg = error || 'Unknown error';
@@ -3184,10 +3489,11 @@ async function startFeishuBridge(config, executeTaskByName) {
3184
3489
  try {
3185
3490
  const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo, senderId) => {
3186
3491
  // Security: check whitelist (empty = deny all) โ€” read live config to support hot-reload
3187
- // Exception: /bind is allowed from any chat so users can self-register new groups
3492
+ // Exception: /bind and /agent bind/new are allowed from any chat so users can self-register new groups
3188
3493
  const liveCfg = loadConfig();
3189
3494
  const allowedIds = (liveCfg.feishu && liveCfg.feishu.allowed_chat_ids) || [];
3190
- const isBindCmd = text && text.trim().startsWith('/bind');
3495
+ const trimmedText = text && text.trim();
3496
+ const isBindCmd = trimmedText && (trimmedText.startsWith('/agent bind') || trimmedText.startsWith('/agent new'));
3191
3497
  if (!allowedIds.includes(chatId) && !isBindCmd) {
3192
3498
  log('WARN', `Feishu: rejected message from ${chatId}`);
3193
3499
  return;
@@ -3284,7 +3590,7 @@ function killExistingDaemon() {
3284
3590
  } catch {
3285
3591
  // Process doesn't exist or already dead
3286
3592
  }
3287
- try { fs.unlinkSync(PID_FILE); } catch {}
3593
+ try { fs.unlinkSync(PID_FILE); } catch { }
3288
3594
  }
3289
3595
 
3290
3596
  function writePid() {
@@ -3346,6 +3652,7 @@ async function main() {
3346
3652
  saveState(state);
3347
3653
 
3348
3654
  log('INFO', `MetaMe daemon started (PID: ${process.pid})`);
3655
+ killOrphanPids(); // Fix3: kill any claude processes left by previous daemon
3349
3656
 
3350
3657
  // Task executor lookup (always reads fresh config)
3351
3658
  function executeTaskByName(name) {
@@ -3427,7 +3734,7 @@ async function main() {
3427
3734
  const r = reloadConfig();
3428
3735
  if (r.success) {
3429
3736
  log('INFO', `Auto-reload OK: ${r.tasks} tasks`);
3430
- notifyFn(`๐Ÿ”„ Config auto-reloaded. ${r.tasks} heartbeat tasks active.`).catch(() => {});
3737
+ notifyFn(`๐Ÿ”„ Config auto-reloaded. ${r.tasks} heartbeat tasks active.`).catch(() => { });
3431
3738
  } else {
3432
3739
  log('ERROR', `Auto-reload failed: ${r.error}`);
3433
3740
  }
@@ -3456,7 +3763,7 @@ async function main() {
3456
3763
 
3457
3764
  // Notify once on startup (single message, no duplicates)
3458
3765
  await sleep(1500); // Let polling settle
3459
- await notifyFn('โœ… Daemon ready.').catch(() => {});
3766
+ await notifyFn('โœ… Daemon ready.').catch(() => { });
3460
3767
 
3461
3768
  // Graceful shutdown
3462
3769
  const shutdown = () => {
@@ -3466,6 +3773,13 @@ async function main() {
3466
3773
  if (heartbeatTimer) clearInterval(heartbeatTimer);
3467
3774
  if (telegramBridge) telegramBridge.stop();
3468
3775
  if (feishuBridge) feishuBridge.stop();
3776
+ // Fix1: kill all tracked claude child processes before exiting
3777
+ for (const [cid, proc] of activeProcesses) {
3778
+ try { proc.child.kill('SIGKILL'); } catch { }
3779
+ log('INFO', `Shutdown: killed claude child for chatId ${cid}`);
3780
+ }
3781
+ activeProcesses.clear();
3782
+ try { if (fs.existsSync(ACTIVE_PIDS_FILE)) fs.unlinkSync(ACTIVE_PIDS_FILE); } catch { }
3469
3783
  cleanPid();
3470
3784
  const s = loadState();
3471
3785
  s.pid = null;