metame-cli 1.5.5 → 1.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.5.5",
3
+ "version": "1.5.6",
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": {
@@ -17,7 +17,8 @@
17
17
  "test": "node --test scripts/*.test.js",
18
18
  "test:daemon-status": "node --test scripts/daemon-restart-status.test.js",
19
19
  "start": "node index.js",
20
- "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-command-session-route.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-engine-runtime.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-write.js scripts/memory-extract.js scripts/memory-search.js scripts/memory-gc.js scripts/memory-nightly-reflect.js scripts/memory-index.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/mentor-engine.js scripts/skill-evolution.js scripts/skill-changelog.js scripts/agent-layer.js scripts/self-reflect.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
20
+ "push": "bash scripts/bin/push-clean.sh",
21
+ "sync:plugin": "cp scripts/platform.js scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-command-session-route.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-engine-runtime.js scripts/daemon-command-router.js scripts/daemon-user-acl.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/daemon-team-dispatch.js scripts/daemon-dispatch-cards.js scripts/daemon-remote-dispatch.js scripts/daemon-siri-bridge.js scripts/daemon-siri-imessage.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-write.js scripts/memory-extract.js scripts/memory-search.js scripts/memory-gc.js scripts/memory-nightly-reflect.js scripts/memory-index.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/mentor-engine.js scripts/skill-evolution.js scripts/skill-changelog.js scripts/agent-layer.js scripts/self-reflect.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo 'Plugin scripts synced'",
21
22
  "sync:readme": "node scripts/sync-readme.js",
22
23
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '鈿狅笍 Daemon not running or restart failed'",
23
24
  "precommit": "npm run sync:plugin && npm run restart:daemon"
@@ -16,7 +16,7 @@ const crypto = require('crypto');
16
16
  const os = require('os');
17
17
  const { socketPath } = require('../platform');
18
18
  const yaml = require('../resolve-yaml');
19
- const { buildEnrichedPrompt, buildTeamRosterHint } = require('../team-dispatch');
19
+ const { buildEnrichedPrompt, buildTeamRosterHint } = require('../daemon-team-dispatch');
20
20
  const {
21
21
  parseRemoteTargetRef,
22
22
  normalizeRemoteDispatchConfig,
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # push-clean.sh — Push to remote, stripping local-only checkpoint/safety commits.
3
+ #
4
+ # Usage: npm run push
5
+ #
6
+ # What it does:
7
+ # 1. Collects non-checkpoint commits between origin/main and HEAD (in order).
8
+ # 2. If none exist → plain `git push` (nothing to strip).
9
+ # 3. Otherwise → cherry-picks them onto a temp branch based at origin/main,
10
+ # pushes that branch as origin/main, then resets local main to match remote.
11
+
12
+ set -euo pipefail
13
+
14
+ REMOTE="${METAME_PUSH_REMOTE:-origin}"
15
+ BRANCH="${METAME_PUSH_BRANCH:-main}"
16
+ TEMP_BRANCH="_push-clean-$(date +%s)"
17
+
18
+ upstream="$REMOTE/$BRANCH"
19
+
20
+ # ── 1. Fetch so we have an up-to-date upstream ref ────────────────────────────
21
+ echo "[push] Fetching $upstream …"
22
+ git fetch "$REMOTE" "$BRANCH" --quiet
23
+
24
+ # ── 2. Collect ALL commits ahead of upstream (oldest first) ──────────────────
25
+ all_commits=()
26
+ while IFS= read -r sha; do
27
+ [ -n "$sha" ] && all_commits+=("$sha")
28
+ done < <(git log --reverse --format="%H" "$upstream"..HEAD 2>/dev/null)
29
+
30
+ if [ ${#all_commits[@]} -eq 0 ]; then
31
+ echo "[push] Nothing ahead of $upstream — already up to date."
32
+ exit 0
33
+ fi
34
+
35
+ # ── 3. Filter out checkpoint/safety commits ───────────────────────────────────
36
+ clean_commits=()
37
+ cp_count=0
38
+ for sha in "${all_commits[@]}"; do
39
+ subject=$(git log -1 --format="%s" "$sha")
40
+ if echo "$subject" | grep -qE '^\[metame-checkpoint\]|^\[metame-safety\]'; then
41
+ cp_count=$((cp_count + 1))
42
+ echo "[push] Skipping checkpoint: $sha ${subject:0:60}"
43
+ else
44
+ clean_commits+=("$sha")
45
+ fi
46
+ done
47
+
48
+ if [ ${#clean_commits[@]} -eq 0 ]; then
49
+ echo "[push] Only checkpoint commits ahead — nothing to push."
50
+ exit 0
51
+ fi
52
+
53
+ echo "[push] Pushing ${#clean_commits[@]} commit(s) (skipping $cp_count checkpoint(s)) …"
54
+
55
+ # ── 4. Cherry-pick onto a temp branch at upstream ────────────────────────────
56
+ git checkout -q -b "$TEMP_BRANCH" "$upstream"
57
+
58
+ for sha in "${clean_commits[@]}"; do
59
+ subject=$(git log -1 --format="%s" "$sha")
60
+ echo "[push] cherry-pick $sha ${subject:0:60}"
61
+ git cherry-pick --allow-empty --keep-redundant-commits "$sha" --quiet
62
+ done
63
+
64
+ # ── 5. Push temp branch → remote main ────────────────────────────────────────
65
+ git push "$REMOTE" "$TEMP_BRANCH:$BRANCH"
66
+
67
+ # ── 6. Reset local main to match remote (fast-forward) ───────────────────────
68
+ git checkout -q "$BRANCH"
69
+ git reset --hard "$REMOTE/$BRANCH"
70
+ git branch -D "$TEMP_BRANCH"
71
+
72
+ echo "[push] Done. Local $BRANCH is now aligned with $REMOTE/$BRANCH."
@@ -7,7 +7,7 @@ const {
7
7
  } = require('./usage-classifier');
8
8
  const { IS_WIN } = require('./platform');
9
9
  const { ENGINE_MODEL_CONFIG, resolveEngineModel } = require('./daemon-engine-runtime');
10
- const { resolveProjectKey: _resolveProjectKey } = require('./team-dispatch');
10
+ const { resolveProjectKey: _resolveProjectKey } = require('./daemon-team-dispatch');
11
11
  const {
12
12
  parseRemoteTargetRef,
13
13
  getRemoteDispatchStatus,
@@ -47,7 +47,7 @@ function createAdminCommandHandler(deps) {
47
47
  getDistillModel = () => 'haiku',
48
48
  } = deps;
49
49
 
50
- // resolveProjectKey: imported from team-dispatch.js (shared with dispatch_to and daemon.js)
50
+ // resolveProjectKey: imported from daemon-team-dispatch.js (shared with dispatch_to and daemon.js)
51
51
  const resolveProjectKey = _resolveProjectKey;
52
52
 
53
53
  /**
@@ -2,7 +2,7 @@
2
2
 
3
3
  let userAcl = null;
4
4
  try { userAcl = require('./daemon-user-acl'); } catch { /* optional */ }
5
- const { findTeamMember: _findTeamMember } = require('./team-dispatch');
5
+ const { findTeamMember: _findTeamMember } = require('./daemon-team-dispatch');
6
6
  const { isRemoteMember } = require('./daemon-remote-dispatch');
7
7
  const imessageIO = (() => { try { return require('./daemon-siri-imessage'); } catch { return null; } })();
8
8
  const siriBridgeMod = (() => { try { return require('./daemon-siri-bridge'); } catch { return null; } })();
@@ -163,7 +163,7 @@ function createBridgeStarter(deps) {
163
163
  const proj = key && cfg.projects ? cfg.projects[key] : null;
164
164
  return { key: key || null, project: proj || null };
165
165
  }
166
- // _findTeamMember is imported from team-dispatch.js (shared with admin-commands)
166
+ // _findTeamMember is imported from daemon-team-dispatch.js (shared with admin-commands)
167
167
 
168
168
  // Creates a bot proxy that redirects all send methods to replyChatId
169
169
  function _createTeamProxyBot(bot, replyChatId) {
@@ -421,6 +421,19 @@ function createBridgeStarter(deps) {
421
421
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
422
422
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
423
423
 
424
+ // Respect team_sticky: route to active agent same as text messages
425
+ const _stFile = loadState();
426
+ const _chatKeyFile = String(chatId);
427
+ const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
428
+ const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
429
+ if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
430
+ const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
431
+ if (_stickyMember) {
432
+ log('INFO', `Telegram file → sticky route to ${_stickyKeyFile}`);
433
+ _dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot, chatId, executeTaskByName, acl);
434
+ continue;
435
+ }
436
+ }
424
437
  handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly).catch(e => {
425
438
  log('ERROR', `Telegram file handler error: ${e.message}`);
426
439
  });
@@ -701,6 +714,19 @@ function createBridgeStarter(deps) {
701
714
  ? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
702
715
  : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
703
716
 
717
+ // Respect team_sticky: route to active agent same as text messages
718
+ const _stFile = loadState();
719
+ const _chatKeyFile = String(chatId);
720
+ const { project: _boundProjFile } = _getBoundProject(chatId, liveCfg);
721
+ const _stickyKeyFile = (_stFile.team_sticky || {})[_chatKeyFile];
722
+ if (_boundProjFile && Array.isArray(_boundProjFile.team) && _boundProjFile.team.length > 0 && _stickyKeyFile) {
723
+ const _stickyMember = _boundProjFile.team.find(m => m.key === _stickyKeyFile);
724
+ if (_stickyMember) {
725
+ log('INFO', `Feishu file → sticky route to ${_stickyKeyFile}`);
726
+ _dispatchToTeamMember(_stickyMember, _boundProjFile, prompt, liveCfg, bot, chatId, executeTaskByName, acl);
727
+ return;
728
+ }
729
+ }
704
730
  await handleCommand(bot, chatId, prompt, liveCfg, executeTaskByName, acl.senderId, acl.readOnly);
705
731
  } catch (err) {
706
732
  log('ERROR', `Feishu file download failed: ${err.message}`);
@@ -6,6 +6,7 @@ function createCheckpointUtils(deps) {
6
6
  const execFileAsync = execFile ? promisify(execFile) : null;
7
7
 
8
8
  const CHECKPOINT_PREFIX = '[metame-checkpoint]';
9
+ const CHECKPOINT_REF_PREFIX = 'refs/metame/checkpoints/';
9
10
  const MAX_CHECKPOINTS = 20;
10
11
 
11
12
  function cpExtractTimestamp(message) {
@@ -40,68 +41,121 @@ function createCheckpointUtils(deps) {
40
41
  // On Windows, git.exe is a console app — windowsHide:true prevents flash
41
42
  const WIN_HIDE = process.platform === 'win32' ? { windowsHide: true } : {};
42
43
 
44
+ // Shared helper: build the commit message and timestamp for a checkpoint.
45
+ function buildCheckpointMsg(label) {
46
+ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
47
+ const safeLabel = label
48
+ ? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
49
+ : '';
50
+ return { ts, safeLabel, msg: `${CHECKPOINT_PREFIX}${safeLabel} (${ts})` };
51
+ }
52
+
53
+ // Build a checkpoint commit stored under refs/metame/checkpoints/{ts} — never pushed by git push.
54
+ // Returns the commit SHA, or null if nothing changed.
43
55
  function gitCheckpoint(cwd, label) {
44
56
  try {
45
57
  execSync('git rev-parse --is-inside-work-tree', { cwd, stdio: 'ignore', ...WIN_HIDE });
58
+
59
+ // Snapshot current index so we can restore it after staging
60
+ const originalTree = execSync('git write-tree', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
61
+
62
+ // Stage everything to get a full snapshot tree
46
63
  execSync('git add -A', { cwd, stdio: 'ignore', timeout: 5000, ...WIN_HIDE });
47
- const status = execSync('git status --porcelain', { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }).trim();
48
- if (!status) return null;
49
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
50
- const safeLabel = label
51
- ? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
52
- : '';
53
- const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
54
- execSync(`git commit -m "${msg}" --no-verify`, { cwd, stdio: 'ignore', timeout: 10000, ...WIN_HIDE });
55
- const hash = execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
56
- log('INFO', `Git checkpoint: ${hash.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
57
- return hash;
64
+ const cpTree = execSync('git write-tree', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim();
65
+
66
+ // Restore index immediately leave the user's staged state intact
67
+ execSync(`git read-tree ${originalTree}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
68
+
69
+ // Compare against HEAD tree — skip if nothing changed
70
+ let headTree = '';
71
+ try { headTree = execSync('git rev-parse HEAD^{tree}', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim(); } catch { /* no commits yet */ }
72
+ if (cpTree === headTree) return null;
73
+
74
+ const { ts, safeLabel, msg } = buildCheckpointMsg(label);
75
+
76
+ // Build parent arg (-p HEAD, or nothing for initial commit)
77
+ let parentFlag = '';
78
+ try { parentFlag = `-p ${execSync('git rev-parse HEAD', { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }).trim()}`; } catch { /* no HEAD */ }
79
+
80
+ // Create an orphaned commit object — NOT on any branch
81
+ const cpSha = execSync(
82
+ `git commit-tree ${cpTree} ${parentFlag} -m "${msg}"`,
83
+ { cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE }
84
+ ).trim();
85
+
86
+ // Point a local-only ref at it — git push never transfers refs/metame/*
87
+ execSync(`git update-ref ${CHECKPOINT_REF_PREFIX}${ts} ${cpSha}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE });
88
+
89
+ log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
90
+ return cpSha;
58
91
  } catch {
59
92
  return null;
60
93
  }
61
94
  }
62
95
 
63
- // Async version: runs git commands without blocking the event loop.
64
- // Call fire-and-forget before spawning Claude; completes well before Claude's first file write.
96
+ // Async version: same logic but non-blocking.
65
97
  async function gitCheckpointAsync(cwd, label) {
66
- if (!execFileAsync) return gitCheckpoint(cwd, label); // fallback
98
+ if (!execFileAsync) return gitCheckpoint(cwd, label);
67
99
  try {
68
100
  await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 3000, ...WIN_HIDE });
101
+
102
+ const { stdout: originalTreeOut } = await execFileAsync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE });
103
+ const originalTree = originalTreeOut.trim();
104
+
69
105
  await execFileAsync('git', ['add', '-A'], { cwd, timeout: 5000, ...WIN_HIDE });
70
- const { stdout: status } = await execFileAsync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE });
71
- if (!status.trim()) return null;
72
- const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
73
- const safeLabel = label
74
- ? ' Before: ' + label.replace(/["\n\r]/g, ' ').slice(0, 60).trim()
75
- : '';
76
- const msg = `${CHECKPOINT_PREFIX}${safeLabel} (${ts})`;
77
- await execFileAsync('git', ['commit', '-m', msg, '--no-verify'], { cwd, timeout: 10000, ...WIN_HIDE });
78
- const { stdout: hash } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE });
79
- log('INFO', `Git checkpoint: ${hash.trim().slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
80
- return hash.trim();
106
+ const { stdout: cpTreeOut } = await execFileAsync('git', ['write-tree'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE });
107
+ const cpTree = cpTreeOut.trim();
108
+
109
+ // Restore index
110
+ await execFileAsync('git', ['read-tree', originalTree], { cwd, timeout: 3000, ...WIN_HIDE });
111
+
112
+ let headTree = '';
113
+ try { const r = await execFileAsync('git', ['rev-parse', 'HEAD^{tree}'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }); headTree = r.stdout.trim(); } catch { /* no HEAD */ }
114
+ if (cpTree === headTree) return null;
115
+
116
+ const { ts, safeLabel, msg } = buildCheckpointMsg(label);
117
+
118
+ let parentArgs = [];
119
+ try { const r = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 3000, ...WIN_HIDE }); parentArgs = ['-p', r.stdout.trim()]; } catch { /* no HEAD */ }
120
+
121
+ const { stdout: cpShaOut } = await execFileAsync('git', ['commit-tree', cpTree, ...parentArgs, '-m', msg], { cwd, encoding: 'utf8', timeout: 10000, ...WIN_HIDE });
122
+ const cpSha = cpShaOut.trim();
123
+
124
+ await execFileAsync('git', ['update-ref', `${CHECKPOINT_REF_PREFIX}${ts}`, cpSha], { cwd, timeout: 3000, ...WIN_HIDE });
125
+
126
+ log('INFO', `Git checkpoint: ${cpSha.slice(0, 8)} in ${path.basename(cwd)}${safeLabel}`);
127
+ return cpSha;
81
128
  } catch {
82
129
  return null;
83
130
  }
84
131
  }
85
132
 
133
+ // List checkpoints, newest first. Returns [{hash, message, ref, parentHash}].
134
+ // Uses %(parent) in for-each-ref format — no extra subprocess per checkpoint.
86
135
  function listCheckpoints(cwd, limit = 20) {
87
136
  try {
88
137
  const raw = execSync(
89
- `git log --fixed-strings --oneline --all --grep="${CHECKPOINT_PREFIX}" -n ${limit} --format="%H %s"`,
138
+ `git for-each-ref --sort=-committerdate --format="%(objectname)|%(refname)|%(parent)|%(contents:subject)" --count=${limit} ${CHECKPOINT_REF_PREFIX}`,
90
139
  { cwd, encoding: 'utf8', timeout: 5000, ...WIN_HIDE }
91
140
  ).trim();
92
141
  if (!raw) return [];
93
- return raw.split('\n').map(line => {
94
- const spaceIdx = line.indexOf(' ');
95
- return { hash: line.slice(0, spaceIdx), message: line.slice(spaceIdx + 1) };
142
+ return raw.split('\n').filter(Boolean).map(line => {
143
+ const [hash, ref, parent, ...rest] = line.split('|');
144
+ return { hash, ref, parentHash: parent || null, message: rest.join('|') };
96
145
  });
97
146
  } catch { return []; }
98
147
  }
99
148
 
149
+ // Delete checkpoints beyond MAX_CHECKPOINTS (oldest first).
100
150
  function cleanupCheckpoints(cwd) {
101
151
  try {
102
152
  const all = listCheckpoints(cwd, 100);
103
153
  if (all.length <= MAX_CHECKPOINTS) return;
104
- log('INFO', `${all.length} checkpoints in ${path.basename(cwd)}, consider: git rebase -i`);
154
+ const toDelete = all.slice(MAX_CHECKPOINTS); // oldest (for-each-ref sorted newest-first)
155
+ for (const cp of toDelete) {
156
+ try { execSync(`git update-ref -d ${cp.ref}`, { cwd, stdio: 'ignore', timeout: 3000, ...WIN_HIDE }); } catch { /* ignore */ }
157
+ }
158
+ log('INFO', `Cleaned up ${toDelete.length} old checkpoints in ${path.basename(cwd)}`);
105
159
  } catch { /* ignore */ }
106
160
  }
107
161
 
@@ -1685,7 +1685,8 @@ ${mentorRadarHint}
1685
1685
  const _isVirtualAgent = String(chatId).startsWith('_agent_') || String(chatId).startsWith('_scope_');
1686
1686
  if (!_isVirtualAgent) {
1687
1687
  try {
1688
- const checkpointResult = (gitCheckpointAsync || gitCheckpoint)(session.cwd, prompt);
1688
+ // Do NOT pass prompt conversation content must never enter git history
1689
+ const checkpointResult = (gitCheckpointAsync || gitCheckpoint)(session.cwd);
1689
1690
  if (checkpointResult && typeof checkpointResult.catch === 'function') {
1690
1691
  checkpointResult.catch(() => { });
1691
1692
  }
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * daemon-dispatch-cards.js — Dispatch presentation layer
5
+ *
6
+ * Pure functions for resolving dispatch targets and building cards/receipts.
7
+ * No daemon state; everything is derived from config passed as arguments.
8
+ *
9
+ * Counterpart to daemon-team-dispatch.js (context enrichment / actor resolution).
10
+ * Used by: daemon.js, daemon-command-router.js
11
+ */
12
+
13
+ const { resolveDispatchActor } = require('./daemon-team-dispatch');
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Target resolution
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ /**
20
+ * Resolve a project key (or team member key) to a rich target descriptor.
21
+ * Returns null when the key is unknown.
22
+ */
23
+ function resolveDispatchTarget(targetKey, config) {
24
+ const rawKey = String(targetKey || '').trim();
25
+ const projects = (config && config.projects) || {};
26
+ if (!rawKey) return null;
27
+
28
+ if (projects[rawKey]) {
29
+ const proj = projects[rawKey];
30
+ return {
31
+ key: rawKey,
32
+ name: proj.name || rawKey,
33
+ icon: proj.icon || '🤖',
34
+ color: proj.color || 'blue',
35
+ parentKey: rawKey,
36
+ parentProject: proj,
37
+ member: null,
38
+ isTeamMember: false,
39
+ };
40
+ }
41
+
42
+ for (const [parentKey, parent] of Object.entries(projects)) {
43
+ if (!Array.isArray(parent && parent.team)) continue;
44
+ const member = parent.team.find(m => m && m.key === rawKey);
45
+ if (!member) continue;
46
+ return {
47
+ key: rawKey,
48
+ name: member.name || rawKey,
49
+ icon: member.icon || parent.icon || '🤖',
50
+ color: member.color || parent.color || 'blue',
51
+ parentKey,
52
+ parentProject: parent,
53
+ member,
54
+ isTeamMember: true,
55
+ };
56
+ }
57
+ return null;
58
+ }
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // TeamTask resume hint
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ function buildTeamTaskResumeHint(taskId, scopeId) {
65
+ const safeTaskId = String(taskId || '').trim();
66
+ if (!safeTaskId) return '';
67
+ const safeScopeId = String(scopeId || '').trim();
68
+ const lines = [
69
+ '',
70
+ `TeamTask: ${safeTaskId}`,
71
+ ];
72
+ if (safeScopeId && safeScopeId !== safeTaskId) lines.push(`Scope: ${safeScopeId}`);
73
+ lines.push(`如需复工,请使用: /TeamTask resume ${safeTaskId}`);
74
+ return lines.join('\n');
75
+ }
76
+
77
+ function appendTeamTaskResumeHint(text, taskId, scopeId) {
78
+ const base = String(text || '').trim();
79
+ const hint = buildTeamTaskResumeHint(taskId, scopeId);
80
+ if (!hint) return base;
81
+ return `${base}${hint}`;
82
+ }
83
+
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+ // Card builders
86
+ // ─────────────────────────────────────────────────────────────────────────────
87
+
88
+ /** Minimal card header for streaming response windows. */
89
+ function buildDispatchResponseCard(targetKey, config) {
90
+ const target = resolveDispatchTarget(targetKey, config);
91
+ if (!target) return null;
92
+ return {
93
+ title: `${target.icon} ${target.name}`,
94
+ color: target.color || 'blue',
95
+ };
96
+ }
97
+
98
+ /** Full task card shown in the source's channel when a dispatch is sent. */
99
+ function buildDispatchTaskCard(fullMsg, targetProject, config) {
100
+ const projects = (config && config.projects) || {};
101
+ const actor = resolveDispatchActor(
102
+ (fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from),
103
+ projects
104
+ );
105
+ const target = resolveDispatchTarget(targetProject, config) || {
106
+ icon: '🤖',
107
+ name: targetProject,
108
+ color: 'blue',
109
+ };
110
+ const prompt = String(fullMsg && fullMsg.payload && (fullMsg.payload.prompt || fullMsg.payload.title) || '').trim();
111
+ const preview = prompt ? `${prompt.slice(0, 300)}${prompt.length > 300 ? '…' : ''}` : '(empty)';
112
+ const lines = [
113
+ `发起: ${actor.icon} ${actor.name}`,
114
+ `目标: ${target.icon} ${target.name}`,
115
+ `编号: ${fullMsg.id}`,
116
+ ];
117
+ if (fullMsg.task_id) lines.push(`TeamTask: ${fullMsg.task_id}`);
118
+ if (fullMsg.scope_id && fullMsg.scope_id !== fullMsg.task_id) lines.push(`Scope: ${fullMsg.scope_id}`);
119
+ lines.push('', preview);
120
+ return {
121
+ title: '📬 新任务',
122
+ body: lines.join('\n'),
123
+ color: target.color || 'blue',
124
+ markdown: `## 📬 新任务\n\n${lines.join('\n')}\n\n---\n${preview}`,
125
+ text: `📬 新任务\n\n${lines.join('\n')}\n\n${preview}`,
126
+ };
127
+ }
128
+
129
+ /** Receipt card sent back to the dispatcher after the target agent accepts or rejects. */
130
+ function buildDispatchReceipt(item, config, result, opts = {}) {
131
+ const targetKey = String(opts.targetKey || item.target || '').trim() || 'unknown';
132
+ const target = resolveDispatchTarget(targetKey, config) || {
133
+ icon: '🤖',
134
+ name: targetKey,
135
+ };
136
+ const actor = resolveDispatchActor(
137
+ String(item && (item.source_sender_key || item.from) || 'user').trim() || 'user',
138
+ (config && config.projects) || {}
139
+ );
140
+ const prompt = String(item && item.prompt || '').trim();
141
+ const preview = prompt ? `${prompt.slice(0, 120)}${prompt.length > 120 ? '...' : ''}` : '(empty)';
142
+ const isFailed = !result || !result.success;
143
+ const title = isFailed ? '❌ Dispatch 回执' : '📮 Dispatch 回执';
144
+ const statusLine = isFailed
145
+ ? `状态: 入队失败 (${String(result && result.error || 'unknown_error').slice(0, 120)})`
146
+ : '状态: 目标端已接收并入队';
147
+ const lines = [
148
+ title,
149
+ '',
150
+ statusLine,
151
+ `发起: ${actor.icon} ${actor.name}`,
152
+ `目标: ${target.icon} ${target.name}`,
153
+ ];
154
+ if (result && result.id) lines.push(`编号: ${result.id}`);
155
+ lines.push(`摘要: ${preview}`);
156
+ if (result && result.task_id) lines.push(buildTeamTaskResumeHint(result.task_id, result.scope_id));
157
+ return {
158
+ status: isFailed ? 'failed' : 'accepted',
159
+ dispatchId: result && result.id ? result.id : '',
160
+ targetKey,
161
+ text: lines.join('\n'),
162
+ };
163
+ }
164
+
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+ // Bot delivery helper
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+
169
+ /** Send a dispatch card via whichever method the bot supports. */
170
+ async function sendDispatchTaskCard(bot, chatId, card) {
171
+ if (!bot || !chatId || !card) return null;
172
+ if (bot.sendCard) return bot.sendCard(chatId, { title: card.title, body: card.body, color: card.color || 'blue' });
173
+ if (bot.sendMarkdown) return bot.sendMarkdown(chatId, card.markdown);
174
+ return bot.sendMessage(chatId, card.text);
175
+ }
176
+
177
+ module.exports = {
178
+ resolveDispatchTarget,
179
+ buildTeamTaskResumeHint,
180
+ appendTeamTaskResumeHint,
181
+ buildDispatchResponseCard,
182
+ buildDispatchTaskCard,
183
+ buildDispatchReceipt,
184
+ sendDispatchTaskCard,
185
+ };
@@ -86,7 +86,12 @@ function createOpsCommandHandler(deps) {
86
86
  let diffFiles = '';
87
87
  const _wh = process.platform === 'win32' ? { windowsHide: true } : {};
88
88
  try { diffFiles = execSync(`git diff --name-only HEAD ${match.hash}`, { cwd, encoding: 'utf8', timeout: 5000, ..._wh }).trim(); } catch { }
89
- execSync(`git reset --hard ${match.hash}`, { cwd, stdio: 'ignore', timeout: 10000, ..._wh });
89
+ // Reset HEAD to checkpoint's parent (removes any commits Claude made)
90
+ if (match.parentHash) {
91
+ execSync(`git reset --hard ${match.parentHash}`, { cwd, stdio: 'ignore', timeout: 10000, ..._wh });
92
+ }
93
+ // Restore working tree to exact checkpoint state (recovers pre-Claude uncommitted changes)
94
+ execSync(`git checkout ${match.hash} -- .`, { cwd, stdio: 'ignore', timeout: 10000, ..._wh });
90
95
  // Truncate context to checkpoint time (covers multi-turn rollback)
91
96
  truncateSessionToCheckpoint(session.id, match.message);
92
97
  const fileList = diffFiles ? diffFiles.split('\n').map(f => path.basename(f)).join(', ') : '';
@@ -226,9 +231,12 @@ function createOpsCommandHandler(deps) {
226
231
  const _wh2 = process.platform === 'win32' ? { windowsHide: true } : {};
227
232
  try { diffFiles2 = execSync(`git diff --name-only HEAD ${cpMatch.hash}`, { cwd: cwd2, encoding: 'utf8', timeout: 5000, ..._wh2 }).trim(); } catch { }
228
233
  if (diffFiles2) {
229
- // Save current state with distinct prefix (excluded from normal /undo list)
230
- gitCheckpoint(cwd2, `[metame-safety] before rollback to: ${targetMsg.slice(0, 40)}`);
231
- execSync(`git reset --hard ${cpMatch.hash}`, { cwd: cwd2, stdio: 'ignore', timeout: 10000, ..._wh2 });
234
+ // Save current state before rollback (excluded from normal /undo list)
235
+ gitCheckpoint(cwd2, '[metame-safety] before rollback');
236
+ if (cpMatch.parentHash) {
237
+ execSync(`git reset --hard ${cpMatch.parentHash}`, { cwd: cwd2, stdio: 'ignore', timeout: 10000, ..._wh2 });
238
+ }
239
+ execSync(`git checkout ${cpMatch.hash} -- .`, { cwd: cwd2, stdio: 'ignore', timeout: 10000, ..._wh2 });
232
240
  gitMsg2 = `\n📁 ${diffFiles2.split('\n').length} 个文件已恢复`;
233
241
  cleanupCheckpoints(cwd2);
234
242
  }
package/scripts/daemon.js CHANGED
@@ -147,7 +147,16 @@ const { createSessionCommandHandler } = require('./daemon-session-commands');
147
147
  const { createSessionStore } = require('./daemon-session-store');
148
148
  const { createCheckpointUtils } = require('./daemon-checkpoints');
149
149
  const { createBridgeStarter } = require('./daemon-bridges');
150
- const { buildTeamRosterHint, buildEnrichedPrompt, resolveDispatchActor, updateDispatchContextFiles } = require('./team-dispatch');
150
+ const { buildTeamRosterHint, buildEnrichedPrompt, updateDispatchContextFiles } = require('./daemon-team-dispatch');
151
+ const {
152
+ resolveDispatchTarget,
153
+ buildTeamTaskResumeHint,
154
+ appendTeamTaskResumeHint,
155
+ buildDispatchResponseCard,
156
+ buildDispatchTaskCard,
157
+ buildDispatchReceipt,
158
+ sendDispatchTaskCard,
159
+ } = require('./daemon-dispatch-cards');
151
160
  const { createFileBrowser } = require('./daemon-file-browser');
152
161
  const { createPidManager, setupRuntimeWatchers } = require('./daemon-runtime-lifecycle');
153
162
  const { repairAgentLayer } = require('./agent-layer');
@@ -1277,26 +1286,6 @@ function sendDispatchReceipt(item, config, receipt) {
1277
1286
  writeDispatchReceiptInbox(item, receipt);
1278
1287
  }
1279
1288
 
1280
- function buildTeamTaskResumeHint(taskId, scopeId) {
1281
- const safeTaskId = String(taskId || '').trim();
1282
- if (!safeTaskId) return '';
1283
- const safeScopeId = String(scopeId || '').trim();
1284
- const lines = [
1285
- '',
1286
- `TeamTask: ${safeTaskId}`,
1287
- ];
1288
- if (safeScopeId && safeScopeId !== safeTaskId) lines.push(`Scope: ${safeScopeId}`);
1289
- lines.push(`如需复工,请使用: /TeamTask resume ${safeTaskId}`);
1290
- return lines.join('\n');
1291
- }
1292
-
1293
- function appendTeamTaskResumeHint(text, taskId, scopeId) {
1294
- const base = String(text || '').trim();
1295
- const hint = buildTeamTaskResumeHint(taskId, scopeId);
1296
- if (!hint) return base;
1297
- return `${base}${hint}`;
1298
- }
1299
-
1300
1289
  function buildDispatchPrompt(targetProject, fullMsg, envelope, metameDir = METAME_DIR) {
1301
1290
  const promptBody = buildEnrichedPrompt(
1302
1291
  targetProject,
@@ -1309,79 +1298,6 @@ function buildDispatchPrompt(targetProject, fullMsg, envelope, metameDir = METAM
1309
1298
  : promptBody;
1310
1299
  }
1311
1300
 
1312
- function resolveDispatchTarget(targetKey, config) {
1313
- const rawKey = String(targetKey || '').trim();
1314
- const projects = (config && config.projects) || {};
1315
- if (!rawKey) return null;
1316
- if (projects[rawKey]) {
1317
- const proj = projects[rawKey];
1318
- return {
1319
- key: rawKey,
1320
- name: proj.name || rawKey,
1321
- icon: proj.icon || '🤖',
1322
- color: proj.color || 'blue',
1323
- parentKey: rawKey,
1324
- parentProject: proj,
1325
- member: null,
1326
- isTeamMember: false,
1327
- };
1328
- }
1329
- for (const [parentKey, parent] of Object.entries(projects)) {
1330
- if (!Array.isArray(parent && parent.team)) continue;
1331
- const member = parent.team.find(m => m && m.key === rawKey);
1332
- if (!member) continue;
1333
- return {
1334
- key: rawKey,
1335
- name: member.name || rawKey,
1336
- icon: member.icon || parent.icon || '🤖',
1337
- color: member.color || parent.color || 'blue',
1338
- parentKey,
1339
- parentProject: parent,
1340
- member,
1341
- isTeamMember: true,
1342
- };
1343
- }
1344
- return null;
1345
- }
1346
-
1347
- function buildDispatchResponseCard(targetKey, config) {
1348
- const target = resolveDispatchTarget(targetKey, config);
1349
- if (!target) return null;
1350
- return {
1351
- title: `${target.icon} ${target.name}`,
1352
- color: target.color || 'blue',
1353
- };
1354
- }
1355
-
1356
- function buildDispatchTaskCard(fullMsg, targetProject, config) {
1357
- const projects = (config && config.projects) || {};
1358
- const actor = resolveDispatchActor(
1359
- (fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from),
1360
- projects
1361
- );
1362
- const target = resolveDispatchTarget(targetProject, config) || {
1363
- icon: '🤖',
1364
- name: targetProject,
1365
- color: 'blue',
1366
- };
1367
- const prompt = String(fullMsg && fullMsg.payload && (fullMsg.payload.prompt || fullMsg.payload.title) || '').trim();
1368
- const preview = prompt ? `${prompt.slice(0, 300)}${prompt.length > 300 ? '…' : ''}` : '(empty)';
1369
- const lines = [
1370
- `发起: ${actor.icon} ${actor.name}`,
1371
- `目标: ${target.icon} ${target.name}`,
1372
- `编号: ${fullMsg.id}`,
1373
- ];
1374
- if (fullMsg.task_id) lines.push(`TeamTask: ${fullMsg.task_id}`);
1375
- if (fullMsg.scope_id && fullMsg.scope_id !== fullMsg.task_id) lines.push(`Scope: ${fullMsg.scope_id}`);
1376
- lines.push('', preview);
1377
- return {
1378
- title: '📬 新任务',
1379
- body: lines.join('\n'),
1380
- color: target.color || 'blue',
1381
- markdown: `## 📬 新任务\n\n${lines.join('\n')}\n\n---\n${preview}`,
1382
- text: `📬 新任务\n\n${lines.join('\n')}\n\n${preview}`,
1383
- };
1384
- }
1385
1301
 
1386
1302
  function resolveDispatchReadOnly(message, config, targetProject) {
1387
1303
  if (message && typeof message.readOnly === 'boolean') return message.readOnly;
@@ -1396,48 +1312,6 @@ function resolveDispatchReadOnly(message, config, targetProject) {
1396
1312
  return true;
1397
1313
  }
1398
1314
 
1399
- async function sendDispatchTaskCard(bot, chatId, card) {
1400
- if (!bot || !chatId || !card) return null;
1401
- if (bot.sendCard) return bot.sendCard(chatId, { title: card.title, body: card.body, color: card.color || 'blue' });
1402
- if (bot.sendMarkdown) return bot.sendMarkdown(chatId, card.markdown);
1403
- return bot.sendMessage(chatId, card.text);
1404
- }
1405
-
1406
- function buildDispatchReceipt(item, config, result, opts = {}) {
1407
- const targetKey = String(opts.targetKey || item.target || '').trim() || 'unknown';
1408
- const target = resolveDispatchTarget(targetKey, config) || {
1409
- icon: '🤖',
1410
- name: targetKey,
1411
- };
1412
- const actor = resolveDispatchActor(
1413
- String(item && (item.source_sender_key || item.from) || 'user').trim() || 'user',
1414
- (config && config.projects) || {}
1415
- );
1416
- const prompt = String(item && item.prompt || '').trim();
1417
- const preview = prompt ? `${prompt.slice(0, 120)}${prompt.length > 120 ? '...' : ''}` : '(empty)';
1418
- const isFailed = !result || !result.success;
1419
- const title = isFailed ? '❌ Dispatch 回执' : '📮 Dispatch 回执';
1420
- const statusLine = isFailed
1421
- ? `状态: 入队失败 (${String(result && result.error || 'unknown_error').slice(0, 120)})`
1422
- : '状态: 目标端已接收并入队';
1423
- const lines = [
1424
- title,
1425
- '',
1426
- statusLine,
1427
- `发起: ${actor.icon} ${actor.name}`,
1428
- `目标: ${target.icon} ${target.name}`,
1429
- ];
1430
- if (result && result.id) lines.push(`编号: ${result.id}`);
1431
- lines.push(`摘要: ${preview}`);
1432
- if (result && result.task_id) lines.push(buildTeamTaskResumeHint(result.task_id, result.scope_id));
1433
- return {
1434
- status: isFailed ? 'failed' : 'accepted',
1435
- dispatchId: result && result.id ? result.id : '',
1436
- targetKey,
1437
- text: lines.join('\n'),
1438
- };
1439
- }
1440
-
1441
1315
  function handleDispatchItem(item, config) {
1442
1316
  if (!item.target || !item.prompt) return;
1443
1317
  const resolvedTarget = resolveDispatchTarget(item.target, config);
@@ -147,7 +147,7 @@ feishu:
147
147
  - ENGINE_MODEL_CONFIG(daemon-engine-runtime.js 集中管理)
148
148
  - daemon-runtime-lifecycle.js 的语法检查和备份机制
149
149
  - daemon-remote-dispatch.js(纯逻辑,无平台差异)
150
- - team-dispatch.js(共享解析/hint/enrichment)
150
+ - daemon-team-dispatch.js(共享解析/hint/enrichment)
151
151
 
152
152
  ### 需分别维护(有平台/引擎特殊分支)
153
153
 
@@ -345,7 +345,7 @@ Claude 看到 hook 注入:
345
345
  | `daemon-bridges.js` | Feishu bridge 拦截 relay 群消息 + `_dispatchToTeamMember` 远端分流 |
346
346
  | `daemon-admin-commands.js` | `/dispatch peers` 查看配置 + `/dispatch to peer:project` 手动派发 |
347
347
  | `scripts/bin/dispatch_to` | 支持 `peer:project` 格式 → 写 `remote-pending.jsonl` |
348
- | `team-dispatch.js` | `buildTeamRosterHint()` 为远端成员生成 `peer:key` 格式命令 |
348
+ | `daemon-team-dispatch.js` | `buildTeamRosterHint()` 为远端成员生成 `peer:key` 格式命令 |
349
349
  | `hooks/team-context.js` | intent hook 注入远端 `peer:key` dispatch 命令 |
350
350
 
351
351
  ### 管理命令
@@ -64,7 +64,7 @@
64
64
  ## 团队 Dispatch 与跨设备通信定位
65
65
 
66
66
  - 共享 Dispatch 工具:
67
- - `scripts/team-dispatch.js`
67
+ - `scripts/daemon-team-dispatch.js`
68
68
  - 关键点:`resolveProjectKey()` 名称/昵称解析(含 team member `parent/member` 复合键);
69
69
  `findTeamMember()` 文本前缀匹配团队成员昵称;
70
70
  `buildTeamRosterHint()` 生成团队上下文块(远端成员自动带 `peer:key` 前缀);
@@ -156,7 +156,7 @@
156
156
  1. 先看配置:`~/.metame/daemon.yaml` 与 `scripts/daemon-default.yaml`
157
157
  2. 再看命令入口:`scripts/daemon-admin-commands.js`、`scripts/daemon-command-router.js`、`scripts/daemon-exec-commands.js`
158
158
  3. 再看执行链路:`scripts/daemon-engine-runtime.js` → `scripts/daemon-claude-engine.js` → `scripts/mentor-engine.js`
159
- 4. 团队/跨设备:`scripts/team-dispatch.js` → `scripts/daemon-remote-dispatch.js` → `scripts/daemon-bridges.js`
159
+ 4. 团队/跨设备:`scripts/daemon-team-dispatch.js` → `scripts/daemon-remote-dispatch.js` → `scripts/daemon-bridges.js`
160
160
  5. 最后看离线任务:`scripts/distill.js`、`scripts/memory-extract.js`、`scripts/memory-nightly-reflect.js`
161
161
 
162
162
  ## 同步提示