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.
Files changed (67) hide show
  1. package/README.md +49 -6
  2. package/index.js +266 -72
  3. package/package.json +7 -3
  4. package/scripts/daemon-admin-commands.js +34 -0
  5. package/scripts/daemon-agent-commands.js +6 -2
  6. package/scripts/daemon-bridges.js +41 -10
  7. package/scripts/daemon-claude-engine.js +128 -29
  8. package/scripts/daemon-command-router.js +16 -0
  9. package/scripts/daemon-command-session-route.js +3 -1
  10. package/scripts/daemon-default.yaml +3 -1
  11. package/scripts/daemon-engine-runtime.js +1 -5
  12. package/scripts/daemon-message-pipeline.js +113 -44
  13. package/scripts/daemon-ops-commands.js +25 -11
  14. package/scripts/daemon-reactive-lifecycle.js +757 -76
  15. package/scripts/daemon-session-commands.js +3 -2
  16. package/scripts/daemon-session-store.js +82 -27
  17. package/scripts/daemon-team-dispatch.js +21 -5
  18. package/scripts/daemon-utils.js +3 -1
  19. package/scripts/daemon.js +80 -2
  20. package/scripts/distill.js +1 -1
  21. package/scripts/docs/file-transfer.md +1 -0
  22. package/scripts/docs/maintenance-manual.md +55 -2
  23. package/scripts/docs/pointer-map.md +34 -0
  24. package/scripts/feishu-adapter.js +25 -0
  25. package/scripts/hooks/intent-file-transfer.js +2 -1
  26. package/scripts/hooks/intent-perpetual.js +109 -0
  27. package/scripts/hooks/intent-research.js +112 -0
  28. package/scripts/intent-registry.js +4 -0
  29. package/scripts/memory-extract.js +29 -1
  30. package/scripts/memory-nightly-reflect.js +104 -0
  31. package/scripts/ops-mission-queue.js +258 -0
  32. package/scripts/ops-verifier.js +197 -0
  33. package/scripts/signal-capture.js +3 -3
  34. package/scripts/skill-evolution.js +11 -2
  35. package/skills/agent-browser/SKILL.md +153 -0
  36. package/skills/agent-reach/SKILL.md +66 -0
  37. package/skills/agent-reach/evolution.json +13 -0
  38. package/skills/deep-research/SKILL.md +77 -0
  39. package/skills/find-skills/SKILL.md +133 -0
  40. package/skills/heartbeat-task-manager/SKILL.md +63 -0
  41. package/skills/macos-local-orchestrator/SKILL.md +192 -0
  42. package/skills/macos-local-orchestrator/agents/openai.yaml +4 -0
  43. package/skills/macos-local-orchestrator/references/tooling-landscape.md +70 -0
  44. package/skills/macos-mail-calendar/SKILL.md +394 -0
  45. package/skills/mcp-installer/SKILL.md +138 -0
  46. package/skills/skill-creator/LICENSE.txt +202 -0
  47. package/skills/skill-creator/README.md +72 -0
  48. package/skills/skill-creator/SKILL.md +96 -0
  49. package/skills/skill-creator/evolution.json +6 -0
  50. package/skills/skill-creator/references/creation-guide.md +116 -0
  51. package/skills/skill-creator/references/evolution-guide.md +74 -0
  52. package/skills/skill-creator/references/output-patterns.md +82 -0
  53. package/skills/skill-creator/references/workflows.md +28 -0
  54. package/skills/skill-creator/scripts/align_all.py +32 -0
  55. package/skills/skill-creator/scripts/auto_evolve_hook.js +247 -0
  56. package/skills/skill-creator/scripts/init_skill.py +303 -0
  57. package/skills/skill-creator/scripts/merge_evolution.py +70 -0
  58. package/skills/skill-creator/scripts/package_skill.py +110 -0
  59. package/skills/skill-creator/scripts/quick_validate.py +103 -0
  60. package/skills/skill-creator/scripts/setup.py +141 -0
  61. package/skills/skill-creator/scripts/smart_stitch.py +82 -0
  62. package/skills/skill-manager/SKILL.md +112 -0
  63. package/skills/skill-manager/scripts/delete_skill.py +31 -0
  64. package/skills/skill-manager/scripts/list_skills.py +61 -0
  65. package/skills/skill-manager/scripts/scan_and_check.py +125 -0
  66. package/skills/skill-manager/scripts/sync_index.py +144 -0
  67. 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 };