metame-cli 1.5.10 → 1.5.12
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 +49 -6
- package/index.js +266 -72
- package/package.json +7 -3
- package/scripts/daemon-admin-commands.js +34 -0
- package/scripts/daemon-agent-commands.js +6 -2
- package/scripts/daemon-bridges.js +41 -10
- package/scripts/daemon-claude-engine.js +128 -29
- package/scripts/daemon-command-router.js +16 -0
- package/scripts/daemon-command-session-route.js +3 -1
- package/scripts/daemon-default.yaml +3 -1
- package/scripts/daemon-engine-runtime.js +1 -5
- package/scripts/daemon-message-pipeline.js +113 -44
- package/scripts/daemon-ops-commands.js +25 -11
- package/scripts/daemon-reactive-lifecycle.js +757 -76
- package/scripts/daemon-session-commands.js +3 -2
- package/scripts/daemon-session-store.js +82 -27
- package/scripts/daemon-team-dispatch.js +21 -5
- package/scripts/daemon-utils.js +3 -1
- package/scripts/daemon.js +80 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/file-transfer.md +1 -0
- package/scripts/docs/maintenance-manual.md +55 -2
- package/scripts/docs/pointer-map.md +34 -0
- package/scripts/feishu-adapter.js +25 -0
- package/scripts/hooks/intent-file-transfer.js +2 -1
- package/scripts/hooks/intent-perpetual.js +109 -0
- package/scripts/hooks/intent-research.js +112 -0
- package/scripts/intent-registry.js +4 -0
- package/scripts/memory-extract.js +29 -1
- package/scripts/memory-nightly-reflect.js +104 -0
- package/scripts/ops-mission-queue.js +258 -0
- package/scripts/ops-verifier.js +197 -0
- package/scripts/signal-capture.js +3 -3
- package/scripts/skill-evolution.js +11 -2
- package/skills/agent-browser/SKILL.md +153 -0
- package/skills/agent-reach/SKILL.md +66 -0
- package/skills/agent-reach/evolution.json +13 -0
- package/skills/deep-research/SKILL.md +77 -0
- package/skills/find-skills/SKILL.md +133 -0
- package/skills/heartbeat-task-manager/SKILL.md +63 -0
- package/skills/macos-local-orchestrator/SKILL.md +192 -0
- package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
- package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
- package/skills/macos-mail-calendar/SKILL.md +394 -0
- package/skills/mcp-installer/SKILL.md +138 -0
- package/skills/skill-creator/LICENSE.txt +202 -0
- package/skills/skill-creator/README.md +72 -0
- package/skills/skill-creator/SKILL.md +96 -0
- package/skills/skill-creator/evolution.json +6 -0
- package/skills/skill-creator/references/creation-guide.md +116 -0
- package/skills/skill-creator/references/evolution-guide.md +74 -0
- package/skills/skill-creator/references/output-patterns.md +82 -0
- package/skills/skill-creator/references/workflows.md +28 -0
- package/skills/skill-creator/scripts/align_all.py +32 -0
- package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
- package/skills/skill-creator/scripts/init_skill.py +303 -0
- package/skills/skill-creator/scripts/merge_evolution.py +70 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/skills/skill-creator/scripts/setup.py +141 -0
- package/skills/skill-creator/scripts/smart_stitch.py +82 -0
- package/skills/skill-manager/SKILL.md +112 -0
- package/skills/skill-manager/scripts/delete_skill.py +31 -0
- package/skills/skill-manager/scripts/list_skills.py +61 -0
- package/skills/skill-manager/scripts/scan_and_check.py +125 -0
- package/skills/skill-manager/scripts/sync_index.py +144 -0
- package/skills/skill-manager/scripts/update_helper.py +39 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { normalizeEngineName: _normalizeEngine } = require('./daemon-utils');
|
|
4
|
+
|
|
3
5
|
function createSessionCommandHandler(deps) {
|
|
4
6
|
const {
|
|
5
7
|
fs,
|
|
@@ -29,8 +31,7 @@ function createSessionCommandHandler(deps) {
|
|
|
29
31
|
} = deps;
|
|
30
32
|
|
|
31
33
|
function normalizeEngineName(name) {
|
|
32
|
-
|
|
33
|
-
return n === 'codex' ? 'codex' : getDefaultEngine();
|
|
34
|
+
return _normalizeEngine(name, getDefaultEngine);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function inferStoredEngine(rawSession) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { normalizeEngineName } = require('./daemon-utils');
|
|
4
5
|
|
|
5
6
|
function normalizeCodexSandboxMode(value, fallback = null) {
|
|
6
7
|
const text = String(value || '').trim().toLowerCase();
|
|
@@ -45,10 +46,6 @@ function normalizeCodexPermissionMeta(meta = {}) {
|
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
function normalizeEngineName(name) {
|
|
49
|
-
return String(name || 'claude').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
49
|
function stripCodexInjectedHints(text) {
|
|
53
50
|
return String(text || '')
|
|
54
51
|
.replace(/\r\n/g, '\n')
|
|
@@ -217,17 +214,36 @@ function createSessionStore(deps) {
|
|
|
217
214
|
}
|
|
218
215
|
|
|
219
216
|
// [M3] 共享辅助:从 reversed JSONL 行数组中提取最后一条外部用户消息(统一规则)
|
|
217
|
+
function _extractMessageText(d) {
|
|
218
|
+
const content = d.message && d.message.content;
|
|
219
|
+
let raw = typeof content === 'string' ? content
|
|
220
|
+
: Array.isArray(content) ? (content.find(c => c.type === 'text') || {}).text || '' : '';
|
|
221
|
+
raw = raw.replace(/\[System hints[\s\S]*/i, '')
|
|
222
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '').trim();
|
|
223
|
+
return raw;
|
|
224
|
+
}
|
|
225
|
+
|
|
220
226
|
function extractLastUserFromLines(lines) {
|
|
221
227
|
for (const line of lines) {
|
|
222
228
|
if (!line) continue;
|
|
223
229
|
try {
|
|
224
230
|
const d = JSON.parse(line);
|
|
225
231
|
if (d.type === 'user' && d.message && d.userType !== 'internal') {
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
232
|
+
const raw = _extractMessageText(d);
|
|
233
|
+
if (raw.length > 2) return raw.slice(0, 80);
|
|
234
|
+
}
|
|
235
|
+
} catch { /* skip */ }
|
|
236
|
+
}
|
|
237
|
+
return '';
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function extractLastAssistantFromLines(lines) {
|
|
241
|
+
for (const line of lines) {
|
|
242
|
+
if (!line) continue;
|
|
243
|
+
try {
|
|
244
|
+
const d = JSON.parse(line);
|
|
245
|
+
if (d.type === 'assistant' && d.message) {
|
|
246
|
+
const raw = _extractMessageText(d);
|
|
231
247
|
if (raw.length > 2) return raw.slice(0, 80);
|
|
232
248
|
}
|
|
233
249
|
} catch { /* skip */ }
|
|
@@ -256,11 +272,30 @@ function createSessionStore(deps) {
|
|
|
256
272
|
}
|
|
257
273
|
}
|
|
258
274
|
}
|
|
259
|
-
// Fallback: decode projectPath from directory name (
|
|
275
|
+
// Fallback: decode projectPath from directory name (macOS: -Users-foo → /Users/foo)
|
|
260
276
|
if (!projPathCache.has(proj) && proj.startsWith('-')) {
|
|
261
277
|
const decoded = proj.replace(/-/g, '/');
|
|
262
278
|
if (fs.existsSync(decoded)) projPathCache.set(proj, decoded);
|
|
263
279
|
}
|
|
280
|
+
// Fallback 2: read cwd from first JSONL entry (works on all platforms,
|
|
281
|
+
// handles directory names that can't be reliably decoded, e.g. Windows paths
|
|
282
|
+
// with drive letters or filenames containing hyphens)
|
|
283
|
+
if (!projPathCache.has(proj)) {
|
|
284
|
+
try {
|
|
285
|
+
const _jsonls = fs.readdirSync(projDir).filter(f => f.endsWith('.jsonl'));
|
|
286
|
+
if (_jsonls.length > 0) {
|
|
287
|
+
const _fd = fs.openSync(path.join(projDir, _jsonls[0]), 'r');
|
|
288
|
+
try {
|
|
289
|
+
const _buf = Buffer.alloc(4096);
|
|
290
|
+
const _bytes = fs.readSync(_fd, _buf, 0, 4096, 0);
|
|
291
|
+
for (const _line of _buf.toString('utf8', 0, _bytes).split('\n')) {
|
|
292
|
+
if (!_line) continue;
|
|
293
|
+
try { const _d = JSON.parse(_line); if (_d.cwd) { projPathCache.set(proj, path.resolve(_d.cwd)); break; } } catch {}
|
|
294
|
+
}
|
|
295
|
+
} finally { fs.closeSync(_fd); }
|
|
296
|
+
}
|
|
297
|
+
} catch {}
|
|
298
|
+
}
|
|
264
299
|
} catch { /* skip */ }
|
|
265
300
|
|
|
266
301
|
try {
|
|
@@ -287,6 +322,8 @@ function createSessionStore(deps) {
|
|
|
287
322
|
}
|
|
288
323
|
|
|
289
324
|
const all = Array.from(sessionMap.values()).map((entry) => ({ ...entry, engine: 'claude' }));
|
|
325
|
+
// Sort by recency BEFORE enrichment so we enrich the most recent sessions first
|
|
326
|
+
all.sort((a, b) => (b.fileMtime || 0) - (a.fileMtime || 0));
|
|
290
327
|
const ENRICH_LIMIT = 20;
|
|
291
328
|
for (let i = 0; i < Math.min(all.length, ENRICH_LIMIT); i++) {
|
|
292
329
|
const s = all[i];
|
|
@@ -338,6 +375,9 @@ function createSessionStore(deps) {
|
|
|
338
375
|
if (!s.lastUser) {
|
|
339
376
|
s.lastUser = extractLastUserFromLines(tailLines);
|
|
340
377
|
}
|
|
378
|
+
if (!s.lastAssistant) {
|
|
379
|
+
s.lastAssistant = extractLastAssistantFromLines(tailLines);
|
|
380
|
+
}
|
|
341
381
|
} finally {
|
|
342
382
|
fs.closeSync(fd);
|
|
343
383
|
}
|
|
@@ -613,6 +653,13 @@ function createSessionStore(deps) {
|
|
|
613
653
|
return s.sessionId ? s.sessionId.slice(0, 8) : '';
|
|
614
654
|
}
|
|
615
655
|
|
|
656
|
+
// ── Display helpers (shared by sessionRichLabel & buildSessionCardElements) ──
|
|
657
|
+
const _escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
658
|
+
function _cleanSnippet(raw, maxLen) {
|
|
659
|
+
if (!raw || raw.length <= 2) return '';
|
|
660
|
+
return _escapeMd(raw.replace(/\n/g, ' ').slice(0, maxLen)) + (raw.length > maxLen ? '…' : '');
|
|
661
|
+
}
|
|
662
|
+
|
|
616
663
|
function sessionRichLabel(s, index, sessionTags) {
|
|
617
664
|
sessionTags = sessionTags || loadSessionTags();
|
|
618
665
|
const title = sessionDisplayTitle(s, 50, sessionTags);
|
|
@@ -622,17 +669,15 @@ function createSessionStore(deps) {
|
|
|
622
669
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
|
|
623
670
|
const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
|
|
624
671
|
|
|
625
|
-
|
|
626
|
-
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
627
|
-
// fallback to firstPrompt when lastUser not found in tail
|
|
628
|
-
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
629
|
-
let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`; // [M4] title 已有 sessionId 兜底,不会为空
|
|
672
|
+
let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`;
|
|
630
673
|
if (tags.length) line += ` ${tags.map(t => `#${t}`).join(' ')}`;
|
|
631
674
|
line += `\n 📁${proj} · ${ago} · ${engineLabel}`;
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
675
|
+
const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
|
|
676
|
+
const lastUserSnippet = _cleanSnippet(s.lastUser, 50);
|
|
677
|
+
const lastAiSnippet = _cleanSnippet(s.lastAssistant, 50);
|
|
678
|
+
if (firstSnippet) line += `\n 📝 ${firstSnippet}`;
|
|
679
|
+
if (lastUserSnippet && lastUserSnippet !== firstSnippet) line += `\n 💬 ${lastUserSnippet}`;
|
|
680
|
+
if (lastAiSnippet) line += `\n 🤖 ${lastAiSnippet}`;
|
|
636
681
|
line += `\n /resume ${shortId}`;
|
|
637
682
|
return line;
|
|
638
683
|
}
|
|
@@ -648,15 +693,16 @@ function createSessionStore(deps) {
|
|
|
648
693
|
const shortId = s.sessionId.slice(0, 6);
|
|
649
694
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
|
|
650
695
|
const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
|
|
651
|
-
|
|
652
|
-
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
653
|
-
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
696
|
+
|
|
654
697
|
let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago} · ${engineLabel}`;
|
|
655
698
|
if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
699
|
+
// Show first prompt, last user message, and last assistant reply
|
|
700
|
+
const firstSnippet = _cleanSnippet(s.firstPrompt, 50);
|
|
701
|
+
const lastUserSnippet = _cleanSnippet(s.lastUser, 50);
|
|
702
|
+
const lastAiSnippet = _cleanSnippet(s.lastAssistant, 50);
|
|
703
|
+
if (firstSnippet) desc += `\n📝 ${firstSnippet}`;
|
|
704
|
+
if (lastUserSnippet && lastUserSnippet !== firstSnippet) desc += `\n💬 ${lastUserSnippet}`;
|
|
705
|
+
if (lastAiSnippet) desc += `\n🤖 ${lastAiSnippet}`;
|
|
660
706
|
elements.push({ tag: 'div', text: { tag: 'lark_md', content: desc } });
|
|
661
707
|
elements.push({ tag: 'action', actions: [{ tag: 'button', text: { tag: 'plain_text', content: `▶️ Switch #${shortId}` }, type: 'primary', value: { cmd: `/resume ${s.sessionId}` } }] });
|
|
662
708
|
});
|
|
@@ -924,6 +970,9 @@ function createSessionStore(deps) {
|
|
|
924
970
|
}
|
|
925
971
|
slot.started = true;
|
|
926
972
|
s.last_active = Date.now();
|
|
973
|
+
// Clear stale findSessionFile cache: the JSONL/SQLite file now exists
|
|
974
|
+
// but may have been cached as null during createSession (before CLI created it).
|
|
975
|
+
if (slot.id) clearSessionFileCache(slot.id);
|
|
927
976
|
} else {
|
|
928
977
|
s.started = true; // old flat format
|
|
929
978
|
s.last_active = Date.now();
|
|
@@ -970,7 +1019,13 @@ function createSessionStore(deps) {
|
|
|
970
1019
|
// Best approach: read cwd directly from session file content (not from dir name)
|
|
971
1020
|
function _isClaudeSessionValid(sessionId, normCwd) {
|
|
972
1021
|
try {
|
|
973
|
-
|
|
1022
|
+
let sessionFile = findSessionFile(sessionId);
|
|
1023
|
+
if (!sessionFile) {
|
|
1024
|
+
// Cache may hold a stale null from createSession (before CLI wrote the JSONL).
|
|
1025
|
+
// Clear and retry once to avoid false invalidation.
|
|
1026
|
+
clearSessionFileCache(sessionId);
|
|
1027
|
+
sessionFile = findSessionFile(sessionId);
|
|
1028
|
+
}
|
|
974
1029
|
if (!sessionFile) {
|
|
975
1030
|
log('WARN', `[SessionValid] ${sessionId.slice(0, 8)}: JSONL file not found`);
|
|
976
1031
|
return false;
|
|
@@ -274,15 +274,31 @@ function buildEnrichedPrompt(target, rawPrompt, metameDir, opts = {}) {
|
|
|
274
274
|
} catch { /* non-critical */ }
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
// 3.
|
|
277
|
+
// 3+5. Structured memory (L1+L2) OR legacy _latest.md fallback
|
|
278
|
+
// Single stat — structured memory supersedes raw last output
|
|
279
|
+
let hasStructuredMemory = false;
|
|
278
280
|
try {
|
|
279
|
-
const
|
|
280
|
-
if (fs.existsSync(
|
|
281
|
-
const content = fs.readFileSync(
|
|
282
|
-
if (content)
|
|
281
|
+
const memFile = path.join(base, 'memory', 'now', `${target}_memory.md`);
|
|
282
|
+
if (fs.existsSync(memFile)) {
|
|
283
|
+
const content = fs.readFileSync(memFile, 'utf8').trim();
|
|
284
|
+
if (content) {
|
|
285
|
+
ctx += `[Memory Context]\n${content}\n\n`;
|
|
286
|
+
hasStructuredMemory = true;
|
|
287
|
+
}
|
|
283
288
|
}
|
|
284
289
|
} catch { /* non-critical */ }
|
|
285
290
|
|
|
291
|
+
if (!hasStructuredMemory) {
|
|
292
|
+
// Fallback: raw last output (for non-reactive projects without memory system)
|
|
293
|
+
try {
|
|
294
|
+
const latestFile = path.join(base, 'memory', 'agents', `${target}_latest.md`);
|
|
295
|
+
if (fs.existsSync(latestFile)) {
|
|
296
|
+
const content = fs.readFileSync(latestFile, 'utf8').trim();
|
|
297
|
+
if (content) ctx += `[${target} 上次产出]\n${content}\n\n`;
|
|
298
|
+
}
|
|
299
|
+
} catch { /* non-critical */ }
|
|
300
|
+
}
|
|
301
|
+
|
|
286
302
|
// 4. Inbox unread messages (archive after reading)
|
|
287
303
|
try {
|
|
288
304
|
const inboxDir = path.join(base, 'memory', 'inbox', target);
|
package/scripts/daemon-utils.js
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
function normalizeEngineName(name, defaultEngine = 'claude') {
|
|
11
11
|
const n = String(name || '').trim().toLowerCase();
|
|
12
|
-
|
|
12
|
+
if (n === 'codex') return 'codex';
|
|
13
|
+
if (n === 'claude') return 'claude';
|
|
14
|
+
return typeof defaultEngine === 'function' ? defaultEngine() : defaultEngine;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
function normalizeCodexSandboxMode(value, fallback = null) {
|
package/scripts/daemon.js
CHANGED
|
@@ -48,6 +48,7 @@ const BRAIN_FILE = path.join(HOME, '.claude_profile.yaml');
|
|
|
48
48
|
const DISPATCH_DIR = path.join(METAME_DIR, 'dispatch');
|
|
49
49
|
const DISPATCH_LOG = path.join(DISPATCH_DIR, 'dispatch-log.jsonl');
|
|
50
50
|
const { sleepSync, socketPath, needsSocketCleanup } = require('./platform');
|
|
51
|
+
const { handleReactiveOutput } = require('./daemon-reactive-lifecycle');
|
|
51
52
|
const SOCK_PATH = socketPath(METAME_DIR);
|
|
52
53
|
|
|
53
54
|
// Resolve claude binary path (daemon may not inherit user's full PATH)
|
|
@@ -286,12 +287,28 @@ function restoreConfig() {
|
|
|
286
287
|
if (!fs.existsSync(bak)) return false;
|
|
287
288
|
try {
|
|
288
289
|
const bakCfg = yaml.load(fs.readFileSync(bak, 'utf8')) || {};
|
|
289
|
-
// Preserve
|
|
290
|
-
//
|
|
290
|
+
// Preserve ALL user-critical fields from current config so /fix never
|
|
291
|
+
// loses secrets, chat IDs, or agent mappings
|
|
291
292
|
let curCfg = {};
|
|
292
293
|
try { curCfg = yaml.load(fs.readFileSync(CONFIG_FILE, 'utf8')) || {}; } catch { }
|
|
294
|
+
// Secret fields that must NEVER be reverted by a restore
|
|
295
|
+
const SECRET_FIELDS = ['app_id', 'app_secret', 'bot_token', 'operator_ids'];
|
|
293
296
|
for (const adapter of ['feishu', 'telegram']) {
|
|
294
297
|
if (curCfg[adapter] && bakCfg[adapter]) {
|
|
298
|
+
// Preserve secrets: current config always wins
|
|
299
|
+
for (const field of SECRET_FIELDS) {
|
|
300
|
+
if (curCfg[adapter][field] != null) {
|
|
301
|
+
bakCfg[adapter][field] = curCfg[adapter][field];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Preserve remote_dispatch secrets
|
|
305
|
+
if (curCfg[adapter].remote_dispatch && bakCfg[adapter].remote_dispatch) {
|
|
306
|
+
if (curCfg[adapter].remote_dispatch.secret) {
|
|
307
|
+
bakCfg[adapter].remote_dispatch.secret = curCfg[adapter].remote_dispatch.secret;
|
|
308
|
+
}
|
|
309
|
+
} else if (curCfg[adapter].remote_dispatch) {
|
|
310
|
+
bakCfg[adapter].remote_dispatch = curCfg[adapter].remote_dispatch;
|
|
311
|
+
}
|
|
295
312
|
const curIds = curCfg[adapter].allowed_chat_ids || [];
|
|
296
313
|
const bakIds = bakCfg[adapter].allowed_chat_ids || [];
|
|
297
314
|
// Union of both lists
|
|
@@ -301,8 +318,15 @@ function restoreConfig() {
|
|
|
301
318
|
bakCfg[adapter].chat_agent_map = Object.assign(
|
|
302
319
|
{}, bakCfg[adapter].chat_agent_map || {}, curCfg[adapter].chat_agent_map || {}
|
|
303
320
|
);
|
|
321
|
+
} else if (curCfg[adapter] && !bakCfg[adapter]) {
|
|
322
|
+
// Backup doesn't have this adapter at all — keep current entirely
|
|
323
|
+
bakCfg[adapter] = curCfg[adapter];
|
|
304
324
|
}
|
|
305
325
|
}
|
|
326
|
+
// Preserve projects (current takes precedence for each project key)
|
|
327
|
+
if (curCfg.projects) {
|
|
328
|
+
bakCfg.projects = Object.assign({}, bakCfg.projects || {}, curCfg.projects);
|
|
329
|
+
}
|
|
306
330
|
writeConfigSafe(bakCfg);
|
|
307
331
|
config = loadConfig(); // eslint-disable-line no-undef -- config is declared in main() closure
|
|
308
332
|
return true;
|
|
@@ -1083,6 +1107,37 @@ function dispatchTask(targetProject, message, config, replyFn, streamOptions = n
|
|
|
1083
1107
|
chain: [], // reset chain for callbacks
|
|
1084
1108
|
}, config);
|
|
1085
1109
|
}
|
|
1110
|
+
|
|
1111
|
+
// ── Reactive lifecycle hook ──
|
|
1112
|
+
try {
|
|
1113
|
+
handleReactiveOutput(targetProject, outStr, loadConfig(), {
|
|
1114
|
+
log,
|
|
1115
|
+
loadState,
|
|
1116
|
+
saveState,
|
|
1117
|
+
checkBudget,
|
|
1118
|
+
handleDispatchItem: (item, cfg) => {
|
|
1119
|
+
dispatchTask(item.target, {
|
|
1120
|
+
from: item.from || '_reactive',
|
|
1121
|
+
type: 'reactive',
|
|
1122
|
+
priority: 'normal',
|
|
1123
|
+
new_session: !!item.new_session,
|
|
1124
|
+
payload: { title: 'reactive dispatch', prompt: item.prompt },
|
|
1125
|
+
}, cfg, null, null);
|
|
1126
|
+
},
|
|
1127
|
+
notifyUser: (msg) => {
|
|
1128
|
+
try {
|
|
1129
|
+
const cfg = loadConfig();
|
|
1130
|
+
if (cfg.feishu && cfg.feishu.enabled && cfg.feishu.admin_chat_id) {
|
|
1131
|
+
const { sendFeishuText } = require('./daemon-notify');
|
|
1132
|
+
sendFeishuText(cfg.feishu.admin_chat_id, msg, cfg);
|
|
1133
|
+
}
|
|
1134
|
+
} catch (e) { log('WARN', `Reactive notify failed: ${e.message}`); }
|
|
1135
|
+
},
|
|
1136
|
+
metameDir: path.join(os.homedir(), '.metame'),
|
|
1137
|
+
});
|
|
1138
|
+
} catch (e) {
|
|
1139
|
+
log('ERROR', `Reactive lifecycle error for ${targetProject}: ${e.message}`);
|
|
1140
|
+
}
|
|
1086
1141
|
};
|
|
1087
1142
|
// If streamOptions provided, use real bot so output appears in target's Feishu channel.
|
|
1088
1143
|
// Otherwise fall back to nullBot which captures output for replyFn.
|
|
@@ -1697,6 +1752,27 @@ function physiologicalHeartbeat(config) {
|
|
|
1697
1752
|
} catch (e) {
|
|
1698
1753
|
log('WARN', `Dispatch log rotation failed: ${e.message}`);
|
|
1699
1754
|
}
|
|
1755
|
+
|
|
1756
|
+
// 4. Reconcile perpetual projects — detect stale reactive loops
|
|
1757
|
+
try {
|
|
1758
|
+
const { reconcilePerpetualProjects } = require('./daemon-reactive-lifecycle');
|
|
1759
|
+
reconcilePerpetualProjects(config, {
|
|
1760
|
+
log,
|
|
1761
|
+
loadState,
|
|
1762
|
+
saveState,
|
|
1763
|
+
notifyUser: (msg) => {
|
|
1764
|
+
try {
|
|
1765
|
+
const cfg = loadConfig();
|
|
1766
|
+
if (cfg.feishu && cfg.feishu.enabled && cfg.feishu.admin_chat_id) {
|
|
1767
|
+
const { sendFeishuText } = require('./daemon-notify');
|
|
1768
|
+
sendFeishuText(cfg.feishu.admin_chat_id, msg, cfg);
|
|
1769
|
+
}
|
|
1770
|
+
} catch (e) { log('WARN', `Reconcile notify failed: ${e.message}`); }
|
|
1771
|
+
},
|
|
1772
|
+
});
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
log('WARN', `Reconcile check failed: ${e.message}`);
|
|
1775
|
+
}
|
|
1700
1776
|
}
|
|
1701
1777
|
|
|
1702
1778
|
// ── Timing constants ─────────────────────────────────────────────────────────
|
|
@@ -2276,6 +2352,7 @@ const { handleAgentCommand } = createAgentCommandHandler({
|
|
|
2276
2352
|
writeConfigSafe,
|
|
2277
2353
|
backupConfig,
|
|
2278
2354
|
execSync,
|
|
2355
|
+
log,
|
|
2279
2356
|
});
|
|
2280
2357
|
|
|
2281
2358
|
// Caffeinate process for /nosleep toggle (macOS only)
|
|
@@ -2315,6 +2392,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2315
2392
|
path,
|
|
2316
2393
|
spawn,
|
|
2317
2394
|
execSync,
|
|
2395
|
+
execFileSync,
|
|
2318
2396
|
log,
|
|
2319
2397
|
loadConfig,
|
|
2320
2398
|
loadState,
|
package/scripts/distill.js
CHANGED
|
@@ -221,7 +221,7 @@ ${promptInput}
|
|
|
221
221
|
|
|
222
222
|
fs.mkdirSync(POSTMORTEM_DIR, { recursive: true });
|
|
223
223
|
const day = new Date().toISOString().slice(0, 10);
|
|
224
|
-
const topicSlug = sanitizeSlug(skeleton.intent
|
|
224
|
+
const topicSlug = sanitizeSlug(title || skeleton.intent, `session-${String(skeleton.session_id || '').slice(0, 8)}`);
|
|
225
225
|
const filePath = path.join(POSTMORTEM_DIR, `${day}-${topicSlug}.md`);
|
|
226
226
|
const markdown = [
|
|
227
227
|
`# ${title}`,
|
|
@@ -354,7 +354,60 @@ Claude 看到 hook 注入:
|
|
|
354
354
|
- `/dispatch to windows:hunter <任务>`:手动跨设备派发
|
|
355
355
|
- `/dispatch to 猎手 <任务>`:按昵称解析,自动检测 `member.peer` 走远端
|
|
356
356
|
|
|
357
|
-
## 12.
|
|
357
|
+
## 12. 永续任务系统(Perpetual Task Engine)
|
|
358
|
+
|
|
359
|
+
### 概念
|
|
360
|
+
|
|
361
|
+
永续任务系统允许任何项目作为 reactive 永续循环运行。Agent 产出信号 → daemon 解析 → 门控检查 → 调度下一步。平台完全领域无关,科研、代码审计、文档维护等任何长期任务均可接入。
|
|
362
|
+
|
|
363
|
+
### 核心组件
|
|
364
|
+
|
|
365
|
+
| 组件 | 文件 | 职责 |
|
|
366
|
+
|------|------|------|
|
|
367
|
+
| Reactive Lifecycle | `daemon-reactive-lifecycle.js` | 信号解析、budget/depth gate、事件溯源、verifier 调用、state 生成 |
|
|
368
|
+
| Event Log | `~/.metame/events/<key>.jsonl` | 唯一 Source of Truth,daemon 独占写入 |
|
|
369
|
+
| Manifest | `<cwd>/perpetual.yaml` | 可选项目清单(completion_signal、脚本路径、约束) |
|
|
370
|
+
| Reconciliation | `reconcilePerpetualProjects()` | heartbeat 中零 token 停滞检测 |
|
|
371
|
+
| Status 命令 | `/status perpetual` | 查看所有永续项目的 phase/depth/mission/status |
|
|
372
|
+
|
|
373
|
+
### 接入一个新永续项目
|
|
374
|
+
|
|
375
|
+
1. 在 `daemon.yaml` 中注册项目,添加 `reactive: true`
|
|
376
|
+
2. 在项目目录创建 `CLAUDE.md`(定义 agent 行为)
|
|
377
|
+
3. 可选:创建 `scripts/verifier.js`(阶段门控)
|
|
378
|
+
4. 可选:创建 `perpetual.yaml`(覆盖默认约定)
|
|
379
|
+
5. 可选:创建 `scripts/archiver.js` + `scripts/mission-queue.js`(归档与任务队列)
|
|
380
|
+
|
|
381
|
+
不创建 perpetual.yaml 时,平台使用默认约定:
|
|
382
|
+
- Verifier: `scripts/verifier.js`
|
|
383
|
+
- Archiver: `scripts/archiver.js`
|
|
384
|
+
- Mission Queue: `scripts/mission-queue.js`
|
|
385
|
+
- 完成信号: `MISSION_COMPLETE`
|
|
386
|
+
|
|
387
|
+
### 事件溯源协议
|
|
388
|
+
|
|
389
|
+
所有状态变更记录在 `~/.metame/events/<projectKey>.jsonl`,一行一个 JSON 事件。`now/<key>.md` 和 `workspace/progress.tsv` 都是 event log 的投影(Projection),可随时从 event log 重建。
|
|
390
|
+
|
|
391
|
+
Event 类型:`MISSION_START` / `DISPATCH` / `MEMBER_COMPLETE` / `PHASE_GATE` / `DEPTH_LIMIT` / `BUDGET_LIMIT` / `MISSION_COMPLETE` / `ARCHIVE` / `STALE` / `INFRA_PAUSE`
|
|
392
|
+
|
|
393
|
+
### 设计契约
|
|
394
|
+
|
|
395
|
+
1. **Tolerant Reader**:`replayEventLog` 逐行解析,损坏行 WARN + skip,绝不 crash
|
|
396
|
+
2. **Error Semantic Isolation**:verifier L2b 区分 404(幻觉,打回 agent)和 50x(基建故障,挂起项目通知人类)
|
|
397
|
+
3. **State 由 daemon 生成**:agent 只读 `now/<key>.md`,不负责维护
|
|
398
|
+
|
|
399
|
+
### 故障排查
|
|
400
|
+
|
|
401
|
+
| 症状 | 检查 |
|
|
402
|
+
|------|------|
|
|
403
|
+
| 永续项目不启动 | `daemon.yaml` 中是否有 `reactive: true`? |
|
|
404
|
+
| 完成信号不触发 | 检查 `perpetual.yaml` 中的 `completion_signal` 是否与 CLAUDE.md 一致 |
|
|
405
|
+
| Verifier 不运行 | 检查 `scripts/verifier.js` 路径(或 manifest 中的自定义路径)是否存在 |
|
|
406
|
+
| 项目挂起 (infra_failure) | 外部 API 不可用,检查网络。非 agent 错误。 |
|
|
407
|
+
| Event log 损坏 | 重启后 replay 会跳过损坏行。`progress.tsv` 和 `now/<key>.md` 可从 event log 重建 |
|
|
408
|
+
| `/status perpetual` 无输出 | 确认项目配置了 `reactive: true` |
|
|
409
|
+
|
|
410
|
+
## 13. 私人配置保护(原 §12)
|
|
358
411
|
|
|
359
412
|
- `daemon.yaml` 是用户私人配置,包含 API keys、chat IDs、个人项目配置
|
|
360
413
|
- **绝不上传**到代码仓库,已加入 `.gitignore`
|
|
@@ -363,7 +416,7 @@ Claude 看到 hook 注入:
|
|
|
363
416
|
- 同样不应上传的文件:`MEMORY.md`、`SOUL.md`、`.env*`
|
|
364
417
|
- Agent 在执行任务时,**绝不能** `cp scripts/daemon.yaml ~/.metame/daemon.yaml`,这会覆盖用户私人配置
|
|
365
418
|
|
|
366
|
-
##
|
|
419
|
+
## 14. 变更后维护动作
|
|
367
420
|
|
|
368
421
|
1. `npm test`
|
|
369
422
|
2. `npm run sync:plugin`
|
|
@@ -130,6 +130,39 @@
|
|
|
130
130
|
- `scripts/memory.js`:`saveFactLabels()` 原子写入 API
|
|
131
131
|
- `scripts/memory-nightly-reflect.js`:`synthesized_insight` 回写、知识胶囊聚合与 `knowledge_capsule` 回写
|
|
132
132
|
|
|
133
|
+
## 永续任务系统(Perpetual Task Engine)
|
|
134
|
+
|
|
135
|
+
- Reactive 生命周期引擎:
|
|
136
|
+
- `scripts/daemon-reactive-lifecycle.js`
|
|
137
|
+
- 关键点:`handleReactiveOutput()` 通用任务链引擎(领域无关);
|
|
138
|
+
`parseReactiveSignals()` 信号解析(NEXT_DISPATCH + 可配完成信号);
|
|
139
|
+
`reconcilePerpetualProjects()` 停滞检测(零 token,heartbeat 驱动);
|
|
140
|
+
`replayEventLog()` 事件溯源状态重放;
|
|
141
|
+
`generateStateFile()` 从 event log 生成 `now/<key>.md`(投影);
|
|
142
|
+
`appendEvent()` 追加事件到 `~/.metame/events/<key>.jsonl`(唯一 SoT);
|
|
143
|
+
`loadProjectManifest()` 读取项目 `perpetual.yaml`(convention over config);
|
|
144
|
+
`resolveProjectScripts()` 按约定/manifest 发现 verifier/archiver/mission-queue 脚本
|
|
145
|
+
|
|
146
|
+
- daemon.js 接入点:
|
|
147
|
+
- `scripts/daemon.js` `outputHandler` 内 `handleReactiveOutput` 调用(try/catch 隔离)
|
|
148
|
+
- `scripts/daemon.js` `physiologicalHeartbeat` 内 `reconcilePerpetualProjects` 调用
|
|
149
|
+
|
|
150
|
+
- 可观测命令:
|
|
151
|
+
- `scripts/daemon-admin-commands.js`
|
|
152
|
+
- 关键点:`/status perpetual`(或 `/status reactive`)显示所有永续项目状态
|
|
153
|
+
|
|
154
|
+
- 设计文档:
|
|
155
|
+
- `docs/perpetual-task-system-design.md`(v4,含 4 份附录)
|
|
156
|
+
- `docs/perpetual-task-system-plan.md`(实施计划,Phase A-C)
|
|
157
|
+
|
|
158
|
+
- 项目清单协议:
|
|
159
|
+
- `<cwd>/perpetual.yaml` — 可选,声明 completion_signal / verifier / archiver / mission_queue 路径
|
|
160
|
+
- 不存在时使用默认约定:`scripts/verifier.js`、`scripts/archiver.js`、`scripts/mission-queue.js`、信号 `MISSION_COMPLETE`
|
|
161
|
+
|
|
162
|
+
- 事件日志:
|
|
163
|
+
- `~/.metame/events/<projectKey>.jsonl` — append-only,daemon 独占写入
|
|
164
|
+
- Event 类型:MISSION_START / DISPATCH / MEMBER_COMPLETE / PHASE_GATE / DEPTH_LIMIT / BUDGET_LIMIT / MISSION_COMPLETE / ARCHIVE / STALE / INFRA_PAUSE
|
|
165
|
+
|
|
133
166
|
## 运行时数据位置
|
|
134
167
|
|
|
135
168
|
- 画像:`~/.claude_profile.yaml`
|
|
@@ -141,6 +174,7 @@
|
|
|
141
174
|
- 复盘文档:`~/.metame/memory/postmortems/`
|
|
142
175
|
- Dispatch 队列:`~/.metame/dispatch/pending.jsonl`(本地 socket 降级)
|
|
143
176
|
- 远端 Dispatch 队列:`~/.metame/dispatch/remote-pending.jsonl`(跨设备中继)
|
|
177
|
+
- **永续任务事件日志**:`~/.metame/events/<projectKey>.jsonl`(唯一 SoT,append-only)
|
|
144
178
|
- 共享进度白板:`~/.metame/memory/now/shared.md`
|
|
145
179
|
- Agent 最新产出:`~/.metame/memory/agents/{key}_latest.md`
|
|
146
180
|
- Agent 收件箱:`~/.metame/memory/inbox/{key}/`(未读),`read/`(已归档)
|
|
@@ -91,6 +91,27 @@ function createBot(config) {
|
|
|
91
91
|
appSecret: app_secret,
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Validate credentials by attempting a lightweight API call.
|
|
96
|
+
* Returns { ok: true } or { ok: false, error: string }.
|
|
97
|
+
*/
|
|
98
|
+
async function validateCredentials() {
|
|
99
|
+
try {
|
|
100
|
+
await withTimeout(client.im.chat.list({ params: { page_size: 1 } }), 15000);
|
|
101
|
+
return { ok: true };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const msg = err && err.message || String(err);
|
|
104
|
+
const isAuthError = /invalid|unauthorized|forbidden|token|credential|app_id|app_secret|permission|99991663|99991664|99991665/i.test(msg);
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
error: isAuthError
|
|
108
|
+
? `Feishu credential validation failed (app_id/app_secret may be incorrect): ${msg}`
|
|
109
|
+
: `Feishu API probe failed (network or config issue): ${msg}`,
|
|
110
|
+
isAuthError,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
// Private: send an interactive card JSON; returns { message_id } or null.
|
|
95
116
|
// All card functions funnel through here to avoid repeating the SDK call.
|
|
96
117
|
async function _sendInteractive(chatId, card) {
|
|
@@ -106,6 +127,7 @@ function createBot(config) {
|
|
|
106
127
|
let _editBrokenAt = 0; // timestamp when broken; auto-resets after 10min
|
|
107
128
|
|
|
108
129
|
return {
|
|
130
|
+
validateCredentials,
|
|
109
131
|
/**
|
|
110
132
|
* Send a plain text message
|
|
111
133
|
*/
|
|
@@ -526,6 +548,9 @@ function createBot(config) {
|
|
|
526
548
|
reconnect() {
|
|
527
549
|
_log('INFO', 'Feishu manual reconnect triggered');
|
|
528
550
|
reconnectDelay = 5000;
|
|
551
|
+
clearTimeout(reconnectTimer);
|
|
552
|
+
try { currentWs?.stop?.(); } catch { /* ignore */ }
|
|
553
|
+
currentWs = null;
|
|
529
554
|
connect();
|
|
530
555
|
},
|
|
531
556
|
isAlive() {
|
|
@@ -35,9 +35,10 @@ module.exports = function detectFileTransfer(prompt) {
|
|
|
35
35
|
|
|
36
36
|
if (isSend) {
|
|
37
37
|
hints.push(
|
|
38
|
-
'- **发送文件到手机**:在回复末尾加 `[[FILE:/absolute/path]]`,daemon
|
|
38
|
+
'- **发送文件到手机**:在回复末尾加 `[[FILE:/absolute/path]]`,daemon 自动发到**当前对话群**',
|
|
39
39
|
'- 多个文件用多个 `[[FILE:...]]` 标记',
|
|
40
40
|
'- **不要读取文件内容再复述**,直接用标记发送(省 token)',
|
|
41
|
+
'- **⛔ 严禁发到 open_id**:即使上下文中有 `ou_...` 用户ID,也绝对不用它发文件——那会发到 bot 私聊而非当前群',
|
|
41
42
|
);
|
|
42
43
|
}
|
|
43
44
|
|