metame-cli 1.5.3 → 1.5.5
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/README.md +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -895,6 +895,161 @@ function detectSignificantSession(skeleton) {
|
|
|
895
895
|
return { significant: reasons.length > 0, reasons };
|
|
896
896
|
}
|
|
897
897
|
|
|
898
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
899
|
+
// Codex session adapter
|
|
900
|
+
// Reads ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl (first line only, ~1KB)
|
|
901
|
+
// and ~/.codex/history.jsonl (user messages). Reuses the same state DB with
|
|
902
|
+
// a 'codex_facts' key to avoid collisions with Claude session IDs.
|
|
903
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
904
|
+
|
|
905
|
+
const CODEX_SESSIONS_ROOT = path.join(HOME, '.codex', 'sessions');
|
|
906
|
+
const CODEX_HISTORY_FILE = path.join(HOME, '.codex', 'history.jsonl');
|
|
907
|
+
// Matches: rollout-YYYY-MM-DDTHH-MM-SS-<uuid>.jsonl (colons replaced with dashes)
|
|
908
|
+
const CODEX_ROLLOUT_PATTERN = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/;
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Load ~/.codex/history.jsonl into a Map<session_id, [{ts, text}]>.
|
|
912
|
+
* Pass sessionIds to load only the sessions you need — avoids reading the
|
|
913
|
+
* whole file (which grows unbounded) when only a few sessions are relevant.
|
|
914
|
+
*
|
|
915
|
+
* @param {string[]|null} sessionIds - allowlist; null/empty loads everything
|
|
916
|
+
*/
|
|
917
|
+
function loadCodexHistory(sessionIds = null) {
|
|
918
|
+
const map = new Map();
|
|
919
|
+
const allow = sessionIds && sessionIds.length > 0 ? new Set(sessionIds) : null;
|
|
920
|
+
try {
|
|
921
|
+
if (!fs.existsSync(CODEX_HISTORY_FILE)) return map;
|
|
922
|
+
const lines = fs.readFileSync(CODEX_HISTORY_FILE, 'utf8').split('\n');
|
|
923
|
+
for (const line of lines) {
|
|
924
|
+
if (!line.trim()) continue;
|
|
925
|
+
let entry;
|
|
926
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
927
|
+
if (!entry.session_id || !entry.text) continue;
|
|
928
|
+
if (allow && !allow.has(entry.session_id)) continue;
|
|
929
|
+
if (!map.has(entry.session_id)) map.set(entry.session_id, []);
|
|
930
|
+
map.get(entry.session_id).push({ ts: entry.ts, text: entry.text });
|
|
931
|
+
}
|
|
932
|
+
} catch { /* non-fatal */ }
|
|
933
|
+
return map;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Find all Codex rollout files not yet processed by memory-extract.
|
|
938
|
+
* Filename pattern: rollout-YYYY-MM-DDTHH-MM-SS-<uuid>.jsonl
|
|
939
|
+
*/
|
|
940
|
+
function findAllUnextractedCodexSessions(limit = 30) {
|
|
941
|
+
if (!fs.existsSync(CODEX_SESSIONS_ROOT)) return [];
|
|
942
|
+
const results = [];
|
|
943
|
+
try {
|
|
944
|
+
const years = fs.readdirSync(CODEX_SESSIONS_ROOT).filter(d => /^\d{4}$/.test(d));
|
|
945
|
+
for (const year of years) {
|
|
946
|
+
const yearDir = path.join(CODEX_SESSIONS_ROOT, year);
|
|
947
|
+
const months = fs.readdirSync(yearDir).filter(d => /^\d{2}$/.test(d));
|
|
948
|
+
for (const month of months) {
|
|
949
|
+
const monthDir = path.join(yearDir, month);
|
|
950
|
+
const days = fs.readdirSync(monthDir).filter(d => /^\d{2}$/.test(d));
|
|
951
|
+
for (const day of days) {
|
|
952
|
+
const dayDir = path.join(monthDir, day);
|
|
953
|
+
let files;
|
|
954
|
+
try { files = fs.readdirSync(dayDir); } catch { continue; }
|
|
955
|
+
for (const file of files) {
|
|
956
|
+
if (!file.startsWith('rollout-') || !file.endsWith('.jsonl')) continue;
|
|
957
|
+
// Extract UUID from: rollout-YYYY-MM-DDTHH-MM-SS-<uuid>.jsonl
|
|
958
|
+
const m = file.match(CODEX_ROLLOUT_PATTERN);
|
|
959
|
+
if (!m) continue;
|
|
960
|
+
const sessionId = m[1];
|
|
961
|
+
if (isProcessed('codex_facts', sessionId)) continue;
|
|
962
|
+
const fullPath = path.join(dayDir, file);
|
|
963
|
+
let fstat;
|
|
964
|
+
try { fstat = fs.statSync(fullPath); } catch { continue; }
|
|
965
|
+
if (fstat.size < MIN_FILE_SIZE) continue;
|
|
966
|
+
results.push({ path: fullPath, session_id: sessionId, mtime: fstat.mtimeMs });
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
} catch { return []; }
|
|
972
|
+
results.sort((a, b) => b.mtime - a.mtime);
|
|
973
|
+
return results.slice(0, limit);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Build { skeleton, evidence } for a Codex session.
|
|
978
|
+
* Reads only the first 2KB of the rollout file (session_meta line) — never
|
|
979
|
+
* loads the full transcript. Enriches with user messages from historyMap.
|
|
980
|
+
*
|
|
981
|
+
* @param {string} rolloutPath - absolute path to rollout-*.jsonl
|
|
982
|
+
* @param {Map} historyMap - returned by loadCodexHistory()
|
|
983
|
+
*/
|
|
984
|
+
function buildCodexInput(rolloutPath, historyMap) {
|
|
985
|
+
let sessionMeta = null;
|
|
986
|
+
let fileSessionId = null;
|
|
987
|
+
try {
|
|
988
|
+
const m = path.basename(rolloutPath).match(CODEX_ROLLOUT_PATTERN);
|
|
989
|
+
if (m) fileSessionId = m[1];
|
|
990
|
+
|
|
991
|
+
// Read only first 2KB to get session_meta without loading the full transcript
|
|
992
|
+
let fd;
|
|
993
|
+
try {
|
|
994
|
+
fd = fs.openSync(rolloutPath, 'r');
|
|
995
|
+
const buf = Buffer.alloc(2048);
|
|
996
|
+
const bytesRead = fs.readSync(fd, buf, 0, 2048, 0);
|
|
997
|
+
const firstLine = buf.slice(0, bytesRead).toString('utf8').split('\n')[0];
|
|
998
|
+
const parsed = JSON.parse(firstLine);
|
|
999
|
+
if (parsed.type === 'session_meta') sessionMeta = parsed.payload;
|
|
1000
|
+
} finally {
|
|
1001
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
1002
|
+
}
|
|
1003
|
+
} catch { /* non-fatal */ }
|
|
1004
|
+
|
|
1005
|
+
const sessionId = (sessionMeta && sessionMeta.id) || fileSessionId;
|
|
1006
|
+
const cwd = (sessionMeta && sessionMeta.cwd) || null;
|
|
1007
|
+
const { project, project_id: projectId } = deriveProjectInfo(cwd || '');
|
|
1008
|
+
|
|
1009
|
+
// User messages from history index (sorted chronologically)
|
|
1010
|
+
const userMsgs = (sessionId && historyMap.get(sessionId)) || [];
|
|
1011
|
+
userMsgs.sort((a, b) => a.ts - b.ts);
|
|
1012
|
+
|
|
1013
|
+
const firstTs = userMsgs.length > 0 ? new Date(userMsgs[0].ts * 1000).toISOString() : null;
|
|
1014
|
+
const lastTs = userMsgs.length > 1 ? new Date(userMsgs[userMsgs.length - 1].ts * 1000).toISOString() : firstTs;
|
|
1015
|
+
const durationMin = userMsgs.length > 1
|
|
1016
|
+
? Math.round((userMsgs[userMsgs.length - 1].ts - userMsgs[0].ts) / 6) / 10
|
|
1017
|
+
: 0;
|
|
1018
|
+
|
|
1019
|
+
const skeleton = {
|
|
1020
|
+
session_id: sessionId || path.basename(rolloutPath, '.jsonl'),
|
|
1021
|
+
user_snippets: userMsgs.map(m => m.text.slice(0, 200)),
|
|
1022
|
+
tool_counts: {},
|
|
1023
|
+
total_tool_calls: 0,
|
|
1024
|
+
message_count: userMsgs.length,
|
|
1025
|
+
duration_min: durationMin,
|
|
1026
|
+
project: project || 'unknown',
|
|
1027
|
+
project_id: projectId || null,
|
|
1028
|
+
project_path: cwd,
|
|
1029
|
+
branch: null,
|
|
1030
|
+
engine: 'codex',
|
|
1031
|
+
model_provider: sessionMeta && sessionMeta.model_provider,
|
|
1032
|
+
first_ts: firstTs,
|
|
1033
|
+
last_ts: lastTs,
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const evidence = {
|
|
1037
|
+
user_messages: userMsgs.map(m => m.text).filter(Boolean).slice(0, 15),
|
|
1038
|
+
tool_traces: [],
|
|
1039
|
+
key_results: [],
|
|
1040
|
+
file_anchors: cwd ? [cwd] : [],
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
return { skeleton, evidence };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Mark a Codex session as facts-extracted.
|
|
1048
|
+
*/
|
|
1049
|
+
function markCodexFactsExtracted(sessionId) {
|
|
1050
|
+
markProcessed('codex_facts', sessionId);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
898
1053
|
module.exports = {
|
|
899
1054
|
findLatestUnanalyzedSession,
|
|
900
1055
|
findSessionById,
|
|
@@ -908,6 +1063,11 @@ module.exports = {
|
|
|
908
1063
|
detectSignificantSession,
|
|
909
1064
|
markAnalyzed,
|
|
910
1065
|
markFactsExtracted,
|
|
1066
|
+
// Codex adapter
|
|
1067
|
+
loadCodexHistory,
|
|
1068
|
+
findAllUnextractedCodexSessions,
|
|
1069
|
+
buildCodexInput,
|
|
1070
|
+
markCodexFactsExtracted,
|
|
911
1071
|
};
|
|
912
1072
|
|
|
913
1073
|
// Direct execution for testing
|
|
@@ -26,7 +26,7 @@ const ABSOLUTE_MAX_CAPTURE_CHARS = 6000;
|
|
|
26
26
|
|
|
27
27
|
// Strong directive signals → high confidence (direct write to T3)
|
|
28
28
|
// Allow up to 6 chars between key words (e.g. "以后代码一律" = "以后" + "代码" + "一律")
|
|
29
|
-
const STRONG_SIGNAL_ZH = /以后.{0,6}(
|
|
29
|
+
const STRONG_SIGNAL_ZH = /以后.{0,6}(都|总是|一律|每次|全部|统一)|永远.{0,4}(不要|别|不能|要)|千万.{0,4}(别|不要)|记住|一定.{0,4}(要|得)|一律|统一用/;
|
|
30
30
|
const STRONG_SIGNAL_EN = /(from now on|always|never|don't ever|remember to|every time)/i;
|
|
31
31
|
|
|
32
32
|
// Implicit preference signals → normal confidence (needs accumulation)
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* team-dispatch.js — Shared team/dispatch utilities
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for:
|
|
7
|
+
* - Project/team member resolution by name or nickname
|
|
8
|
+
* - Team roster hint generation (injected into member sessions)
|
|
9
|
+
* - Prompt enrichment with scoped context (private now / shared now / inbox / _latest.md)
|
|
10
|
+
* - Dispatch context file writes for target-only and team-shared tasks
|
|
11
|
+
*
|
|
12
|
+
* Used by: dispatch_to binary, daemon-admin-commands, daemon-bridges, daemon.js
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
|
|
19
|
+
const METAME_DIR = path.join(os.homedir(), '.metame');
|
|
20
|
+
|
|
21
|
+
function _escapeRe(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
22
|
+
|
|
23
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Resolution helpers
|
|
25
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a target name (key or nickname) to a project key.
|
|
29
|
+
* For top-level projects: returns the project key.
|
|
30
|
+
* For team members: returns 'parentKey/memberKey'.
|
|
31
|
+
*/
|
|
32
|
+
function resolveProjectKey(targetName, projects) {
|
|
33
|
+
if (!targetName || !projects) return null;
|
|
34
|
+
for (const [key, proj] of Object.entries(projects)) {
|
|
35
|
+
const nicks = Array.isArray(proj.nicknames)
|
|
36
|
+
? proj.nicknames
|
|
37
|
+
: (proj.nicknames ? [proj.nicknames] : []);
|
|
38
|
+
if (key === targetName || nicks.some(n => n === targetName)) return key;
|
|
39
|
+
|
|
40
|
+
if (Array.isArray(proj.team)) {
|
|
41
|
+
for (const member of proj.team) {
|
|
42
|
+
const memberNicks = Array.isArray(member.nicknames) ? member.nicknames : [];
|
|
43
|
+
if (member.key === targetName || memberNicks.some(n => n === targetName)) {
|
|
44
|
+
return `${key}/${member.key}`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find a team member by nickname prefix in a text string.
|
|
54
|
+
* Returns { member, rest } where rest is the text after stripping the nickname,
|
|
55
|
+
* or null if no match.
|
|
56
|
+
*/
|
|
57
|
+
function findTeamMember(text, team) {
|
|
58
|
+
const t = String(text || '').trim();
|
|
59
|
+
for (const member of team) {
|
|
60
|
+
const nicks = Array.isArray(member.nicknames) ? member.nicknames : [];
|
|
61
|
+
for (const nick of nicks) {
|
|
62
|
+
const n = String(nick || '').trim();
|
|
63
|
+
if (!n) continue;
|
|
64
|
+
if (t.toLowerCase() === n.toLowerCase()) return { member, rest: '' };
|
|
65
|
+
const re = new RegExp(`^${_escapeRe(n)}[\\s,,、::]+`, 'i');
|
|
66
|
+
const m = t.match(re);
|
|
67
|
+
if (m) return { member, rest: t.slice(m[0].length).trim() };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Team roster hint
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a team context block to inject into a team member's session.
|
|
79
|
+
* Tells the member who they are, who their teammates are, and how to reach them.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} parentKey - project key of the parent (e.g. 'business')
|
|
82
|
+
* @param {string} memberKey - key of the member receiving the hint (e.g. 'hunter')
|
|
83
|
+
* @param {object} projects - full projects config
|
|
84
|
+
* @returns {string} - formatted hint block, or '' if not applicable
|
|
85
|
+
*/
|
|
86
|
+
function buildTeamRosterHint(parentKey, memberKey, projects) {
|
|
87
|
+
if (!projects || !parentKey || !projects[parentKey]) return '';
|
|
88
|
+
const parent = projects[parentKey];
|
|
89
|
+
if (!Array.isArray(parent.team) || parent.team.length === 0) return '';
|
|
90
|
+
const self = parent.team.find(m => m.key === memberKey);
|
|
91
|
+
if (!self) return '';
|
|
92
|
+
|
|
93
|
+
const dispatchBin = path.join(METAME_DIR, 'bin', 'dispatch_to');
|
|
94
|
+
const teammates = parent.team.filter(m => m.key !== memberKey);
|
|
95
|
+
|
|
96
|
+
const lines = teammates.map(m => {
|
|
97
|
+
const target = m.peer ? `${m.peer}:${m.key}` : m.key;
|
|
98
|
+
const location = m.peer ? ` [远端:${m.peer}]` : '';
|
|
99
|
+
return `- ${m.key}(${m.name || m.key}${location}): \`${dispatchBin} --from ${memberKey} ${target} "消息"\``;
|
|
100
|
+
});
|
|
101
|
+
// Parent project as escalation target
|
|
102
|
+
lines.push(
|
|
103
|
+
`- ${parentKey}(${parent.name || parentKey}, 向上汇报): \`${dispatchBin} --from ${memberKey} ${parentKey} "消息"\``
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
`[你是团队的一员]`,
|
|
108
|
+
`身份: ${self.key}(${self.name || self.key})`,
|
|
109
|
+
`所属项目: ${parentKey}(${parent.name || parentKey})`,
|
|
110
|
+
``,
|
|
111
|
+
`团队成员(通过 dispatch_to 联络):`,
|
|
112
|
+
...lines,
|
|
113
|
+
].join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveDispatchActor(sourceKey, projects) {
|
|
117
|
+
const rawKey = String(sourceKey || '').trim();
|
|
118
|
+
const userSources = new Set(['', 'unknown', 'claude_session', '_claude_session', 'user']);
|
|
119
|
+
if (userSources.has(rawKey)) return { key: 'user', name: '用户', icon: '👤', isUser: true };
|
|
120
|
+
const proj = projects && projects[rawKey];
|
|
121
|
+
if (proj) return { key: rawKey, name: proj.name || rawKey, icon: proj.icon || '🤖', isUser: false };
|
|
122
|
+
return { key: rawKey || 'unknown', name: rawKey || 'unknown', icon: '🤖', isUser: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildPrivateNowContent({ actor, target, title, prompt, timeStr, dispatchId, taskId, scopeId, chain }) {
|
|
126
|
+
const lines = [
|
|
127
|
+
'# 当前任务',
|
|
128
|
+
`**最后更新**: ${timeStr} **更新者**: ${actor.icon} ${actor.name}`,
|
|
129
|
+
'',
|
|
130
|
+
'## 当前派发',
|
|
131
|
+
`- **目标**: ${target.icon} ${target.name} (${target.key})`,
|
|
132
|
+
`- **任务**: ${title || prompt.slice(0, 120) || '(empty)'}`,
|
|
133
|
+
dispatchId ? `- **编号**: ${dispatchId}` : '',
|
|
134
|
+
taskId ? `- **TeamTask**: ${taskId}` : '',
|
|
135
|
+
scopeId && scopeId !== taskId ? `- **Scope**: ${scopeId}` : '',
|
|
136
|
+
'',
|
|
137
|
+
'## 任务链',
|
|
138
|
+
chain && chain.length > 0 ? chain.join(' → ') : `${actor.key} → ${target.key}`,
|
|
139
|
+
].filter(Boolean);
|
|
140
|
+
return `${lines.join('\n')}\n`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildSharedNowContent({ actor, target, title, prompt, timeStr, dispatchId, taskId, scopeId, chain }) {
|
|
144
|
+
const lines = [
|
|
145
|
+
'# 共享当前状态',
|
|
146
|
+
`**最后更新**: ${timeStr} **更新者**: ${actor.icon} ${actor.name}`,
|
|
147
|
+
'',
|
|
148
|
+
'## 当前任务',
|
|
149
|
+
`- **派发给**: ${target.icon} ${target.name} (${target.key})`,
|
|
150
|
+
`- **任务**: ${title || prompt.slice(0, 120) || '(empty)'}`,
|
|
151
|
+
dispatchId ? `- **编号**: ${dispatchId}` : '',
|
|
152
|
+
taskId ? `- **TeamTask**: ${taskId}` : '',
|
|
153
|
+
scopeId && scopeId !== taskId ? `- **Scope**: ${scopeId}` : '',
|
|
154
|
+
`- **时间**: ${timeStr}`,
|
|
155
|
+
'',
|
|
156
|
+
'## 任务链',
|
|
157
|
+
chain && chain.length > 0 ? chain.join(' → ') : `${actor.key} → ${target.key}`,
|
|
158
|
+
].filter(Boolean);
|
|
159
|
+
return `${lines.join('\n')}\n`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function updateDispatchContextFiles({ fs: fsMod = fs, path: pathMod = path, baseDir = METAME_DIR, fullMsg, targetProject, config, envelope, logger = null }) {
|
|
163
|
+
if (!fullMsg || !targetProject) return { targetNowPath: null, sharedNowPath: null, tasksFilePath: null };
|
|
164
|
+
|
|
165
|
+
const logWarn = (msg) => {
|
|
166
|
+
if (typeof logger === 'function') logger(msg);
|
|
167
|
+
};
|
|
168
|
+
const nowDir = pathMod.join(baseDir, 'memory', 'now');
|
|
169
|
+
const sharedDir = pathMod.join(baseDir, 'memory', 'shared');
|
|
170
|
+
const targetNowPath = pathMod.join(nowDir, `${targetProject}.md`);
|
|
171
|
+
const sharedNowPath = pathMod.join(nowDir, 'shared.md');
|
|
172
|
+
const tasksFilePath = pathMod.join(sharedDir, 'tasks.md');
|
|
173
|
+
fsMod.mkdirSync(nowDir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
const projects = (config && config.projects) || {};
|
|
176
|
+
const actor = resolveDispatchActor((fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from), projects);
|
|
177
|
+
const targetProj = projects[targetProject] || {};
|
|
178
|
+
const target = { key: targetProject, name: targetProj.name || targetProject, icon: targetProj.icon || '🤖' };
|
|
179
|
+
const prompt = String(fullMsg && fullMsg.payload && fullMsg.payload.prompt || '').trim();
|
|
180
|
+
const title = String(fullMsg && fullMsg.payload && fullMsg.payload.title || '').trim();
|
|
181
|
+
const now = new Date();
|
|
182
|
+
const timeStr = now.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', hour12: false });
|
|
183
|
+
const dateStr = now.toISOString().slice(0, 10);
|
|
184
|
+
const taskId = String(envelope && envelope.task_id || '').trim();
|
|
185
|
+
const scopeId = String(envelope && envelope.scope_id || '').trim();
|
|
186
|
+
const isSharedTeamTask = !!(envelope && envelope.task_kind === 'team');
|
|
187
|
+
|
|
188
|
+
fsMod.writeFileSync(targetNowPath, buildPrivateNowContent({
|
|
189
|
+
actor, target, title, prompt, timeStr,
|
|
190
|
+
dispatchId: fullMsg.id, taskId, scopeId, chain: fullMsg.chain,
|
|
191
|
+
}), 'utf8');
|
|
192
|
+
|
|
193
|
+
if (!isSharedTeamTask) return { targetNowPath, sharedNowPath: null, tasksFilePath: null };
|
|
194
|
+
|
|
195
|
+
fsMod.writeFileSync(sharedNowPath, buildSharedNowContent({
|
|
196
|
+
actor, target, title, prompt, timeStr,
|
|
197
|
+
dispatchId: fullMsg.id, taskId, scopeId, chain: fullMsg.chain,
|
|
198
|
+
}), 'utf8');
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
if (!fsMod.existsSync(sharedDir)) fsMod.mkdirSync(sharedDir, { recursive: true });
|
|
202
|
+
const taskLine = `- [${dateStr}] ${actor.icon} ${actor.name} → ${target.icon} ${target.name}: ${title || prompt.slice(0, 40)}`;
|
|
203
|
+
let tasksContent = fsMod.existsSync(tasksFilePath)
|
|
204
|
+
? fsMod.readFileSync(tasksFilePath, 'utf8')
|
|
205
|
+
: '# 任务看板\n\n## 🔄 进行中\n\n## ✅ 已完成\n\n## 📅 待开始\n';
|
|
206
|
+
if (!tasksContent.includes(taskLine)) {
|
|
207
|
+
const lines = tasksContent.split('\n');
|
|
208
|
+
const nextLines = [];
|
|
209
|
+
let inserted = false;
|
|
210
|
+
let inProgress = false;
|
|
211
|
+
for (const line of lines) {
|
|
212
|
+
nextLines.push(line);
|
|
213
|
+
if (line.includes('## 🔄 进行中')) {
|
|
214
|
+
inProgress = true;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (inProgress && line.startsWith('## ')) {
|
|
218
|
+
nextLines.splice(nextLines.length - 1, 0, taskLine);
|
|
219
|
+
inserted = true;
|
|
220
|
+
inProgress = false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!inserted) nextLines.push(taskLine);
|
|
224
|
+
tasksContent = nextLines.join('\n');
|
|
225
|
+
fsMod.writeFileSync(tasksFilePath, tasksContent, 'utf8');
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {
|
|
228
|
+
logWarn(`Failed to update shared task board: ${e.message}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { targetNowPath, sharedNowPath, tasksFilePath };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
235
|
+
// Prompt enrichment (shared context injection)
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Enrich a dispatch prompt with shared context read at send time:
|
|
240
|
+
* 1. now/<target>.md — target private progress handoff
|
|
241
|
+
* 2. now/shared.md — global team progress whiteboard (only when includeShared=true)
|
|
242
|
+
* 2. agents/<target>_latest.md — target's last output
|
|
243
|
+
* 3. inbox/<target>/ — unread messages (archived to read/ after reading)
|
|
244
|
+
*
|
|
245
|
+
* This is a push-model pull: caller enriches just before sending.
|
|
246
|
+
*
|
|
247
|
+
* @param {string} target - project/member key of the recipient
|
|
248
|
+
* @param {string} rawPrompt - original prompt
|
|
249
|
+
* @param {string} [metameDir] - override METAME_DIR (for testing)
|
|
250
|
+
* @returns {string}
|
|
251
|
+
*/
|
|
252
|
+
function buildEnrichedPrompt(target, rawPrompt, metameDir, opts = {}) {
|
|
253
|
+
const base = metameDir || METAME_DIR;
|
|
254
|
+
const includeShared = !!(opts && opts.includeShared);
|
|
255
|
+
let ctx = '';
|
|
256
|
+
|
|
257
|
+
// 1. Target private now file
|
|
258
|
+
try {
|
|
259
|
+
const targetNowFile = path.join(base, 'memory', 'now', `${target}.md`);
|
|
260
|
+
if (fs.existsSync(targetNowFile)) {
|
|
261
|
+
const content = fs.readFileSync(targetNowFile, 'utf8').trim();
|
|
262
|
+
if (content) ctx += `[当前进度 now/${target}.md]\n${content}\n\n`;
|
|
263
|
+
}
|
|
264
|
+
} catch { /* non-critical */ }
|
|
265
|
+
|
|
266
|
+
// 2. Shared progress whiteboard for real team tasks only
|
|
267
|
+
if (includeShared) {
|
|
268
|
+
try {
|
|
269
|
+
const nowFile = path.join(base, 'memory', 'now', 'shared.md');
|
|
270
|
+
if (fs.existsSync(nowFile)) {
|
|
271
|
+
const content = fs.readFileSync(nowFile, 'utf8').trim();
|
|
272
|
+
if (content) ctx += `[共享进度 now/shared.md]\n${content}\n\n`;
|
|
273
|
+
}
|
|
274
|
+
} catch { /* non-critical */ }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 3. Target's last output
|
|
278
|
+
try {
|
|
279
|
+
const latestFile = path.join(base, 'memory', 'agents', `${target}_latest.md`);
|
|
280
|
+
if (fs.existsSync(latestFile)) {
|
|
281
|
+
const content = fs.readFileSync(latestFile, 'utf8').trim();
|
|
282
|
+
if (content) ctx += `[${target} 上次产出]\n${content}\n\n`;
|
|
283
|
+
}
|
|
284
|
+
} catch { /* non-critical */ }
|
|
285
|
+
|
|
286
|
+
// 4. Inbox unread messages (archive after reading)
|
|
287
|
+
try {
|
|
288
|
+
const inboxDir = path.join(base, 'memory', 'inbox', target);
|
|
289
|
+
const readDir = path.join(inboxDir, 'read');
|
|
290
|
+
if (fs.existsSync(inboxDir)) {
|
|
291
|
+
const files = fs.readdirSync(inboxDir).filter(f => f.endsWith('.md')).sort();
|
|
292
|
+
if (files.length > 0) {
|
|
293
|
+
ctx += `[📬 Agent Inbox — ${files.length} 条未读消息]\n`;
|
|
294
|
+
fs.mkdirSync(readDir, { recursive: true });
|
|
295
|
+
for (const f of files) {
|
|
296
|
+
const fp = path.join(inboxDir, f);
|
|
297
|
+
ctx += fs.readFileSync(fp, 'utf8').trim() + '\n---\n';
|
|
298
|
+
fs.renameSync(fp, path.join(readDir, f));
|
|
299
|
+
}
|
|
300
|
+
ctx += '\n';
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
} catch { /* non-critical */ }
|
|
304
|
+
|
|
305
|
+
return ctx ? `${ctx}---\n${rawPrompt}` : rawPrompt;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
module.exports = {
|
|
309
|
+
resolveProjectKey,
|
|
310
|
+
findTeamMember,
|
|
311
|
+
buildTeamRosterHint,
|
|
312
|
+
buildEnrichedPrompt,
|
|
313
|
+
resolveDispatchActor,
|
|
314
|
+
updateDispatchContextFiles,
|
|
315
|
+
};
|