metame-cli 1.5.11 → 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/index.js +64 -7
- package/package.json +3 -2
- package/scripts/daemon-agent-commands.js +6 -2
- package/scripts/daemon-bridges.js +23 -9
- package/scripts/daemon-claude-engine.js +87 -28
- package/scripts/daemon-command-router.js +16 -0
- package/scripts/daemon-command-session-route.js +3 -1
- package/scripts/daemon-engine-runtime.js +1 -5
- package/scripts/daemon-message-pipeline.js +113 -44
- package/scripts/daemon-reactive-lifecycle.js +405 -9
- 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 +1 -0
- package/scripts/docs/file-transfer.md +1 -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/ops-mission-queue.js +258 -0
- package/scripts/ops-verifier.js +197 -0
- 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
|
@@ -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
|
@@ -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
|
|
|
@@ -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 = {}) {
|
|
@@ -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 };
|