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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Perpetual Task Intent Module
|
|
5
|
+
*
|
|
6
|
+
* Detects when the user wants to interact with perpetual/reactive projects
|
|
7
|
+
* (start research, check progress, manage missions, dispatch to agents).
|
|
8
|
+
*
|
|
9
|
+
* HIGH PRECISION: Only fires on explicit perpetual task verbs + target patterns.
|
|
10
|
+
* Will NOT fire on casual mentions of "研究" in conversational context.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} prompt
|
|
13
|
+
* @param {object} config - daemon.yaml config
|
|
14
|
+
* @param {string} projectKey - current project key
|
|
15
|
+
* @returns {string|null}
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// ── Intent patterns ──
|
|
23
|
+
// Each pattern requires BOTH a verb AND a target/context to fire.
|
|
24
|
+
// Single words like "研究" alone will NOT trigger — must be paired with action.
|
|
25
|
+
|
|
26
|
+
const PERPETUAL_INTENTS = [
|
|
27
|
+
{
|
|
28
|
+
// Direct "永续任务" / "perpetual task" / "长期任务" — highest confidence, no verb needed
|
|
29
|
+
pattern: /永续任务|永续.{0,5}(研究|课题|项目)|perpetual.{0,5}task|长期任务|long.?term.{0,5}(task|mission)/i,
|
|
30
|
+
hint: (config) => {
|
|
31
|
+
const bin = path.join(os.homedir(), '.metame', 'bin', 'dispatch_to');
|
|
32
|
+
const reactive = getReactiveProjects(config);
|
|
33
|
+
if (reactive.length === 0) return '[永续任务] 暂无 reactive 项目。在 daemon.yaml 中添加 `reactive: true` 配置。';
|
|
34
|
+
const targets = reactive.map(r => ` ${r.icon} **${r.name}** → \`${bin} ${r.key} "你的任务描述"\``).join('\n');
|
|
35
|
+
return [
|
|
36
|
+
'[永续任务系统]',
|
|
37
|
+
`可用项目:\n${targets}`,
|
|
38
|
+
'命令: `/status perpetual` 查看进度 | `dispatch_to <key> "任务"` 启动',
|
|
39
|
+
'Agent 内部用 NEXT_DISPATCH 派发子任务,MISSION_COMPLETE 结束任务。',
|
|
40
|
+
].join('\n');
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// User wants to start/launch a perpetual research mission
|
|
45
|
+
// Requires action verb + research/mission target
|
|
46
|
+
pattern: /(开始|启动|开启|launch|start|kick off).{0,15}(研究|课题|实验|mission|research|调研)/i,
|
|
47
|
+
hint: (config) => {
|
|
48
|
+
const bin = path.join(os.homedir(), '.metame', 'bin', 'dispatch_to');
|
|
49
|
+
const reactive = getReactiveProjects(config);
|
|
50
|
+
if (reactive.length === 0) return null;
|
|
51
|
+
const targets = reactive.map(r => ` \`${bin} ${r.key} "开始任务"\``).join('\n');
|
|
52
|
+
return `[永续任务] 启动命令:\n${targets}\n或在 agent 内部用 NEXT_DISPATCH 指令派发子任务。`;
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
// User wants to check perpetual project progress
|
|
57
|
+
pattern: /(查看|看看|检查|check).{0,10}(进度|进展|状态|progress|status).{0,10}(研究|课题|永续|perpetual|reactive)?/i,
|
|
58
|
+
hint: () => '[永续任务] `/status perpetual` 查看所有永续项目进度',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
// User wants to see the topic/mission pool
|
|
62
|
+
pattern: /(课题|任务|mission|topic).{0,8}(池|pool|列表|list|队列|queue)/i,
|
|
63
|
+
hint: () => '[永续任务] 查看任务队列: `cat workspace/topics.md`\n管理: `node scripts/topic-pool.js list`',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// User wants to pause/stop a perpetual project
|
|
67
|
+
pattern: /(暂停|停止|pause|stop).{0,15}(研究|科研|课题|永续|perpetual|reactive)/i,
|
|
68
|
+
hint: () => '[永续任务] 暂停方式:\n- Budget/depth 超限自动暂停\n- 或手动设置 `daemon_state.json` 中 `reactive.<key>.status = "paused"`',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
// User asks about event log or progress history
|
|
72
|
+
pattern: /(事件|event).{0,5}(日志|log)|progress\.tsv|进度日志/i,
|
|
73
|
+
hint: () => '[永续任务] 事件日志: `tail ~/.metame/events/<project>.jsonl`\n进度表: `cat workspace/progress.tsv`',
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
function getReactiveProjects(config) {
|
|
78
|
+
if (!config || !config.projects) return [];
|
|
79
|
+
return Object.entries(config.projects)
|
|
80
|
+
.filter(([, proj]) => proj && proj.reactive)
|
|
81
|
+
.map(([key, proj]) => ({ key, name: proj.name || key, icon: proj.icon || '' }));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Negative patterns: these look like perpetual intent but are actually
|
|
85
|
+
// one-shot skills (deep-research, casual mention, etc.)
|
|
86
|
+
const NEGATIVE_PATTERNS = [
|
|
87
|
+
/深度研究|深度调研|deep.?research|调研.{0,5}(一下|下|看看)/i, // deep-research skill
|
|
88
|
+
/研究.{0,5}(一下|下|看看|这个|那个|怎么)/i, // casual "look into this" (Chinese)
|
|
89
|
+
/\bresearch\b.{0,5}\b(this|it|that|the|how|what|why)\b/i, // casual "research this/how" (English)
|
|
90
|
+
/搜索|搜一下|查一下|google/i, // web search (not English "search" alone)
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
module.exports = function detectPerpetual(prompt, config) {
|
|
94
|
+
const text = String(prompt || '').trim();
|
|
95
|
+
if (!text) return null;
|
|
96
|
+
|
|
97
|
+
// Bail early if text matches a negative pattern (one-shot, not perpetual)
|
|
98
|
+
if (NEGATIVE_PATTERNS.some(re => re.test(text))) return null;
|
|
99
|
+
|
|
100
|
+
const hints = [];
|
|
101
|
+
for (const intent of PERPETUAL_INTENTS) {
|
|
102
|
+
if (intent.pattern.test(text)) {
|
|
103
|
+
const h = typeof intent.hint === 'function' ? intent.hint(config) : intent.hint;
|
|
104
|
+
if (h) hints.push(h);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return hints.length > 0 ? hints.join('\n') : null;
|
|
109
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Research Intent Module — 科研场景按需注入
|
|
5
|
+
*
|
|
6
|
+
* 仅在用户/agent 涉及科研操作时注入对应提示:
|
|
7
|
+
* - 文献调研 → 注入调研方法论
|
|
8
|
+
* - 实验设计 → 注入实验规范
|
|
9
|
+
* - 论文写作 → 注入写作标准
|
|
10
|
+
* - 任务推进 → 注入永续循环协议
|
|
11
|
+
* - 数据分析 → 注入数据溯源规则
|
|
12
|
+
*
|
|
13
|
+
* 只对 paper_rev 项目生效。
|
|
14
|
+
*
|
|
15
|
+
* @param {string} prompt
|
|
16
|
+
* @param {object} config
|
|
17
|
+
* @param {string} projectKey
|
|
18
|
+
* @returns {string|null}
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const RESEARCH_INTENTS = [
|
|
22
|
+
{
|
|
23
|
+
// 文献调研
|
|
24
|
+
pattern: /文献|调研|literature|survey|paper|论文.{0,5}(搜|找|查)|state.of.the.art|相关工作|related work/i,
|
|
25
|
+
hint: [
|
|
26
|
+
'[研究方法:文献调研]',
|
|
27
|
+
'1. WebSearch 搜索最新论文(限近3年,关注顶刊 CACAIE/ASCE/CMAME)',
|
|
28
|
+
'2. 每篇记录:标题、方法、数据集、核心发现、与本研究的关联',
|
|
29
|
+
'3. 产出写入 workspace/literature/<topic>.md',
|
|
30
|
+
'4. 识别 research gap 后更新 workspace/research-state.md',
|
|
31
|
+
].join('\n'),
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
// 算法设计/实验设计
|
|
35
|
+
pattern: /算法|设计|design|实验方案|experiment|PINNs?|Bayesian|贝叶斯|POD|DMD|强化学习|reinforcement|EnKF|降阶/i,
|
|
36
|
+
hint: [
|
|
37
|
+
'[研究方法:算法设计]',
|
|
38
|
+
'1. 先确认相关文献调研已完成(查 workspace/literature/)',
|
|
39
|
+
'2. 设计写入 workspace/experiments/<name>/design.md',
|
|
40
|
+
'3. 明确:输入数据、模型架构、损失函数、评价指标、baseline 对比方案',
|
|
41
|
+
'4. 如需 FEM 数据支持,NEXT_DISPATCH: fem_sim "任务描述"',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
// 代码实现
|
|
46
|
+
pattern: /代码|编写|实现|implement|coding|训练|training|PyTorch|TensorFlow|numpy|脚本/i,
|
|
47
|
+
hint: [
|
|
48
|
+
'[研究方法:代码实现]',
|
|
49
|
+
'1. 代码写入 workspace/experiments/<name>/ 目录',
|
|
50
|
+
'2. 试验数据引用: fem-workspace/experimental_data.csv, optimization_history.csv',
|
|
51
|
+
'3. 运行后记录结果到 workspace/experiments/<name>/results.md',
|
|
52
|
+
'4. 所有数值必须可溯源,不编造结果',
|
|
53
|
+
].join('\n'),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
// 论文撰写
|
|
57
|
+
pattern: /撰写|写作|draft|论文.{0,3}(写|改|稿)|manuscript|章节|section|abstract|introduction|conclusion/i,
|
|
58
|
+
hint: [
|
|
59
|
+
'[研究方法:论文撰写]',
|
|
60
|
+
'目标期刊: Computer-Aided Civil and Infrastructure Engineering (CACAIE)',
|
|
61
|
+
'写作规范: 被动语态适度、hedging language、术语一致、每段有 topic sentence',
|
|
62
|
+
'产出写入 workspace/drafts/<section>.md',
|
|
63
|
+
].join('\n'),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// 任务推进/永续循环
|
|
67
|
+
pattern: /任务|mission|下一步|next|推进|继续|开始|start|进度|progress|REVISION_COMPLETE/i,
|
|
68
|
+
hint: [
|
|
69
|
+
'[永续研究循环协议]',
|
|
70
|
+
'1. 读 workspace/missions.md 取最高优先级 pending 任务',
|
|
71
|
+
'2. 读 workspace/research-state.md 了解当前进度',
|
|
72
|
+
'3. 执行任务,产出写入对应 workspace/ 子目录',
|
|
73
|
+
'4. 更新 research-state.md 记录进展',
|
|
74
|
+
'5. 发现新问题 → 编辑 missions.md 追加 pending',
|
|
75
|
+
'6. 完成 → 输出 REVISION_COMPLETE',
|
|
76
|
+
'7. 派发 FEM 任务 → NEXT_DISPATCH: fem_sim "描述"',
|
|
77
|
+
].join('\n'),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
// 数据分析
|
|
81
|
+
pattern: /数据|分析|analyze|对比|比较|benchmark|baseline|性能|结果|results|R²|RMSE|score/i,
|
|
82
|
+
hint: [
|
|
83
|
+
'[研究方法:数据分析]',
|
|
84
|
+
'Baseline: SGDE 框架 multi-objective score=0.9685, R²=0.9642',
|
|
85
|
+
'可用数据源: fem-workspace/ 下所有 CSV 和 optimization_results/',
|
|
86
|
+
'分析结果写入对应实验目录的 results.md',
|
|
87
|
+
].join('\n'),
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
// 负面模式:排除日常对话
|
|
92
|
+
const NEGATIVE = [
|
|
93
|
+
/你好|谢谢|OK|好的|收到/i,
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
module.exports = function detectResearch(prompt, config, projectKey) {
|
|
97
|
+
// 只对 paper_rev 项目生效
|
|
98
|
+
if (projectKey && projectKey !== 'paper_rev') return null;
|
|
99
|
+
|
|
100
|
+
const text = String(prompt || '').trim();
|
|
101
|
+
if (!text || text.length < 4) return null;
|
|
102
|
+
if (NEGATIVE.some(re => re.test(text) && text.length < 10)) return null;
|
|
103
|
+
|
|
104
|
+
const hints = [];
|
|
105
|
+
for (const intent of RESEARCH_INTENTS) {
|
|
106
|
+
if (intent.pattern.test(text)) {
|
|
107
|
+
hints.push(intent.hint);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return hints.length > 0 ? hints.join('\n\n') : null;
|
|
112
|
+
};
|
|
@@ -14,6 +14,8 @@ const DEFAULTS = Object.freeze({
|
|
|
14
14
|
file_transfer: true,
|
|
15
15
|
memory_recall: true,
|
|
16
16
|
doc_router: true,
|
|
17
|
+
perpetual: true,
|
|
18
|
+
research: true,
|
|
17
19
|
});
|
|
18
20
|
|
|
19
21
|
const INTENT_MODULES = Object.freeze({
|
|
@@ -23,6 +25,8 @@ const INTENT_MODULES = Object.freeze({
|
|
|
23
25
|
file_transfer: require('./hooks/intent-file-transfer'),
|
|
24
26
|
memory_recall: require('./hooks/intent-memory-recall'),
|
|
25
27
|
doc_router: require('./hooks/intent-doc-router'),
|
|
28
|
+
perpetual: require('./hooks/intent-perpetual'),
|
|
29
|
+
research: require('./hooks/intent-research'),
|
|
26
30
|
});
|
|
27
31
|
|
|
28
32
|
function resolveEnabledIntents(config = {}) {
|
|
@@ -74,6 +74,7 @@ const SESSION_TAGS_FILE = path.join(os.homedir(), '.metame', 'session_tags.json'
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* Persist session name and derived tags to ~/.metame/session_tags.json.
|
|
77
|
+
* Consumed by /sessions and /resume commands for friendly display titles.
|
|
77
78
|
* Merges into existing file — never overwrites existing entries.
|
|
78
79
|
*/
|
|
79
80
|
function saveSessionTag(sessionId, sessionName, facts) {
|
|
@@ -337,6 +338,19 @@ async function run() {
|
|
|
337
338
|
|
|
338
339
|
sessionAnalytics.markFactsExtracted(skeleton.session_id);
|
|
339
340
|
|
|
341
|
+
// Persist session summary to memory.db sessions table (makes sessions searchable)
|
|
342
|
+
try {
|
|
343
|
+
const keywords = facts.flatMap(f => Array.isArray(f.tags) ? f.tags : [])
|
|
344
|
+
.filter((v, i, a) => a.indexOf(v) === i).slice(0, 10).join(',');
|
|
345
|
+
memory.saveSession({
|
|
346
|
+
sessionId: skeleton.session_id,
|
|
347
|
+
project: skeleton.project || 'unknown',
|
|
348
|
+
scope: skeleton.project_id || null,
|
|
349
|
+
summary: `[${session_name}] ${facts.map(f => f.value).join(' | ').slice(0, 2000)}`,
|
|
350
|
+
keywords,
|
|
351
|
+
});
|
|
352
|
+
} catch { /* non-fatal — facts already saved, session is bonus */ }
|
|
353
|
+
|
|
340
354
|
// P2-A: persist session name + tags to session_tags.json
|
|
341
355
|
saveSessionTag(skeleton.session_id, session_name, facts);
|
|
342
356
|
|
|
@@ -391,12 +405,26 @@ async function run() {
|
|
|
391
405
|
const superMsg = superseded > 0 ? `, ${superseded} superseded` : '';
|
|
392
406
|
const labelMsg = labelsSaved > 0 ? `, ${labelsSaved} labels` : '';
|
|
393
407
|
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): ${saved} facts saved${superMsg}${labelMsg}`);
|
|
408
|
+
|
|
409
|
+
// Persist Codex session summary to memory.db sessions table
|
|
410
|
+
try {
|
|
411
|
+
const keywords = facts.flatMap(f => Array.isArray(f.tags) ? f.tags : [])
|
|
412
|
+
.filter((v, i, a) => a.indexOf(v) === i).slice(0, 10).join(',');
|
|
413
|
+
memory.saveSession({
|
|
414
|
+
sessionId: cs.session_id,
|
|
415
|
+
project: skeleton.project || 'unknown',
|
|
416
|
+
scope: skeleton.project_id || null,
|
|
417
|
+
summary: `[${session_name}] ${facts.map(f => f.value).join(' | ').slice(0, 2000)}`,
|
|
418
|
+
keywords,
|
|
419
|
+
});
|
|
420
|
+
} catch { /* non-fatal */ }
|
|
421
|
+
|
|
422
|
+
saveSessionTag(cs.session_id, session_name, facts);
|
|
394
423
|
} else {
|
|
395
424
|
console.log(`[memory-extract] Codex ${cs.session_id.slice(0, 8)} (${session_name}): no facts extracted`);
|
|
396
425
|
}
|
|
397
426
|
|
|
398
427
|
sessionAnalytics.markCodexFactsExtracted(cs.session_id);
|
|
399
|
-
saveSessionTag(cs.session_id, session_name, facts);
|
|
400
428
|
processed++;
|
|
401
429
|
} catch (e) {
|
|
402
430
|
console.log(`[memory-extract] Codex session error: ${e.message}`);
|
|
@@ -503,6 +503,109 @@ entity_prefix: ${group.prefix}
|
|
|
503
503
|
try { if (memory && typeof memory.close === 'function') memory.close(); } catch { /* non-fatal */ }
|
|
504
504
|
}
|
|
505
505
|
|
|
506
|
+
// ── Conflict Resolution ──────────────────────────────────────────────
|
|
507
|
+
// Query CONFLICT facts grouped by entity+relation, ask Haiku to pick winner.
|
|
508
|
+
// Loser is marked superseded_by winner; winner restored to OK.
|
|
509
|
+
let conflictsResolved = 0;
|
|
510
|
+
try {
|
|
511
|
+
const conflictGroups = db.prepare(`
|
|
512
|
+
SELECT entity, relation, COUNT(*) as cnt
|
|
513
|
+
FROM facts
|
|
514
|
+
WHERE conflict_status = 'CONFLICT' AND superseded_by IS NULL
|
|
515
|
+
GROUP BY entity, relation
|
|
516
|
+
HAVING cnt >= 2
|
|
517
|
+
ORDER BY cnt DESC
|
|
518
|
+
LIMIT 10
|
|
519
|
+
`).all();
|
|
520
|
+
|
|
521
|
+
if (conflictGroups.length > 0) {
|
|
522
|
+
console.log(`[NIGHTLY-REFLECT] Found ${conflictGroups.length} conflict group(s) to resolve.`);
|
|
523
|
+
|
|
524
|
+
// Collect all conflicting facts for these groups (batch to reduce queries)
|
|
525
|
+
const allConflicts = [];
|
|
526
|
+
for (const g of conflictGroups) {
|
|
527
|
+
const rows = db.prepare(`
|
|
528
|
+
SELECT id, entity, relation, value, confidence, created_at
|
|
529
|
+
FROM facts
|
|
530
|
+
WHERE entity = ? AND relation = ? AND conflict_status = 'CONFLICT' AND superseded_by IS NULL
|
|
531
|
+
ORDER BY created_at DESC
|
|
532
|
+
`).all(g.entity, g.relation);
|
|
533
|
+
if (rows.length >= 2) allConflicts.push({ entity: g.entity, relation: g.relation, facts: rows });
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (allConflicts.length > 0) {
|
|
537
|
+
// Limit to 5 groups to avoid truncating serialized JSON
|
|
538
|
+
const conflictInput = allConflicts.slice(0, 5).map(g => ({
|
|
539
|
+
entity: g.entity,
|
|
540
|
+
relation: g.relation,
|
|
541
|
+
candidates: g.facts.slice(0, 5).map(f => ({ id: f.id, value: f.value.slice(0, 150), confidence: f.confidence, created_at: f.created_at })),
|
|
542
|
+
}));
|
|
543
|
+
|
|
544
|
+
const resolvePrompt = `你是知识库冲突调解员。以下是同一 entity+relation 下的冲突事实,请选出每组最准确的一条保留。
|
|
545
|
+
|
|
546
|
+
冲突组(JSON):
|
|
547
|
+
${JSON.stringify(conflictInput, null, 2)}
|
|
548
|
+
|
|
549
|
+
输出 JSON 数组,每个元素对应一组冲突的裁决:
|
|
550
|
+
[
|
|
551
|
+
{
|
|
552
|
+
"entity": "...",
|
|
553
|
+
"relation": "...",
|
|
554
|
+
"winner_id": "保留的fact id",
|
|
555
|
+
"reason": "一句话理由"
|
|
556
|
+
}
|
|
557
|
+
]
|
|
558
|
+
|
|
559
|
+
规则:
|
|
560
|
+
- 优先选最新(created_at)且 confidence=high 的
|
|
561
|
+
- 如果旧条目更准确具体,选旧的
|
|
562
|
+
- 只输出 JSON 数组`;
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const resolveRaw = await Promise.race([
|
|
566
|
+
callHaiku(resolvePrompt, distillEnv, 60000),
|
|
567
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), 65000)),
|
|
568
|
+
]);
|
|
569
|
+
const verdicts = parseJsonFromLlm(resolveRaw);
|
|
570
|
+
if (Array.isArray(verdicts)) {
|
|
571
|
+
for (const v of verdicts) {
|
|
572
|
+
if (!v || !v.winner_id || !v.entity || !v.relation) continue;
|
|
573
|
+
// Validate winner exists in our conflict set
|
|
574
|
+
const group = allConflicts.find(g => g.entity === v.entity && g.relation === v.relation);
|
|
575
|
+
if (!group) continue;
|
|
576
|
+
const winnerExists = group.facts.some(f => f.id === v.winner_id);
|
|
577
|
+
if (!winnerExists) continue;
|
|
578
|
+
|
|
579
|
+
const loserIds = group.facts.filter(f => f.id !== v.winner_id).map(f => f.id);
|
|
580
|
+
if (loserIds.length === 0) continue;
|
|
581
|
+
|
|
582
|
+
// Mark losers as superseded
|
|
583
|
+
const placeholders = loserIds.map(() => '?').join(',');
|
|
584
|
+
db.prepare(
|
|
585
|
+
`UPDATE facts SET superseded_by = ?, conflict_status = 'OK', updated_at = datetime('now')
|
|
586
|
+
WHERE id IN (${placeholders})`
|
|
587
|
+
).run(v.winner_id, ...loserIds);
|
|
588
|
+
|
|
589
|
+
// Restore winner
|
|
590
|
+
db.prepare(
|
|
591
|
+
`UPDATE facts SET conflict_status = 'OK', updated_at = datetime('now') WHERE id = ?`
|
|
592
|
+
).run(v.winner_id);
|
|
593
|
+
|
|
594
|
+
conflictsResolved += loserIds.length;
|
|
595
|
+
}
|
|
596
|
+
if (conflictsResolved > 0) {
|
|
597
|
+
console.log(`[NIGHTLY-REFLECT] Resolved ${conflictsResolved} conflicting fact(s).`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (e) {
|
|
601
|
+
console.log(`[NIGHTLY-REFLECT] Conflict resolution failed (non-fatal): ${e.message}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} catch (e) {
|
|
606
|
+
console.log(`[NIGHTLY-REFLECT] Conflict query failed (non-fatal): ${e.message}`);
|
|
607
|
+
}
|
|
608
|
+
|
|
506
609
|
// Write audit log
|
|
507
610
|
writeReflectLog({
|
|
508
611
|
status: 'success',
|
|
@@ -510,6 +613,7 @@ entity_prefix: ${group.prefix}
|
|
|
510
613
|
decisions_written: decisions.length,
|
|
511
614
|
lessons_written: lessons.length,
|
|
512
615
|
synthesized_insights_saved: synthesizedSaved,
|
|
616
|
+
conflicts_resolved: conflictsResolved,
|
|
513
617
|
capsules_written: capsulesWritten,
|
|
514
618
|
capsule_facts_saved: capsuleFactsSaved,
|
|
515
619
|
decision_file: decisions.length > 0 ? decisionFile : null,
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MetaMe Ops mission queue.
|
|
5
|
+
* Same CLI interface as AgentScientist's topic-pool.js:
|
|
6
|
+
* next | activate <id> | complete <id> | list | scan
|
|
7
|
+
*
|
|
8
|
+
* The 'scan' command is unique to ops: it reads daemon logs + event logs
|
|
9
|
+
* and generates fix missions automatically.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
|
|
16
|
+
const MISSIONS_FILE = 'workspace/missions.md';
|
|
17
|
+
const SECTIONS = ['pending', 'active', 'completed', 'abandoned'];
|
|
18
|
+
const METAME_DIR = path.join(os.homedir(), '.metame');
|
|
19
|
+
|
|
20
|
+
// ── Mission file parser (same format as topic-pool) ──────────
|
|
21
|
+
|
|
22
|
+
function getMissionsPath(cwd) {
|
|
23
|
+
return path.join(cwd, MISSIONS_FILE);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseMission(line) {
|
|
27
|
+
const m = line.match(/^-\s*\[([^\]]+)\]\s*(.+)$/);
|
|
28
|
+
if (!m) return null;
|
|
29
|
+
const id = m[1].trim();
|
|
30
|
+
let rest = m[2].trim();
|
|
31
|
+
let priority = 999;
|
|
32
|
+
const pm = rest.match(/\(priority:\s*(\d+)\)\s*$/);
|
|
33
|
+
if (pm) {
|
|
34
|
+
priority = parseInt(pm[1], 10);
|
|
35
|
+
rest = rest.slice(0, pm.index).trim();
|
|
36
|
+
}
|
|
37
|
+
return { id, title: rest, priority };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseMissionsFile(content) {
|
|
41
|
+
const sections = { pending: [], active: [], completed: [], abandoned: [] };
|
|
42
|
+
let current = null;
|
|
43
|
+
for (const line of content.split('\n')) {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
const secMatch = trimmed.match(/^##\s+(pending|active|completed|abandoned)\s*$/);
|
|
46
|
+
if (secMatch) { current = secMatch[1]; continue; }
|
|
47
|
+
if (current && trimmed.startsWith('- [')) {
|
|
48
|
+
const mission = parseMission(trimmed);
|
|
49
|
+
if (mission) { mission.status = current; sections[current].push(mission); }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return sections;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatMissions(sections) {
|
|
56
|
+
const lines = ['# MetaMe Ops Missions', ''];
|
|
57
|
+
for (const sec of SECTIONS) {
|
|
58
|
+
lines.push(`## ${sec}`);
|
|
59
|
+
for (const t of (sections[sec] || [])) {
|
|
60
|
+
lines.push(t.priority !== 999
|
|
61
|
+
? `- [${t.id}] ${t.title} (priority: ${t.priority})`
|
|
62
|
+
: `- [${t.id}] ${t.title}`);
|
|
63
|
+
}
|
|
64
|
+
lines.push('');
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readSections(cwd) {
|
|
70
|
+
const fp = getMissionsPath(cwd);
|
|
71
|
+
if (!fs.existsSync(fp)) return { pending: [], active: [], completed: [], abandoned: [] };
|
|
72
|
+
return parseMissionsFile(fs.readFileSync(fp, 'utf-8'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function writeSections(cwd, sections) {
|
|
76
|
+
const fp = getMissionsPath(cwd);
|
|
77
|
+
fs.mkdirSync(path.dirname(fp), { recursive: true });
|
|
78
|
+
fs.writeFileSync(fp, formatMissions(sections), 'utf-8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findMission(sections, id) {
|
|
82
|
+
for (const sec of SECTIONS) {
|
|
83
|
+
const idx = (sections[sec] || []).findIndex(t => t.id === id);
|
|
84
|
+
if (idx !== -1) return { section: sec, index: idx, topic: sections[sec][idx] };
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Standard queue commands ──────────────────────────────────
|
|
90
|
+
|
|
91
|
+
function nextMission(cwd) {
|
|
92
|
+
const sections = readSections(cwd);
|
|
93
|
+
if (sections.active.length > 0) {
|
|
94
|
+
return { success: false, message: `already active: ${sections.active[0].id}` };
|
|
95
|
+
}
|
|
96
|
+
if (sections.pending.length === 0) return { success: false, message: 'no pending missions' };
|
|
97
|
+
const sorted = [...sections.pending].sort((a, b) => a.priority - b.priority);
|
|
98
|
+
return { success: true, topic: { id: sorted[0].id, title: sorted[0].title, status: 'pending', priority: sorted[0].priority } };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function activateMission(cwd, id) {
|
|
102
|
+
const sections = readSections(cwd);
|
|
103
|
+
const found = findMission(sections, id);
|
|
104
|
+
if (!found) return { success: false, message: `not found: ${id}` };
|
|
105
|
+
if (found.section !== 'pending') return { success: false, message: `${id} is ${found.section}` };
|
|
106
|
+
const mission = sections.pending.splice(found.index, 1)[0];
|
|
107
|
+
mission.status = 'active';
|
|
108
|
+
sections.active.push(mission);
|
|
109
|
+
writeSections(cwd, sections);
|
|
110
|
+
return { success: true, topic: { id: mission.id, title: mission.title, status: 'active' } };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function completeMission(cwd, id) {
|
|
114
|
+
const sections = readSections(cwd);
|
|
115
|
+
const found = findMission(sections, id);
|
|
116
|
+
if (!found) return { success: false, message: `not found: ${id}` };
|
|
117
|
+
if (found.section !== 'active') return { success: false, message: `${id} is ${found.section}` };
|
|
118
|
+
const mission = sections.active.splice(found.index, 1)[0];
|
|
119
|
+
mission.status = 'completed';
|
|
120
|
+
sections.completed.push(mission);
|
|
121
|
+
writeSections(cwd, sections);
|
|
122
|
+
return { success: true, topic: { id: mission.id, title: mission.title, status: 'completed' } };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function listMissions(cwd) {
|
|
126
|
+
const sections = readSections(cwd);
|
|
127
|
+
const topics = [];
|
|
128
|
+
for (const sec of SECTIONS) {
|
|
129
|
+
for (const t of (sections[sec] || [])) topics.push({ id: t.id, title: t.title, status: sec, priority: t.priority });
|
|
130
|
+
}
|
|
131
|
+
return { success: true, topics };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Log scanner: generates missions from error patterns ──────
|
|
135
|
+
|
|
136
|
+
function scanLogs(cwd) {
|
|
137
|
+
const sections = readSections(cwd);
|
|
138
|
+
const existingTitles = new Set(
|
|
139
|
+
[...sections.pending, ...sections.active, ...sections.completed, ...sections.abandoned].map(t => t.title)
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const newMissions = [];
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
const ONE_DAY = 24 * 60 * 60 * 1000;
|
|
145
|
+
|
|
146
|
+
// 1. Scan daemon log for repeated errors
|
|
147
|
+
const logPath = path.join(METAME_DIR, 'daemon.log');
|
|
148
|
+
if (fs.existsSync(logPath)) {
|
|
149
|
+
try {
|
|
150
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
151
|
+
const lines = content.split('\n');
|
|
152
|
+
// Only look at recent lines (last 500)
|
|
153
|
+
const recent = lines.slice(-500);
|
|
154
|
+
const errorCounts = {};
|
|
155
|
+
for (const line of recent) {
|
|
156
|
+
if (!/\bERR\b|\bWARN\b|\bError\b|\bfailed\b/i.test(line)) continue;
|
|
157
|
+
// Normalize: strip timestamps and variable data
|
|
158
|
+
const normalized = line
|
|
159
|
+
.replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z?/g, '<TS>')
|
|
160
|
+
.replace(/\d{10,}/g, '<ID>')
|
|
161
|
+
.replace(/\/tmp\/[^\s]+/g, '<TMP>')
|
|
162
|
+
.trim();
|
|
163
|
+
if (normalized.length < 20) continue;
|
|
164
|
+
const key = normalized.slice(0, 120);
|
|
165
|
+
errorCounts[key] = (errorCounts[key] || 0) + 1;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Generate missions for errors occurring 3+ times
|
|
169
|
+
// Dedup key is the pattern alone (not the count, which changes every scan)
|
|
170
|
+
for (const [pattern, count] of Object.entries(errorCounts)) {
|
|
171
|
+
if (count < 3) continue;
|
|
172
|
+
const dedupKey = `Fix recurring error: ${pattern.slice(0, 80)}`;
|
|
173
|
+
if (existingTitles.has(dedupKey)) continue;
|
|
174
|
+
// Use stable dedup key as title (count excluded to prevent duplicates)
|
|
175
|
+
newMissions.push({ title: dedupKey, priority: Math.max(1, 10 - count) });
|
|
176
|
+
}
|
|
177
|
+
} catch { /* skip unreadable log */ }
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2. Scan event logs for stuck/stale projects
|
|
181
|
+
const eventsDir = path.join(METAME_DIR, 'events');
|
|
182
|
+
if (fs.existsSync(eventsDir)) {
|
|
183
|
+
try {
|
|
184
|
+
const evFiles = fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl'));
|
|
185
|
+
for (const f of evFiles) {
|
|
186
|
+
const fp = path.join(eventsDir, f);
|
|
187
|
+
const projectKey = path.basename(f, '.jsonl');
|
|
188
|
+
try {
|
|
189
|
+
const stat = fs.statSync(fp);
|
|
190
|
+
const staleHours = (now - stat.mtimeMs) / (60 * 60 * 1000);
|
|
191
|
+
// If event log wasn't updated in 48h but project is supposed to be active
|
|
192
|
+
if (staleHours > 48) {
|
|
193
|
+
// Stable dedup key: project name only (not hours, which changes every scan)
|
|
194
|
+
const title = `Investigate stale project: ${projectKey}`;
|
|
195
|
+
if (!existingTitles.has(title)) {
|
|
196
|
+
newMissions.push({ title, priority: 5 });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch { /* skip */ }
|
|
200
|
+
}
|
|
201
|
+
} catch { /* skip */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 3. Check test health
|
|
205
|
+
try {
|
|
206
|
+
const testFiles = fs.readdirSync(path.join(cwd, 'scripts'))
|
|
207
|
+
.filter(f => f.endsWith('.test.js'));
|
|
208
|
+
for (const tf of testFiles) {
|
|
209
|
+
try {
|
|
210
|
+
execSyncSafe(`node --test scripts/${tf}`, cwd, 30000);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
const title = `Fix failing tests in ${tf}`;
|
|
213
|
+
if (!existingTitles.has(title)) {
|
|
214
|
+
newMissions.push({ title, priority: 1 });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch { /* skip */ }
|
|
219
|
+
|
|
220
|
+
// Add new missions to pending
|
|
221
|
+
if (newMissions.length > 0) {
|
|
222
|
+
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
|
223
|
+
let counter = sections.pending.length + sections.active.length + sections.completed.length + sections.abandoned.length + 1;
|
|
224
|
+
for (const m of newMissions) {
|
|
225
|
+
const id = `ops-${date}-${String(counter++).padStart(3, '0')}`;
|
|
226
|
+
sections.pending.push({ id, title: m.title, priority: m.priority, status: 'pending' });
|
|
227
|
+
}
|
|
228
|
+
writeSections(cwd, sections);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { success: true, scanned: true, new_missions: newMissions.length, total_pending: sections.pending.length };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function execSyncSafe(cmd, cwd, timeout) {
|
|
235
|
+
const { execSync } = require('child_process');
|
|
236
|
+
return execSync(cmd, { cwd, timeout, encoding: 'utf8', stdio: 'pipe' });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── CLI ──────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
if (require.main === module) {
|
|
242
|
+
const cwd = process.env.MISSION_CWD || process.cwd();
|
|
243
|
+
const [,, command, ...args] = process.argv;
|
|
244
|
+
|
|
245
|
+
let result;
|
|
246
|
+
switch (command) {
|
|
247
|
+
case 'next': result = nextMission(cwd); break;
|
|
248
|
+
case 'activate': result = args[0] ? activateMission(cwd, args[0]) : { success: false, message: 'usage: activate <id>' }; break;
|
|
249
|
+
case 'complete': result = args[0] ? completeMission(cwd, args[0]) : { success: false, message: 'usage: complete <id>' }; break;
|
|
250
|
+
case 'list': result = listMissions(cwd); break;
|
|
251
|
+
case 'scan': result = scanLogs(cwd); break;
|
|
252
|
+
default: result = { success: false, message: `unknown: ${command}. Available: next, activate, complete, list, scan` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { nextMission, activateMission, completeMission, listMissions, scanLogs };
|