metame-cli 1.5.11 → 1.5.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/index.js +64 -7
  2. package/package.json +3 -2
  3. package/scripts/daemon-agent-commands.js +6 -2
  4. package/scripts/daemon-bridges.js +23 -9
  5. package/scripts/daemon-claude-engine.js +81 -28
  6. package/scripts/daemon-command-router.js +16 -0
  7. package/scripts/daemon-command-session-route.js +3 -1
  8. package/scripts/daemon-engine-runtime.js +1 -5
  9. package/scripts/daemon-message-pipeline.js +113 -44
  10. package/scripts/daemon-reactive-lifecycle.js +405 -9
  11. package/scripts/daemon-session-commands.js +3 -2
  12. package/scripts/daemon-session-store.js +82 -27
  13. package/scripts/daemon-team-dispatch.js +21 -5
  14. package/scripts/daemon-utils.js +3 -1
  15. package/scripts/daemon.js +1 -0
  16. package/scripts/docs/file-transfer.md +1 -0
  17. package/scripts/hooks/intent-file-transfer.js +2 -1
  18. package/scripts/hooks/intent-perpetual.js +109 -0
  19. package/scripts/hooks/intent-research.js +112 -0
  20. package/scripts/intent-registry.js +4 -0
  21. package/scripts/ops-mission-queue.js +258 -0
  22. package/scripts/ops-verifier.js +197 -0
  23. package/skills/agent-browser/SKILL.md +153 -0
  24. package/skills/agent-reach/SKILL.md +66 -0
  25. package/skills/agent-reach/evolution.json +13 -0
  26. package/skills/deep-research/SKILL.md +77 -0
  27. package/skills/find-skills/SKILL.md +133 -0
  28. package/skills/heartbeat-task-manager/SKILL.md +63 -0
  29. package/skills/macos-local-orchestrator/SKILL.md +192 -0
  30. package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
  31. package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
  32. package/skills/macos-mail-calendar/SKILL.md +394 -0
  33. package/skills/mcp-installer/SKILL.md +138 -0
  34. package/skills/skill-creator/LICENSE.txt +202 -0
  35. package/skills/skill-creator/README.md +72 -0
  36. package/skills/skill-creator/SKILL.md +96 -0
  37. package/skills/skill-creator/evolution.json +6 -0
  38. package/skills/skill-creator/references/creation-guide.md +116 -0
  39. package/skills/skill-creator/references/evolution-guide.md +74 -0
  40. package/skills/skill-creator/references/output-patterns.md +82 -0
  41. package/skills/skill-creator/references/workflows.md +28 -0
  42. package/skills/skill-creator/scripts/align_all.py +32 -0
  43. package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
  44. package/skills/skill-creator/scripts/init_skill.py +303 -0
  45. package/skills/skill-creator/scripts/merge_evolution.py +70 -0
  46. package/skills/skill-creator/scripts/package_skill.py +110 -0
  47. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  48. package/skills/skill-creator/scripts/setup.py +141 -0
  49. package/skills/skill-creator/scripts/smart_stitch.py +82 -0
  50. package/skills/skill-manager/SKILL.md +112 -0
  51. package/skills/skill-manager/scripts/delete_skill.py +31 -0
  52. package/skills/skill-manager/scripts/list_skills.py +61 -0
  53. package/skills/skill-manager/scripts/scan_and_check.py +125 -0
  54. package/skills/skill-manager/scripts/sync_index.py +144 -0
  55. package/skills/skill-manager/scripts/update_helper.py +39 -0
package/index.js CHANGED
@@ -412,16 +412,21 @@ try {
412
412
  }
413
413
  } catch { /* non-fatal */ }
414
414
 
415
- // Worktree guard: team members running in worktrees must NEVER deploy to ~/.metame/
416
- // Their worktree is an isolated sandbox deploying would overwrite production symlinks.
417
- // Detect any .worktrees/ parent in the path (covers both ~/.metame/worktrees/ and repo-local .worktrees/).
418
- const _isInWorktree = __dirname.split(path.sep).includes('.worktrees') ||
419
- __dirname.startsWith(path.join(HOME_DIR, '.metame', 'worktrees'));
415
+ // Worktree guard: worktrees must NEVER deploy to ~/.metame/ — they are isolated sandboxes.
416
+ // Detection: git worktrees have a .git FILE (pointing to main repo), not a .git DIRECTORY.
417
+ // This is reliable regardless of worktree path conventions.
418
+ const _dotGitPath = path.join(__dirname, '.git');
419
+ const _isInWorktree = (() => {
420
+ try {
421
+ const stat = fs.statSync(_dotGitPath);
422
+ return stat.isFile(); // .git is a file → worktree; directory → main repo; missing → npm install
423
+ } catch { return false; }
424
+ })();
420
425
  if (_isInWorktree) {
421
426
  console.error(`\n${icon("stop")} ACTION BLOCKED: Worktree Deploy Prevented`);
422
- console.error(` You are running from a worktree (${path.basename(__dirname)}).`);
427
+ console.error(` You are running from a git worktree (${path.basename(__dirname)}).`);
423
428
  console.error(' Deploying from a worktree would overwrite production daemon code.');
424
- console.error(' Use \x1b[36mtouch ~/.metame/daemon.js\x1b[0m to hot-reload instead.\n');
429
+ console.error(' Commit your changes, then deploy from the main repo.\n');
425
430
  process.exit(1);
426
431
  }
427
432
 
@@ -695,6 +700,58 @@ function ensureHookInstalled() {
695
700
 
696
701
  ensureHookInstalled();
697
702
 
703
+ // ---------------------------------------------------------
704
+ // 1.6a AUTO-ENABLE BUNDLED PLUGINS
705
+ // ---------------------------------------------------------
706
+ function ensurePluginsEnabled() {
707
+ try {
708
+ let settings = {};
709
+ if (fs.existsSync(CLAUDE_SETTINGS)) {
710
+ settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, 'utf8'));
711
+ }
712
+
713
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
714
+ if (!settings.extraKnownMarketplaces) settings.extraKnownMarketplaces = {};
715
+
716
+ const bundledPlugins = {
717
+ 'example-skills@anthropic-agent-skills': true,
718
+ 'ralph-loop@claude-plugins-official': true,
719
+ 'planning-with-files@planning-with-files': true,
720
+ };
721
+
722
+ const bundledMarketplaces = {
723
+ 'planning-with-files': {
724
+ source: { source: 'github', repo: 'OthmanAdi/planning-with-files' },
725
+ },
726
+ };
727
+
728
+ let modified = false;
729
+
730
+ for (const [key, val] of Object.entries(bundledPlugins)) {
731
+ if (!(key in settings.enabledPlugins)) {
732
+ settings.enabledPlugins[key] = val;
733
+ modified = true;
734
+ }
735
+ }
736
+
737
+ for (const [key, val] of Object.entries(bundledMarketplaces)) {
738
+ if (!(key in settings.extraKnownMarketplaces)) {
739
+ settings.extraKnownMarketplaces[key] = val;
740
+ modified = true;
741
+ }
742
+ }
743
+
744
+ if (modified) {
745
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2), 'utf8');
746
+ console.log(`${icon("brain")} MetaMe: Bundled plugins enabled.`);
747
+ }
748
+ } catch {
749
+ // Non-fatal
750
+ }
751
+ }
752
+
753
+ ensurePluginsEnabled();
754
+
698
755
  // ---------------------------------------------------------
699
756
  // 1.6b LOCAL ACTIVITY HEARTBEAT
700
757
  // ---------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.11",
3
+ "version": "1.5.13",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -13,7 +13,8 @@
13
13
  "!scripts/test_daemon.js",
14
14
  "!scripts/hooks/test-*.js",
15
15
  "!scripts/daemon.yaml",
16
- "!scripts/daemon.yaml.bak"
16
+ "!scripts/daemon.yaml.bak",
17
+ "skills/"
17
18
  ],
18
19
  "scripts": {
19
20
  "test": "node --test scripts/*.test.js",
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+
3
5
  function createAgentCommandHandler(deps) {
4
6
  const {
5
7
  fs,
@@ -34,11 +36,11 @@ function createAgentCommandHandler(deps) {
34
36
  agentFlowTtlMs,
35
37
  agentBindTtlMs,
36
38
  getDefaultEngine = () => 'claude',
39
+ log = () => {},
37
40
  } = deps;
38
41
 
39
42
  function normalizeEngineName(name) {
40
- const n = String(name || '').trim().toLowerCase();
41
- return n === 'codex' ? 'codex' : getDefaultEngine();
43
+ return _normalizeEngine(name, getDefaultEngine);
42
44
  }
43
45
 
44
46
  function inferStoredEngine(rawSession) {
@@ -358,7 +360,9 @@ function createAgentCommandHandler(deps) {
358
360
  const curSession = getSession(route.sessionChatId) || getSession(chatId);
359
361
  const curCwd = route.cwd || (curSession ? curSession.cwd : null);
360
362
  const currentEngine = getCurrentEngine(chatId);
363
+ log('DEBUG', `[/resume] chatId=${chatId} curCwd=${curCwd} engine=${currentEngine} route.sessionChatId=${route.sessionChatId}`);
361
364
  const recentSessions = listRecentSessions(5, curCwd, currentEngine);
365
+ log('DEBUG', `[/resume] recentSessions=${recentSessions.length} ids=[${recentSessions.map(s=>s.sessionId.slice(0,8)).join(',')}]`);
362
366
  const resumeChoices = buildResumeChoices({
363
367
  recentSessions,
364
368
  currentLogical,
@@ -767,6 +767,7 @@ function createBridgeStarter(deps) {
767
767
  log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
768
768
  const parentId = extractFeishuReplyMessageId(event);
769
769
  let _replyAgentKey = null;
770
+ let _replyMappingFound = false; // true = mapping exists (agentKey may be null = main)
770
771
  // Load state once for the entire routing block
771
772
  const _st = loadState();
772
773
  if (parentId) {
@@ -775,6 +776,7 @@ function createBridgeStarter(deps) {
775
776
  if (parentId) {
776
777
  const mapped = _st.msg_sessions && _st.msg_sessions[parentId];
777
778
  if (mapped) {
779
+ _replyMappingFound = true;
778
780
  if (typeof restoreSessionFromReply === 'function') {
779
781
  restoreSessionFromReply(chatId, mapped);
780
782
  } else {
@@ -852,16 +854,28 @@ function createBridgeStarter(deps) {
852
854
  // Bare /stop, no sticky set → fall through to handleCommand
853
855
  }
854
856
 
855
- // 0. Quoted reply → force route + set sticky
856
- if (_replyAgentKey) {
857
- const member = _boundProj.team.find(m => m.key === _replyAgentKey);
858
- if (member) {
859
- _setSticky(member.key);
860
- log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
861
- _dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
862
- return;
857
+ // 0. Quoted reply → force route based on which agent sent the parent message.
858
+ // Cases:
859
+ // a) agentKey = known team member route to that member (set sticky)
860
+ // b) agentKey = null, mapping found → user replied to main; clear sticky, route to main
861
+ // c) parentId present, no mapping → intent is explicit, avoid sticky; clear sticky, route to main
862
+ if (parentId) {
863
+ if (_replyAgentKey) {
864
+ const member = _boundProj.team.find(m => m.key === _replyAgentKey);
865
+ if (member) {
866
+ _setSticky(member.key);
867
+ log('INFO', `Quoted reply → force route to ${_replyAgentKey} (sticky set)`);
868
+ _dispatchToTeamMember(member, _boundProj, trimmedText, liveCfg, bot, chatId, executeTaskByName, acl);
869
+ return;
870
+ }
871
+ // agentKey set but not a current team member → fall through to main
872
+ log('INFO', `Quoted reply agentKey=${_replyAgentKey} not in team, routing to main`);
863
873
  }
864
- log('INFO', `Quoted reply agentKey=${_replyAgentKey} not in team, falling through`);
874
+ // Cases b & c: no agentKey (main agent) or stale/unknown agentKey
875
+ _clearSticky();
876
+ log('INFO', `Quoted reply → route to main (agentKey=${_replyAgentKey} mappingFound=${_replyMappingFound})`);
877
+ await pipeline.processMessage(chatId, trimmedText, { bot, config: liveCfg, executeTaskByName, senderId: acl.senderId, readOnly: acl.readOnly });
878
+ return;
865
879
  }
866
880
  // 1. Explicit nickname → route + set sticky
867
881
  const teamMatch = _findTeamMember(trimmedText, _boundProj.team);
@@ -106,7 +106,15 @@ function createClaudeEngine(deps) {
106
106
  }
107
107
  // Card reuse for merge-pause: when a task is paused for message merging,
108
108
  // save the statusMsgId so the next askClaude reuses the same card.
109
- const _pausedCards = new Map(); // chatId -> { statusMsgId, cardHeader }
109
+ // Entries auto-expire via periodic sweep (60s) to prevent unbounded growth.
110
+ const _pausedCards = new Map(); // chatId -> { statusMsgId, cardHeader, savedAt }
111
+ const _PAUSED_CARD_TTL = 60000;
112
+ setInterval(() => {
113
+ const now = Date.now();
114
+ for (const [k, v] of _pausedCards) {
115
+ if (now - (v.savedAt || 0) > _PAUSED_CARD_TTL) _pausedCards.delete(k);
116
+ }
117
+ }, _PAUSED_CARD_TTL).unref();
110
118
 
111
119
  let mentorEngine = null;
112
120
  try { mentorEngine = require('./mentor-engine'); } catch { /* optional */ }
@@ -239,6 +247,7 @@ function createClaudeEngine(deps) {
239
247
  && !!error
240
248
  && (!output || !!errorCode)
241
249
  && failureKind !== 'user-stop'
250
+ && failureKind !== 'merge-pause'
242
251
  && !!canRetry;
243
252
  }
244
253
 
@@ -481,6 +490,13 @@ function createClaudeEngine(deps) {
481
490
  retryPromptPrefix: '',
482
491
  };
483
492
  }
493
+ if (code === 'INTERRUPTED_MERGE_PAUSE' || lowered.includes('paused for merge')) {
494
+ return {
495
+ kind: 'merge-pause',
496
+ userMessage: '',
497
+ retryPromptPrefix: '',
498
+ };
499
+ }
484
500
  const interrupted = (
485
501
  lowered.includes('stopped by user')
486
502
  || lowered.includes('interrupted')
@@ -1275,9 +1291,9 @@ function createClaudeEngine(deps) {
1275
1291
  };
1276
1292
  const _ackBoundKey = _ackAgentMap[_ackChatIdStr] || projectKeyFromVirtualChatId(_ackChatIdStr);
1277
1293
  const _ackBoundProj = _ackBoundKey && config.projects ? config.projects[_ackBoundKey] : null;
1278
- // _ackCardHeader: non-null for agents with icon/name (team members, dispatch); passed to editMessage to preserve header on streaming edits
1279
- let _ackCardHeader = (_ackBoundProj && _ackBoundProj.icon && _ackBoundProj.name)
1280
- ? { title: `${_ackBoundProj.icon} ${_ackBoundProj.name}`, color: _ackBoundProj.color || 'blue' }
1294
+ // _ackCardHeader: non-null for bound projects with a name; passed to editMessage to preserve header on streaming edits
1295
+ let _ackCardHeader = (_ackBoundProj && _ackBoundProj.name)
1296
+ ? { title: `${_ackBoundProj.icon || '🤖'} ${_ackBoundProj.name}`, color: _ackBoundProj.color || 'blue' }
1281
1297
  : null;
1282
1298
  // Reuse card from a paused merge (same card, no new push)
1283
1299
  const _pausedCard = _pausedCards.get(chatId);
@@ -1287,7 +1303,6 @@ function createClaudeEngine(deps) {
1287
1303
  const cardAge = _pausedCard.savedAt ? Date.now() - _pausedCard.savedAt : 0;
1288
1304
  if (cardAge > 30000) {
1289
1305
  log('INFO', `[askClaude] Discarding stale paused card for ${chatId} (${Math.round(cardAge / 1000)}s old)`);
1290
- if (_pausedCard.statusMsgId && bot.deleteMessage) bot.deleteMessage(chatId, _pausedCard.statusMsgId).catch(() => {});
1291
1306
  } else {
1292
1307
  statusMsgId = _pausedCard.statusMsgId;
1293
1308
  if (_pausedCard.cardHeader) _ackCardHeader = _pausedCard.cardHeader;
@@ -1365,6 +1380,10 @@ function createClaudeEngine(deps) {
1365
1380
  const sessionRaw = getSession(sessionChatId);
1366
1381
  const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
1367
1382
  const boundEngineName = (boundProject && boundProject.engine) ? normalizeEngineName(boundProject.engine) : getDefaultEngine();
1383
+ // effectiveCwd: single source of truth for this request's working directory.
1384
+ // For bound projects, config always wins over stored session cwd.
1385
+ // Resolved once here; all downstream createSession/spawn calls use this.
1386
+ let effectiveCwd = boundCwd || null;
1368
1387
 
1369
1388
  // Engine is determined from config only — bound agent config wins, then global default.
1370
1389
  const engineName = normalizeEngineName(
@@ -1406,6 +1425,14 @@ function createClaudeEngine(deps) {
1406
1425
  let session = resolveSessionForEngine(sessionChatId, engineName) || { cwd: boundCwd || HOME, engine: engineName, id: null, started: false };
1407
1426
  session.engine = engineName; // keep local copy for Codex resume detection below
1408
1427
  session.logicalChatId = sessionChatId;
1428
+ // Finalize effectiveCwd: bound config > stored session > HOME
1429
+ if (!effectiveCwd) effectiveCwd = (session && session.cwd) || HOME;
1430
+ // Correct stored cwd if it drifted from config (e.g., stale state from prior bug)
1431
+ if (session.cwd !== effectiveCwd) {
1432
+ log('WARN', `[SessionCwd] correcting session cwd for ${sessionChatId}: ${session.cwd || 'unknown'} -> ${effectiveCwd}`);
1433
+ session = { ...session, cwd: effectiveCwd };
1434
+ await patchSessionSerialized(sessionChatId, (cur) => ({ ...cur, cwd: effectiveCwd }));
1435
+ }
1409
1436
 
1410
1437
  // Warm pool: check if a persistent process is available for this session (Claude only).
1411
1438
  // Declared early so downstream logic can skip expensive operations when reusing warm process.
@@ -1425,7 +1452,7 @@ function createClaudeEngine(deps) {
1425
1452
  }
1426
1453
  session = createSession(
1427
1454
  sessionChatId,
1428
- session.cwd,
1455
+ effectiveCwd,
1429
1456
  boundProject && boundProject.name ? boundProject.name : '',
1430
1457
  engineName,
1431
1458
  engineName === 'codex' ? requestedCodexPermissionProfile : undefined
@@ -1492,7 +1519,7 @@ function createClaudeEngine(deps) {
1492
1519
  const resumeInspection = inspectClaudeResumeSession(session, model);
1493
1520
  if (resumeInspection.shouldResume === false) {
1494
1521
  log('INFO', `[ModelPin] session ${session.id.slice(0, 8)} flagged as ${resumeInspection.reason}; starting fresh Claude session`);
1495
- session = createSession(sessionChatId, session.cwd, boundProject && boundProject.name ? boundProject.name : '', runtime.name);
1522
+ session = createSession(sessionChatId, effectiveCwd, boundProject && boundProject.name ? boundProject.name : '', runtime.name);
1496
1523
  } else if (resumeInspection.modelPin) {
1497
1524
  if (resumeInspection.modelPin !== model) {
1498
1525
  log('INFO', `[ModelPin] resuming ${session.id.slice(0, 8)} with original model ${resumeInspection.modelPin} (configured: ${model})`);
@@ -1546,6 +1573,8 @@ function createClaudeEngine(deps) {
1546
1573
  const memory = require('./memory');
1547
1574
 
1548
1575
  // L1: NOW.md per-agent whiteboard injection(按 projectKey 隔离,防并发冲突)
1576
+ // One-shot: inject once then clear, same pattern as compactContext.
1577
+ // Prevents re-injection on daemon restart or new session for the same chat.
1549
1578
  if (!session.started) {
1550
1579
  try {
1551
1580
  const nowDir = path.join(HOME, '.metame', 'memory', 'now');
@@ -1555,6 +1584,8 @@ function createClaudeEngine(deps) {
1555
1584
  const nowContent = fs.readFileSync(nowPath, 'utf8').trim();
1556
1585
  if (nowContent) {
1557
1586
  memoryHint += `\n\n[Current task context:\n${nowContent}]`;
1587
+ // Clear after injection to prevent re-triggering on next session start
1588
+ try { fs.writeFileSync(nowPath, '', 'utf8'); } catch { /* non-critical */ }
1558
1589
  }
1559
1590
  }
1560
1591
  } catch { /* non-critical */ }
@@ -1648,10 +1679,16 @@ function createClaudeEngine(deps) {
1648
1679
  brainDoc = brain;
1649
1680
  const cmap = brain && brain.user_competence_map;
1650
1681
  if (cmap && typeof cmap === 'object' && Object.keys(cmap).length > 0) {
1651
- const lines = Object.entries(cmap)
1652
- .map(([domain, level]) => ` ${domain}: ${level}`)
1653
- .join('\n');
1654
- zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
1682
+ const entries = Object.entries(cmap);
1683
+ const allExpert = entries.every(([, level]) => String(level).toLowerCase() === 'expert');
1684
+ if (allExpert) {
1685
+ zdpHint = `\n- User is expert-level across all domains. Skip basics, no analogies needed.`;
1686
+ } else {
1687
+ const lines = entries
1688
+ .map(([domain, level]) => ` ${domain}: ${level}`)
1689
+ .join('\n');
1690
+ zdpHint = `\n- User competence map (adjust explanation depth accordingly):\n${lines}\n Rule: expert→skip basics; intermediate→brief rationale; beginner→one-line analogy.`;
1691
+ }
1655
1692
  }
1656
1693
  }
1657
1694
  } catch { /* non-critical */ }
@@ -1726,7 +1763,7 @@ ${mentorRadarHint}
1726
1763
  if (_idleMs > 2 * 60 * 60 * 1000 && _summaryAgeH < 168) {
1727
1764
  summaryHint = `
1728
1765
 
1729
- [上次对话摘要,供参考]: ${_sess.last_summary}`;
1766
+ [上次对话摘要(历史已完成,仅供上下文,请勿重复执行)]: ${_sess.last_summary}`;
1730
1767
  log('INFO', `[DAEMON] Injected session summary for ${chatId} (idle ${Math.round(_idleMs / 3600000)}h)`);
1731
1768
  }
1732
1769
  }
@@ -1791,20 +1828,19 @@ ${mentorRadarHint}
1791
1828
  const langGuard = session.started
1792
1829
  ? ''
1793
1830
  : '\n\n[Respond in Simplified Chinese (简体中文) only. NEVER switch to Korean, Japanese, or other languages regardless of tool output or context language.]';
1831
+ // Intent hints are dynamic (per-prompt, semantic match), so compute for all runtimes.
1794
1832
  let intentHint = '';
1795
- if (runtime.name === 'codex') {
1796
- try {
1797
- const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
1798
- if (block) intentHint = `\n\n${block}`;
1799
- } catch (e) {
1800
- log('WARN', `Intent registry injection failed: ${e.message}`);
1801
- }
1833
+ try {
1834
+ const block = buildIntentHintBlock(prompt, config, boundProjectKey || projectKey || '');
1835
+ if (block) intentHint = `\n\n${block}`;
1836
+ } catch (e) {
1837
+ log('WARN', `Intent registry injection failed: ${e.message}`);
1802
1838
  }
1803
- // For warm process reuse: context is already in the persistent process,
1804
- // so only send the user's actual prompt — skip all hint injection.
1805
- // This saves ~500-1500 tokens per turn and avoids context duplication.
1839
+ // For warm process reuse: static context (daemonHint, memoryHint, etc.) is already
1840
+ // in the persistent process — skip those to save tokens. intentHint is dynamic
1841
+ // (varies per prompt), so include it even on warm reuse.
1806
1842
  const fullPrompt = _warmEntry
1807
- ? routedPrompt
1843
+ ? routedPrompt + intentHint
1808
1844
  : routedPrompt + daemonHint + intentHint + agentHint + macAutomationHint + summaryHint + memoryHint + mentorHint + langGuard;
1809
1845
  if (runtime.name === 'codex' && session.started && session.id && requestedCodexPermissionProfile) {
1810
1846
  const actualPermissionProfile = getActualCodexPermissionProfile(session);
@@ -1941,17 +1977,24 @@ ${mentorRadarHint}
1941
1977
  ...(runtime.name === 'codex' ? { runtimeSessionObserved: true } : {}),
1942
1978
  ...(runtime.name === 'codex' ? actualPermissionProfile : {}),
1943
1979
  };
1944
- return { ...cur, cwd: session.cwd || cur.cwd || HOME, engines };
1980
+ return { ...cur, cwd: effectiveCwd || cur.cwd || HOME, engines };
1945
1981
  });
1946
1982
  if (runtime.name === 'codex' && wasStarted && prevSessionId && prevSessionId !== safeNextId && prevSessionId !== '__continue__') {
1947
1983
  log('WARN', `Codex thread migrated for ${chatId}: ${prevSessionId.slice(0, 8)} -> ${safeNextId.slice(0, 8)}`);
1948
1984
  }
1985
+ // Keep card header in sync with the real session ID reported by the engine
1986
+ if (_ackCardHeader && _ackCardHeader._baseTitle) {
1987
+ _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${safeNextId.slice(0, 8)})` };
1988
+ }
1949
1989
  };
1950
1990
 
1951
1991
  // Check if user cancelled during pre-spawn phase (sentinel was marked aborted)
1952
1992
  // Stamp session ID on card header so user can track session continuity
1993
+ if (_ackCardHeader) {
1994
+ _ackCardHeader._baseTitle = _ackCardHeader.title; // preserve original title for onSession updates
1995
+ }
1953
1996
  if (session && session.id && _ackCardHeader) {
1954
- _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader.title}(${session.id.slice(0, 8)})` };
1997
+ _ackCardHeader = { ..._ackCardHeader, title: `${_ackCardHeader._baseTitle}(${session.id.slice(0, 8)})` };
1955
1998
  }
1956
1999
 
1957
2000
  const _preSentinel = activeProcesses.get(chatId);
@@ -2018,7 +2061,7 @@ ${mentorRadarHint}
2018
2061
  );
2019
2062
  session = createSession(
2020
2063
  sessionChatId,
2021
- session.cwd,
2064
+ effectiveCwd,
2022
2065
  boundProject && boundProject.name ? boundProject.name : '',
2023
2066
  'codex',
2024
2067
  requestedCodexPermissionProfile
@@ -2095,7 +2138,7 @@ ${mentorRadarHint}
2095
2138
  if (resumeFailure.kind !== 'interrupted') {
2096
2139
  session = createSession(
2097
2140
  sessionChatId,
2098
- session.cwd,
2141
+ effectiveCwd,
2099
2142
  boundProject && boundProject.name ? boundProject.name : '',
2100
2143
  'codex',
2101
2144
  requestedCodexPermissionProfile
@@ -2203,6 +2246,16 @@ ${mentorRadarHint}
2203
2246
  return { ok: true };
2204
2247
  }
2205
2248
 
2249
+ // Merge-pause with partial output: save card for reuse, discard partial output
2250
+ if (output && errorCode === 'INTERRUPTED_MERGE_PAUSE') {
2251
+ if (statusMsgId) {
2252
+ _pausedCards.set(chatId, { statusMsgId, cardHeader: _ackCardHeader, savedAt: Date.now() });
2253
+ if (bot.editMessage) bot.editMessage(chatId, statusMsgId, '⏸ 合并中…', _ackCardHeader).catch(() => {});
2254
+ log('INFO', `[askClaude] Merge-pause with partial output, saved card ${statusMsgId} for ${chatId}`);
2255
+ }
2256
+ return { ok: false, error: 'Paused for merge', errorCode };
2257
+ }
2258
+
2206
2259
  if (output) {
2207
2260
  if (runtime.name === 'codex') {
2208
2261
  _codexResumeRetryTs.delete(getCodexResumeRetryKey(chatId, 'interrupted'));
@@ -2403,7 +2456,7 @@ ${mentorRadarHint}
2403
2456
  if (runtime.name === 'claude' && _isSessionResumeFail) {
2404
2457
  const _reason = errMsg.includes('already in use') ? 'locked' : _isThinkingSignatureError ? 'thinking-signature-invalid' : 'not found';
2405
2458
  log('WARN', `Session ${session.id} unusable (${_reason}), creating new`);
2406
- session = createSession(sessionChatId, session.cwd, '', runtime.name);
2459
+ session = createSession(sessionChatId, effectiveCwd, '', runtime.name);
2407
2460
 
2408
2461
  const retryArgs = runtime.buildArgs({
2409
2462
  model,
@@ -643,6 +643,19 @@ function createCommandRouter(deps) {
643
643
  return;
644
644
  }
645
645
 
646
+ // /btw — quick side question (read-only, concise, bypasses cooldown)
647
+ if (/^\/btw(\s|$)/i.test(text)) {
648
+ const btwQuestion = text.replace(/^\/btw\s*/i, '').trim();
649
+ if (!btwQuestion) {
650
+ await bot.sendMessage(chatId, '用法: /btw <问题>\n快速提问,不影响主会话节奏');
651
+ return;
652
+ }
653
+ const btwPrompt = `[Side question — answer concisely from existing context, no need for tools]\n\n${btwQuestion}`;
654
+ resetCooldown(chatId);
655
+ await askClaude(bot, chatId, btwPrompt, config, true, senderId);
656
+ return;
657
+ }
658
+
646
659
  if (text.startsWith('/')) {
647
660
  const currentModel = (config.daemon && config.daemon.model) || 'opus';
648
661
  const currentProvider = providerMod ? providerMod.getActiveName() : 'anthropic';
@@ -664,6 +677,9 @@ function createCommandRouter(deps) {
664
677
  '/agent reset — 重置当前 Agent 角色',
665
678
  '/agent soul [repair] — 查看/修复 Agent Soul 身份层',
666
679
  '',
680
+ '💬 快捷:',
681
+ '/btw <问题> — 快速旁白提问(只读,不打断主任务)',
682
+ '',
667
683
  '📂 Session 管理:',
668
684
  '/new [path] [name] — 新建会话',
669
685
  '/sessions — 浏览所有最近会话',
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
4
+
3
5
  function createCommandSessionResolver(deps) {
4
6
  const {
5
7
  path,
@@ -11,7 +13,7 @@ function createCommandSessionResolver(deps) {
11
13
  } = deps;
12
14
 
13
15
  function normalizeEngineName(name) {
14
- return String(name || '').trim().toLowerCase() === 'codex' ? 'codex' : getDefaultEngine();
16
+ return _normalizeEngine(name, getDefaultEngine);
15
17
  }
16
18
 
17
19
  function inferStoredEngine(rawSession) {
@@ -4,6 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync } = require('child_process');
7
+ const { normalizeEngineName } = require('./daemon-utils');
7
8
 
8
9
  const CODEX_TOOL_MAP = Object.freeze({
9
10
  command_execution: 'Bash',
@@ -14,11 +15,6 @@ const CODEX_TOOL_MAP = Object.freeze({
14
15
  web_fetch: 'WebFetch',
15
16
  });
16
17
 
17
- function normalizeEngineName(name) {
18
- const text = String(name || '').trim().toLowerCase();
19
- return text === 'codex' ? 'codex' : 'claude';
20
- }
21
-
22
18
  function resolveBinary(engineName, deps = {}) {
23
19
  const engine = normalizeEngineName(engineName);
24
20
  const home = deps.HOME || os.homedir();