metame-cli 1.5.5 → 1.5.8
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/index.js +9 -4
- package/package.json +3 -2
- package/scripts/bin/dispatch_to +1 -1
- package/scripts/bin/push-clean.sh +72 -0
- package/scripts/daemon-admin-commands.js +2 -2
- package/scripts/daemon-bridges.js +28 -2
- package/scripts/daemon-checkpoints.js +84 -30
- package/scripts/daemon-claude-engine.js +2 -1
- package/scripts/daemon-dispatch-cards.js +185 -0
- package/scripts/daemon-ops-commands.js +12 -4
- package/scripts/daemon.js +10 -136
- package/scripts/docs/maintenance-manual.md +2 -2
- package/scripts/docs/pointer-map.md +2 -2
- /package/scripts/{team-dispatch.js → daemon-team-dispatch.js} +0 -0
package/index.js
CHANGED
|
@@ -303,15 +303,20 @@ function requestDaemonRestart({
|
|
|
303
303
|
// Auto-deploy bundled scripts to ~/.metame/
|
|
304
304
|
// IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
|
|
305
305
|
const scriptsDir = path.join(__dirname, 'scripts');
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
// Auto-detect ALL runtime scripts: daemon-*.js + all other non-test, non-utility .js/.yaml/.sh files.
|
|
307
|
+
// This prevents "missing module" crashes when new files are added without updating a manual list.
|
|
308
|
+
const EXCLUDED_SCRIPTS = new Set(['sync-readme.js', 'test_daemon.js']);
|
|
309
|
+
const BUNDLED_SCRIPTS = (() => {
|
|
308
310
|
try {
|
|
309
|
-
return fs.readdirSync(scriptsDir).filter((f) =>
|
|
311
|
+
return fs.readdirSync(scriptsDir).filter((f) => {
|
|
312
|
+
if (EXCLUDED_SCRIPTS.has(f)) return false;
|
|
313
|
+
if (/\.test\.js$/.test(f)) return false;
|
|
314
|
+
return /\.(js|yaml|sh)$/.test(f);
|
|
315
|
+
});
|
|
310
316
|
} catch {
|
|
311
317
|
return [];
|
|
312
318
|
}
|
|
313
319
|
})();
|
|
314
|
-
const BUNDLED_SCRIPTS = [...new Set([...BUNDLED_BASE_SCRIPTS, ...DAEMON_MODULE_SCRIPTS])];
|
|
315
320
|
|
|
316
321
|
// Protect daemon.yaml: create backup before any sync operation
|
|
317
322
|
const DAEMON_YAML_BACKUP = path.join(METAME_DIR, 'daemon.yaml.bak');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metame-cli",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.8",
|
|
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
|
-
"
|
|
20
|
+
"push": "bash scripts/bin/push-clean.sh",
|
|
21
|
+
"sync:plugin": "node -e \"const fs=require('fs'),path=require('path');const ex=new Set(['sync-readme.js','test_daemon.js']);const files=fs.readdirSync('scripts').filter(f=>!ex.has(f)&&!/\\.test\\.js$/.test(f)&&/\\.(js|yaml|sh)$/.test(f));files.forEach(f=>fs.copyFileSync('scripts/'+f,'plugin/scripts/'+f));\" && mkdir -p plugin/scripts/hooks && cp scripts/hooks/*.js plugin/scripts/hooks/ && 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"
|
package/scripts/bin/dispatch_to
CHANGED
|
@@ -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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
execSync(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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:
|
|
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);
|
|
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:
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
await execFileAsync('git', ['
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
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
|
|
95
|
-
return { hash
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
230
|
-
gitCheckpoint(cwd2,
|
|
231
|
-
|
|
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,
|
|
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
|
## 同步提示
|
|
File without changes
|