metame-cli 1.5.9 → 1.5.11
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 +218 -64
- package/package.json +6 -3
- package/scripts/daemon-admin-commands.js +34 -0
- package/scripts/daemon-bridges.js +18 -1
- package/scripts/daemon-claude-engine.js +41 -1
- package/scripts/daemon-default.yaml +3 -1
- package/scripts/daemon-ops-commands.js +25 -11
- package/scripts/daemon-reactive-lifecycle.js +355 -70
- package/scripts/daemon-utils.js +55 -0
- package/scripts/daemon.js +79 -2
- package/scripts/distill.js +1 -1
- package/scripts/docs/maintenance-manual.md +55 -2
- package/scripts/docs/orphan-files-review.md +72 -0
- package/scripts/docs/pointer-map.md +34 -0
- package/scripts/feishu-adapter.js +25 -0
- package/scripts/hooks/intent-auto-rules.js +50 -0
- package/scripts/memory-extract.js +29 -1
- package/scripts/memory-nightly-reflect.js +104 -0
- package/scripts/signal-capture.js +3 -3
- package/scripts/skill-evolution.js +11 -2
- package/scripts/daemon.yaml +0 -444
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* daemon-utils.js
|
|
5
|
+
*
|
|
6
|
+
* Shared normalization helpers used across daemon modules.
|
|
7
|
+
* Single source of truth — no other module should redefine these.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function normalizeEngineName(name, defaultEngine = 'claude') {
|
|
11
|
+
const n = String(name || '').trim().toLowerCase();
|
|
12
|
+
return n === 'codex' ? 'codex' : (typeof defaultEngine === 'function' ? defaultEngine() : defaultEngine);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeCodexSandboxMode(value, fallback = null) {
|
|
16
|
+
const text = String(value || '').trim().toLowerCase();
|
|
17
|
+
if (!text) return fallback;
|
|
18
|
+
if (text === 'read-only' || text === 'readonly') return 'read-only';
|
|
19
|
+
if (text === 'workspace-write' || text === 'workspace') return 'workspace-write';
|
|
20
|
+
if (
|
|
21
|
+
text === 'danger-full-access'
|
|
22
|
+
|| text === 'dangerous'
|
|
23
|
+
|| text === 'full-access'
|
|
24
|
+
|| text === 'full'
|
|
25
|
+
|| text === 'bypass'
|
|
26
|
+
|| text === 'writable'
|
|
27
|
+
) return 'danger-full-access';
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeCodexApprovalPolicy(value, fallback = null) {
|
|
32
|
+
const text = String(value || '').trim().toLowerCase();
|
|
33
|
+
if (!text) return fallback;
|
|
34
|
+
if (text === 'never' || text === 'no' || text === 'none') return 'never';
|
|
35
|
+
if (text === 'on-failure' || text === 'on_failure' || text === 'failure') return 'on-failure';
|
|
36
|
+
if (text === 'on-request' || text === 'on_request' || text === 'request') return 'on-request';
|
|
37
|
+
if (text === 'untrusted') return 'untrusted';
|
|
38
|
+
return fallback;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mergeAgentMaps(cfg) {
|
|
42
|
+
return {
|
|
43
|
+
...(cfg.telegram ? cfg.telegram.chat_agent_map : {}),
|
|
44
|
+
...(cfg.feishu ? cfg.feishu.chat_agent_map : {}),
|
|
45
|
+
...(cfg.imessage ? cfg.imessage.chat_agent_map : {}),
|
|
46
|
+
...(cfg.siri_bridge ? cfg.siri_bridge.chat_agent_map : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
normalizeEngineName,
|
|
52
|
+
normalizeCodexSandboxMode,
|
|
53
|
+
normalizeCodexApprovalPolicy,
|
|
54
|
+
mergeAgentMaps,
|
|
55
|
+
};
|
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 ─────────────────────────────────────────────────────────
|
|
@@ -2315,6 +2391,7 @@ const { handleOpsCommand } = createOpsCommandHandler({
|
|
|
2315
2391
|
path,
|
|
2316
2392
|
spawn,
|
|
2317
2393
|
execSync,
|
|
2394
|
+
execFileSync,
|
|
2318
2395
|
log,
|
|
2319
2396
|
loadConfig,
|
|
2320
2397
|
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`
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 未被 daemon.js 直接引用的脚本文件审查
|
|
2
|
+
|
|
3
|
+
> 审查日期: 2026-03-17
|
|
4
|
+
> 目的: 确认哪些文件是活跃的、哪些可以安全删除
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 分类结果
|
|
9
|
+
|
|
10
|
+
### ACTIVE — 被其他活跃模块间接引用(无需处理)
|
|
11
|
+
|
|
12
|
+
| 文件 | 引用方 | 说明 |
|
|
13
|
+
|------|--------|------|
|
|
14
|
+
| `session-analytics.js` | daemon-claude-engine, distill, memory-extract, session-summarize | 会话分析核心库 |
|
|
15
|
+
| `mentor-engine.js` | daemon-claude-engine, daemon-admin-commands | AI 导师引擎 |
|
|
16
|
+
| `intent-registry.js` | daemon-claude-engine, hooks/intent-engine | 意图识别注册表 |
|
|
17
|
+
| `daemon-command-session-route.js` | daemon-exec-commands, daemon-ops-commands | 会话路由解析 |
|
|
18
|
+
| `daemon-siri-bridge.js` | daemon-bridges.js | Siri HTTP 桥接 |
|
|
19
|
+
| `daemon-siri-imessage.js` | daemon-siri-bridge.js | iMessage 数据库读取 |
|
|
20
|
+
| `telegram-adapter.js` | daemon-bridges.js | Telegram 适配器 |
|
|
21
|
+
| `feishu-adapter.js` | daemon-bridges.js | 飞书适配器 |
|
|
22
|
+
| `session-summarize.js` | daemon.js (spawn) | 会话总结,由 daemon.js 第1158行 spawn 调用 |
|
|
23
|
+
|
|
24
|
+
### HEARTBEAT — daemon.yaml 心跳任务调用(无需处理)
|
|
25
|
+
|
|
26
|
+
| 文件 | daemon.yaml 任务名 | 说明 |
|
|
27
|
+
|------|-------------------|------|
|
|
28
|
+
| `distill.js` (1447行) | `cognitive-distill` | 认知蒸馏引擎 |
|
|
29
|
+
| `memory-extract.js` (428行) | `memory-extract` | 记忆提取 |
|
|
30
|
+
| `memory-nightly-reflect.js` (607行) | (nightly task) | 夜间反思 |
|
|
31
|
+
| `self-reflect.js` (378行) | `self-reflect` | 自我反思 |
|
|
32
|
+
|
|
33
|
+
### DEPENDENCY — 被心跳任务间接依赖(无需处理)
|
|
34
|
+
|
|
35
|
+
| 文件 | 被谁引用 | 说明 |
|
|
36
|
+
|------|----------|------|
|
|
37
|
+
| `signal-capture.js` | distill.js + Claude Code UserPromptSubmit hook | 信号捕获(hook + 数据源) |
|
|
38
|
+
| `pending-traits.js` (147行) | distill.js | 待处理特征 |
|
|
39
|
+
| `skill-changelog.js` | skill-evolution.js | 技能变更日志 |
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
### ORPHAN — 疑似孤儿文件,需要王总决策
|
|
44
|
+
|
|
45
|
+
| 文件 | 行数 | 分析 | 建议 |
|
|
46
|
+
|------|------|------|------|
|
|
47
|
+
| `daemon-reactive-lifecycle.js` | ~500 | 未被任何生产模块 require,仅被 test 和 verify 脚本引用。**但仍会被部署到 ~/.metame/**(不在 EXCLUDED_SCRIPTS 中)。是计划中的"反应式生命周期"功能,尚未集成 | **删除或归档**?如果不再计划实现。至少应加入 EXCLUDED_SCRIPTS 避免无用部署 |
|
|
48
|
+
| `verify-reactive-claude-md.js` | ~100 | 仅引用 daemon-reactive-lifecycle,单独的验证脚本 | 随 reactive-lifecycle 一起处理 |
|
|
49
|
+
| `sync-readme.js` | ~50 | 未被任何模块引用,是独立 README 翻译工具。已在 EXCLUDED_SCRIPTS 中,不会被部署到 ~/.metame/ | **保留为 CLI 工具**(无害)还是 **删除**? |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
### plugin/scripts/ 孤儿文件(无对应源文件)
|
|
54
|
+
|
|
55
|
+
这些文件只存在于 `plugin/scripts/`,在 `scripts/` 中没有源文件:
|
|
56
|
+
|
|
57
|
+
| 文件 | 说明 | 建议 |
|
|
58
|
+
|------|------|------|
|
|
59
|
+
| `auto-start-daemon.js` | SessionStart hook 自启动 daemon | 迁移到 `scripts/` 还是删除? |
|
|
60
|
+
| `distill-on-start.js` | 启动时 spawn 蒸馏 | 同上 |
|
|
61
|
+
| `inject-profile.js` | 注入 SYSTEM KERNEL 协议头 | 同上 |
|
|
62
|
+
| `setup.js` | 创建 ~/.claude_profile.yaml | 同上 |
|
|
63
|
+
|
|
64
|
+
> 风险:下次 `npm run sync:plugin` 会用 `scripts/` 覆盖 `plugin/scripts/`,这 4 个文件可能丢失。
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 需要王总确认的决策
|
|
69
|
+
|
|
70
|
+
1. **daemon-reactive-lifecycle.js** — 这个反应式生命周期功能还计划集成吗?如果不做了就可以删掉。
|
|
71
|
+
2. **sync-readme.js** — 是否还在用?
|
|
72
|
+
3. **plugin/scripts/ 的 4 个孤儿** — 应该迁移到 `scripts/` 作为源文件,还是已经不需要了?
|
|
@@ -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() {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Auto-Rules Intent Module
|
|
5
|
+
*
|
|
6
|
+
* Injects auto-promoted defensive rules distilled by memory-nightly-reflect.
|
|
7
|
+
* Rules live in ~/.metame/auto-rules.md — one-liners promoted from recurring
|
|
8
|
+
* hot-fact patterns (3+ occurrences in memory.db).
|
|
9
|
+
*
|
|
10
|
+
* Always fires when rules exist — no pattern detection needed.
|
|
11
|
+
* Overhead: ~10–15 one-liners ≈ 200 tokens per turn.
|
|
12
|
+
*
|
|
13
|
+
* File is cached for CACHE_TTL_MS to avoid per-prompt disk I/O.
|
|
14
|
+
* Cache refreshes automatically after nightly-reflect writes new rules.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const os = require('os');
|
|
20
|
+
|
|
21
|
+
const RULES_FILE = path.join(os.homedir(), '.metame', 'auto-rules.md');
|
|
22
|
+
const CACHE_TTL_MS = 60 * 1000; // 1 minute — nightly-reflect runs once/day
|
|
23
|
+
|
|
24
|
+
let _cached = null;
|
|
25
|
+
let _cacheTs = 0;
|
|
26
|
+
|
|
27
|
+
function loadRules() {
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
if (_cached !== null && now - _cacheTs < CACHE_TTL_MS) return _cached;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(RULES_FILE, 'utf8');
|
|
33
|
+
const rules = content
|
|
34
|
+
.split('\n')
|
|
35
|
+
.map(l => l.trim())
|
|
36
|
+
.filter(l => l && !l.startsWith('<!--'));
|
|
37
|
+
_cached = rules.length > 0 ? rules : [];
|
|
38
|
+
} catch {
|
|
39
|
+
_cached = [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_cacheTs = now;
|
|
43
|
+
return _cached;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = function detectAutoRules() {
|
|
47
|
+
const rules = loadRules();
|
|
48
|
+
if (rules.length === 0) return null;
|
|
49
|
+
return ['[自动晋升规则 — 夜间蒸馏高频模式]', ...rules.map(r => `- ${r}`)].join('\n');
|
|
50
|
+
};
|
|
@@ -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,
|