42 Commits

Author SHA1 Message Date
starmen1265 8fb52c48fb Auto backup before evolver run - 2026年 03月 03日 星期二 23:13:28 CST 2026-03-03 23:13:28 +08:00
starmen1265 5289fe68b3 Auto backup before evolver run - 2026年 03月 03日 星期二 23:13:05 CST 2026-03-03 23:13:05 +08:00
starmen1265 f11d322c3f Auto backup before evolver run - 2026年 03月 03日 星期二 23:12:56 CST 2026-03-03 23:12:56 +08:00
starmen1265 429e5a797d 自动同步: 2026-03-03 19:00 2026-03-03 19:00:01 +08:00
starmen1265 338d693557 自动同步: 2026-03-03 15:00 2026-03-03 15:00:01 +08:00
starmen1265 dd86bb62dc 自动同步: 2026-03-03 10:00 2026-03-03 10:00:01 +08:00
starmen1265 10b58d1a22 自动同步: 2026-03-03 04:00 2026-03-03 04:00:01 +08:00
starmen1265 d765fdc019 自动同步: 2026-03-03 03:00 2026-03-03 03:00:01 +08:00
starmen1265 db0fff3a2a 自动同步: 2026-03-03 02:00 2026-03-03 02:00:01 +08:00
starmen1265 09f8a501a3 自动同步: 2026-03-03 01:00 2026-03-03 01:00:01 +08:00
starmen1265 0a44560f58 自动同步: 2026-03-03 00:00 2026-03-03 00:00:01 +08:00
starmen1265 098d230aad 自动同步: 2026-03-02 19:00 2026-03-02 19:00:01 +08:00
starmen1265 fbb8c5bbc3 自动同步: 2026-03-02 17:00 2026-03-02 17:00:01 +08:00
starmen1265 b39b585272 自动同步: 2026-03-02 04:00 2026-03-02 04:00:01 +08:00
starmen1265 3497f6c066 自动同步: 2026-03-02 03:00 2026-03-02 03:00:01 +08:00
starmen1265 9340896e1f 自动同步: 2026-03-02 02:00 2026-03-02 02:00:01 +08:00
starmen1265 6982d31a14 chore: 更新 .gitignore 忽略 sessions 和设备文件 2026-03-02 01:22:40 +08:00
starmen1265 05ef6ecb14 docs: 更新 2026-03-02 工作日志 - Gitea CI 排查完成 2026-03-02 01:18:52 +08:00
starmen1265 1a71ce9386 自动同步: 2026-03-02 01:00 2026-03-02 01:00:01 +08:00
starmen1265 bd289a21b8 自动同步: 2026-03-02 00:00 2026-03-02 00:00:02 +08:00
starmen1265 49607221db 自动同步: 2026-03-01 23:00 2026-03-01 23:00:01 +08:00
starmen1265 2b6b390615 自动同步: 2026-03-01 19:00 2026-03-01 19:00:01 +08:00
starmen1265 41db7f28a1 自动同步: 2026-03-01 18:00 2026-03-01 18:00:01 +08:00
starmen1265 6c2bfdca21 自动同步: 2026-03-01 17:00 2026-03-01 17:00:01 +08:00
starmen1265 bd88c3910d 自动同步: 2026-03-01 09:00 2026-03-01 09:00:01 +08:00
starmen1265 f392eeff9d 自动同步: 2026-03-01 04:00 2026-03-01 04:00:01 +08:00
starmen1265 314955826d 自动同步: 2026-03-01 03:00 2026-03-01 03:00:01 +08:00
starmen1265 c2afd463d4 自动同步: 2026-03-01 02:00 2026-03-01 02:00:01 +08:00
starmen1265 4775c8474e 自动同步: 2026-03-01 01:00 2026-03-01 01:00:02 +08:00
starmen1265 e47cc1e8ad 自动同步: 2026-03-01 00:00 2026-03-01 00:00:01 +08:00
starmen1265 9d4cce0aab 自动同步: 2026-02-28 22:00 2026-02-28 22:00:01 +08:00
starmen1265 25fe227471 自动同步: 2026-02-28 20:00 2026-02-28 20:00:01 +08:00
starmen1265 dc3e9e5e78 更新会话记录 2026-02-28 19:06:37 +08:00
starmen1265 9d74b405e7 自动同步: 2026-02-28 19:00 2026-02-28 19:00:01 +08:00
starmen1265 1643884993 自动同步: 2026-02-28 18:00 2026-02-28 18:00:01 +08:00
starmen1265 9cbf85c785 自动同步: 2026-02-28 17:00 2026-02-28 17:00:01 +08:00
starmen1265 2e1322aa12 自动同步: 2026-02-28 16:00 2026-02-28 16:00:01 +08:00
starmen1265 418ec30ab5 添加thinker代理和feishu-art账号 2026-02-28 15:27:41 +08:00
starmen1265 c24f508b91 添加feishu-worker账号并绑定knowledge-base 2026-02-28 15:17:27 +08:00
starmen1265 22d728efad 更新会话记录 2026-02-28 15:00:27 +08:00
starmen1265 3071647122 自动同步: 2026-02-28 15:00 2026-02-28 15:00:01 +08:00
starmen1265 a2c82bf1cc 添加feishu-dev账号并绑定dev-assist 2026-02-28 14:59:41 +08:00
124 changed files with 20058 additions and 485 deletions
+6 -2
View File
@@ -1,8 +1,12 @@
extensions/
agents/knowledge-base/workspace/skills/
agents/knowledge-base/sessions/sessions.json
logs/git-sync.log
agents/*/sessions/
agents/*/sessions/*.json
!agents/knowledge-base/sessions/sessions.json
devices/paired.json
identity/device-auth.json
subagents/runs.json
logs/git-sync.log
update-check.json
*.jsonl
*.lock
+15 -13
View File
@@ -347,8 +347,8 @@
"memoryFlushCompactionCount": 0
},
"agent:dev-assist:main": {
"sessionId": "7b8f5fc5-2527-4066-9985-13c873cdfeea",
"updatedAt": 1772212755695,
"sessionId": "da8f811d-332e-4695-ab12-c12dc48abad3",
"updatedAt": 1772388420747,
"systemSent": true,
"abortedLastRun": false,
"chatType": "direct",
@@ -369,7 +369,7 @@
"to": "user:ou_302b162eb824d405718845441a32d9cf",
"accountId": "feishu-dev"
},
"sessionFile": "/home/starmen1265/.openclaw/agents/dev-assist/sessions/7b8f5fc5-2527-4066-9985-13c873cdfeea.jsonl",
"sessionFile": "/home/starmen1265/.openclaw/agents/dev-assist/sessions/da8f811d-332e-4695-ab12-c12dc48abad3.jsonl",
"compactionCount": 0,
"skillsSnapshot": {
"prompt": "\n\nThe following skills provide specialized instructions for specific tasks.\nUse the read tool to load a skill's file when the task matches its description.\nWhen a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\n\n<available_skills>\n <skill>\n <name>clawlist</name>\n <description>MUST use for any multi-step project, long-running task, or infinite monitoring workflow. Plan, execute, track, and verify tasks with checkpoint validation. For projects, automation, and ongoing operations.</description>\n <location>/home/starmen1265/.openclaw/skills/clawlist/SKILL.md</location>\n </skill>\n</available_skills>",
@@ -388,18 +388,14 @@
"disableModelInvocation": false
}
],
"version": 1772187037367
"version": 0
},
"inputTokens": 6520,
"outputTokens": 871,
"totalTokens": 13560,
"modelProvider": "zhipu",
"model": "glm-5",
"contextTokens": 200000,
"systemPromptReport": {
"source": "run",
"generatedAt": 1772212727160,
"sessionId": "7b8f5fc5-2527-4066-9985-13c873cdfeea",
"generatedAt": 1772388345577,
"sessionId": "da8f811d-332e-4695-ab12-c12dc48abad3",
"sessionKey": "agent:dev-assist:main",
"provider": "zhipu",
"model": "glm-5",
@@ -410,9 +406,9 @@
"sandboxed": false
},
"systemPrompt": {
"chars": 23791,
"chars": 23820,
"projectContextChars": 13850,
"nonProjectContextChars": 9941
"nonProjectContextChars": 9970
},
"injectedWorkspaceFiles": [
{
@@ -651,6 +647,12 @@
}
]
}
}
},
"inputTokens": 31031,
"outputTokens": 2064,
"totalTokens": 200000,
"contextTokens": 200000,
"memoryFlushAt": 1772377559686,
"memoryFlushCompactionCount": 0
}
}
@@ -0,0 +1,45 @@
# 2026-02-28 记忆
## TODO
### 网络/SSH 问题
- [ ] 翻墙状态下无法访问 micecola.top:22 SSH 端口,需要调整回来
- [ ] 网关设置为 192.168.1.200(旁路由)时出口 IP 变化导致 SSH 被拒
### tmux + Claude Code 配置
- [ ] 创建 tmux 配置文件 (~/.tmux.conf)
- [ ] 创建一键启动脚本 (~/dev.sh)
- [ ] 考虑安装 tmux-resurrect 和 tmux-continuum 插件
**参考资源:**
- https://www.blle.co/blog/claude-code-tmux-beautiful-terminal
- https://github.com/pchalasani/claude-code-tools
---
## 今日事件
### 网络修复
- 修复网络搜索问题:将网关和 DNS 设置为 192.168.1.200(旁路由,可翻墙)
- 需要用户手动执行 sudo 命令修改网络配置
### kb-web 开发
- **分支**: `feature/article-word-count`
- **改动**: 文章类型显示原文字数代替时间
- 解析 `type` 字段判断资源类型
- 文章类型:显示 `📝 X 字`
- 视频类型:保持显示时长
- **待部署**: `sudo ./deploy/deploy.sh test feature/article-word-count`
### Git 配置
- kb-web 远程仓库地址已改为使用 SSH config alias: `gitea-account:starmen1265/kb-web.git`
- 推送时需要先加载 SSH agent: `eval "$(ssh-agent -s)" && ssh-add ~/.ssh/id_ed25519_gitea_account`
### CI/CD 方案设计
- 已创建完整方案文档: `kb-web/docs/CI-CD-PLAN.md`
- 分支策略: main(生产) → develop(测试) → feature/*(开发)
- 使用 Gitea Actions 实现 CI/CD
- Gitea 版本: 1.23.4 ✅ 支持 Actions
- Runner 状态: micecola.top 上有容器但未正确配置(缺少 instance address)
- 内网 Gitea: 192.168.1.22:8418 (Web), :2223 (SSH)
- **问题**: micecola.top 无法访问内网 Gitea,Runner 需要部署到内网
@@ -0,0 +1,41 @@
# 2026-03-01 记忆
## 对话记录
### OpenClaw + tmux + Claude 代码开发工作流查询
张青询问如何使用 OpenClaw + temux + Claude 进行代码开发。
**澄清过程:**
- 最初误以为是 "Termux"(Android 终端模拟器)
- 张青纠正为 "temux"
- 经搜索确认实际为 "tmux"(终端复用器)
**核心发现:**
1. **架构模式**:
- 用户通过飞书/Telegram/WhatsApp 发消息给 OpenClaw
- OpenClaw 通过 SSH 连接到服务器
- 服务器上用 tmux 运行 Claude Code
- tmux 保持会话持久化,即使断开连接也不丢失上下文
2. **tmux + Claude Code 优势**:
- 会话持久化:SSH 断开后 Claude 上下文不丢失
- 后台运行:长时间任务不阻塞
- 多窗格:一边跑 Claude,一边看日志、跑测试
- 远程访问:手机、办公室、家里随时接入
3. **相关资源**:
- OpenClaw 有 `tmux-agents` 技能,可管理后台 coding agents
- 安装:`clawhub install tmux-agents`
- 功能:在 tmux 会话中启动 Claude Code/Codex/Gemini CLI,检查进度,获取结果
4. **推荐配置**:
- tmux 前缀键改为 Ctrl+a
- 启用 24-bit 真彩色
- 历史记录设为 50000(Claude 输出较长)
- 使用 Catppuccin Mocha 主题
## 待办事项
- 无
File diff suppressed because one or more lines are too long
+5 -6
View File
@@ -1,13 +1,12 @@
{
"WARNING": "This file is automatically generated by act-runner. Do not edit it manually unless you know what you are doing. Removing this file will cause act runner to re-register as a new runner.",
"id": 3,
"uuid": "4974ee15-8d34-4173-8d87-15fb1ad23066",
"name": "starmen1265-imini-S",
"token": "b0b5ddb7f467e3d463d6aa196088219cff7e279a",
"id": 10,
"uuid": "d0c1c656-076b-466a-a93c-f76ba3919876",
"name": "local-runner",
"token": "6a0604b731220e1871d605bdfe1d5a704ddff212",
"address": "http://192.168.1.22:8418",
"labels": [
"linux:host",
"amd64:host"
"linux:host"
],
"ephemeral": false
}
+7 -10
View File
@@ -3,7 +3,14 @@
## 待处理
### 重要
- [ ] 调整权限策略,让其他几个代理也能使用root权限
- [ ] 研究如何使用 tmux+ClaudeCode 进行开发
- [ ] 研究如何使用 git worktree 进行开发
- [ ] 研究如何使用 PR 流程规范开发
- [ ] 将通用技能从 kb 技能仓库独立出来
- [ ] 🔒 处理安全巡检报告问题(2026-02-10)
- [ ] 推送 gitea-runner-custom 镜像到 Docker Hub
- 需要提供 Docker Hub 用户名
- [ ] 修复凭证目录权限: chmod 700 /home/admin/.openclaw/credentials
- [ ] 禁用控制UI不安全认证: gateway.controlUi.allowInsecureAuth=false
- [ ] 审查/移除Feishu插件 (src/media.ts:477)
@@ -33,16 +40,6 @@
### 基础设施
- [x] 调通外网代理
- [ ] 调通远程桌面连接
### 字幕增强方案(待实现)
- [x] ~~编译 Whisper.cpp~~ - 已完成基础功能
- Whisper 编译完成,集成到 KB Workflow
- [ ] **集成 pyannote.audio 说话者分离**
- 创建 kb-whisper-diarization 技能
- 安装依赖: `pip install pyannote.audio torch`
- 设置 HF_TOKEN 环境变量
- 申请模型权限: https://huggingface.co/pyannote/speaker-diarization
---
@@ -0,0 +1,14 @@
# 2026-03-01 工作日志
## 网络配置修改
- WLAN 网关和 DNS 从 192.168.1.1 修改为 192.168.1.200
- 修改后网络连接正常激活
## Docker 构建尝试
- 尝试构建 gitea-runner-php 镜像
- Dockerfile 位置: /home/starmen1265/work/kb-web/docker/runner/Dockerfile
- 构建失败,错误: "Temporary failure resolving 'deb.debian.org'"
- 原因: 网络问题(可能与刚才修改的网关/DNS设置有关)
## 待处理
- 等待网络稳定后重新尝试构建 Docker 镜像
@@ -0,0 +1,54 @@
# 2026-03-02 工作日志
## Gitea Actions CI 问题排查 - 成功 ✅
### 问题描述
- CI 输出被截断,看不到详细错误信息
- 多次修改 ci.yml 但输出不变
- PHP 语法检查形同虚设(退出码问题)
- test.yml 和 ci.yml 触发条件混乱
### 根本原因
1. **workflow 缓存问题**:act runner 缓存了 workflow 文件
2. **分支触发问题**:ci.yml 只在 main/develop 触发,feature 分支触发的是 test.yml
3. **find -exec 退出码问题**:只反映 find 自身,不反映 php -l 的失败状态
### 解决方案
1. 清除 act 缓存:`rm -rf ~/.cache/act ~/.cache/actcache`
2. 重启 act_runner daemon
3. 修改 ci.yml 触发条件为所有分支
4. 更新自定义镜像添加 Node.js(gitea-runner-php:latest)
5. 修复 PHP 语法检查脚本:正确捕获退出码
6. 删除 test.yml,统一使用 ci.yml
### 最终 CI 配置
```yaml
on:
push:
branches: ["**"]
pull_request:
branches: [main, develop]
```
### PHP 语法检查(正确版本)
```bash
errors=0
for file in $(find . -name "*.php" -not -path "./vendor/*"); do
if ! php -l "$file" > /dev/null 2>&1; then
echo "❌ 语法错误: $file"
php -l "$file"
errors=$((errors + 1))
fi
done
if [ $errors -gt 0 ]; then
echo "发现 $errors 个语法错误"
exit 1
fi
echo "✅ 所有 PHP 文件语法正确"
```
### 修复的文件
- index.php(缺少分号)
- ci.yml(多处修改)
- docker/runner/Dockerfile(添加 Node.js)
- 删除 test.yml
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
{"type":"session","version":3,"id":"4ae29b33-fd1c-4aed-b638-913f59b321c6","timestamp":"2026-02-28T16:15:52.907Z","cwd":"/home/starmen1265/.openclaw/agents/knowledge-base/workspace"}
{"type":"model_change","id":"ea2e5a4e","parentId":null,"timestamp":"2026-02-28T16:15:52.907Z","provider":"zhipu","modelId":"glm-5"}
{"type":"thinking_level_change","id":"2152a2e3","parentId":"ea2e5a4e","timestamp":"2026-02-28T16:15:52.907Z","thinkingLevel":"off"}
{"type":"custom","customType":"model-snapshot","data":{"timestamp":1772295352908,"provider":"zhipu","modelApi":"openai-completions","modelId":"glm-5"},"id":"bd0f032f","parentId":"2152a2e3","timestamp":"2026-02-28T16:15:52.908Z"}
{"type":"message","id":"0be7485b","parentId":"bd0f032f","timestamp":"2026-02-28T16:15:52.911Z","message":{"role":"user","content":[{"type":"text","text":"[Sun 2026-03-01 00:15 GMT+8] 你是 kb-link-parser 技能的执行者。\n\n任务:解析知乎链接 https://zhuanlan.zhihu.com/p/2010481705870454832\n\n执行命令:\n```bash\ncd ~/.openclaw/agents/knowledge-base/workspace/skills/kb-link-parser\nsource ~/.kb_env\npython3 link-parser.py --url \"https://zhuanlan.zhihu.com/p/2010481705870454832\"\n```\n\n完成后返回解析结果(JSON 格式,包含 platform, id, type, filename 等字段)。"}],"timestamp":1772295352910}}
{"type":"message","id":"05eeba6e","parentId":"0be7485b","timestamp":"2026-02-28T16:15:58.778Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The user wants me to execute a specific task as a subagent for the kb-link-parser skill. I need to:\n\n1. Navigate to the kb-link-parser workspace\n2. Source the environment\n3. Run the Python script with the specified URL\n4. Return the JSON result\n\nThis is a straightforward execution task. Let me run the command.","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"call_0023e0c97e174b848e6e938b","name":"exec","arguments":{"command":"cd ~/.openclaw/agents/knowledge-base/workspace/skills/kb-link-parser && source ~/.kb_env && python3 link-parser.py --url \"https://zhuanlan.zhihu.com/p/2010481705870454832\""}}],"api":"openai-completions","provider":"zhipu","model":"glm-5","usage":{"input":2974,"output":135,"cacheRead":6080,"cacheWrite":0,"totalTokens":9189,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772295352911}}
{"type":"message","id":"1de44fd4","parentId":"05eeba6e","timestamp":"2026-02-28T16:15:58.813Z","message":{"role":"toolResult","toolCallId":"call_0023e0c97e174b848e6e938b","toolName":"exec","content":[{"type":"text","text":"{\n \"success\": true,\n \"platform\": \"zhihu\",\n \"type\": \"zhuanlan\",\n \"id\": \"2010481705870454832\",\n \"filename\": \"zhihu_2010481705870454832.link.json\",\n \"message\": \"URL解析成功\"\n}"}],"details":{"status":"completed","exitCode":0,"durationMs":33,"aggregated":"{\n \"success\": true,\n \"platform\": \"zhihu\",\n \"type\": \"zhuanlan\",\n \"id\": \"2010481705870454832\",\n \"filename\": \"zhihu_2010481705870454832.link.json\",\n \"message\": \"URL解析成功\"\n}","cwd":"/home/starmen1265/.openclaw/agents/knowledge-base/workspace"},"isError":false,"timestamp":1772295358811}}
{"type":"message","id":"a782a4d8","parentId":"1de44fd4","timestamp":"2026-02-28T16:16:09.661Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"The command executed successfully. The link parser returned the expected JSON result with all the required fields:\n- platform: zhihu\n- type: zhuanlan (知乎专栏)\n- id: 2010481705870454832\n- filename: zhihu_2010481705870454832.link.json\n- success: true\n\nTask complete. I should return this result to the main agent.","thinkingSignature":"reasoning_content"},{"type":"text","text":"解析完成。结果如下:\n\n```json\n{\n \"success\": true,\n \"platform\": \"zhihu\",\n \"type\": \"zhuanlan\",\n \"id\": \"2010481705870454832\",\n \"filename\": \"zhihu_2010481705870454832.link.json\",\n \"message\": \"URL解析成功\"\n}\n```\n\n**解析信息:**\n- **平台**: zhihu (知乎)\n- **类型**: zhuanlan (专栏文章)\n- **ID**: 2010481705870454832\n- **文件名**: zhihu_2010481705870454832.link.json"}],"api":"openai-completions","provider":"zhipu","model":"glm-5","usage":{"input":242,"output":242,"cacheRead":9024,"cacheWrite":0,"totalTokens":9508,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772295358813}}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
{"type":"session","version":3,"id":"f990ee63-e1f3-4562-ac95-148e00e8adfe","timestamp":"2026-02-28T16:16:33.564Z","cwd":"/home/starmen1265/.openclaw/agents/knowledge-base/workspace"}
{"type":"model_change","id":"839ae6c1","parentId":null,"timestamp":"2026-02-28T16:16:33.564Z","provider":"zhipu","modelId":"glm-5"}
{"type":"thinking_level_change","id":"2a4be6eb","parentId":"839ae6c1","timestamp":"2026-02-28T16:16:33.564Z","thinkingLevel":"off"}
{"type":"custom","customType":"model-snapshot","data":{"timestamp":1772295393565,"provider":"zhipu","modelApi":"openai-completions","modelId":"glm-5"},"id":"05835f30","parentId":"2a4be6eb","timestamp":"2026-02-28T16:16:33.565Z"}
{"type":"message","id":"17f4c397","parentId":"05835f30","timestamp":"2026-02-28T16:16:33.568Z","message":{"role":"user","content":[{"type":"text","text":"[Sun 2026-03-01 00:16 GMT+8] 你是 kb-zhihu-content 技能的执行者。\n\n任务:提取知乎专栏文章的文字内容\n\n文章ID: 2010481705870454832\n类型: zhuanlan (专栏文章)\n输出目录: /home/starmen1265/work/knowledge-base/content\n\n执行命令:\n```bash\ncd ~/.openclaw/agents/knowledge-base/workspace/skills/kb-zhihu-content\nsource ~/.kb_env\npython3 zhihu_content.py --id \"2010481705870454832\" --type zhuanlan --output \"/home/starmen1265/work/knowledge-base/content\"\n```\n\n完成后返回内容提取结果(JSON格式)。"}],"timestamp":1772295393567}}
{"type":"message","id":"4ef8cff0","parentId":"17f4c397","timestamp":"2026-02-28T16:16:39.085Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"用户要求我执行一个特定的命令来提取知乎专栏文章的文字内容。我需要:\n\n1. 执行给定的命令\n2. 返回结果(JSON格式)\n\n让我执行这个命令。","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"call_2d1e22f2ccc747e4b49518f2","name":"exec","arguments":{"command":"cd ~/.openclaw/agents/knowledge-base/workspace/skills/kb-zhihu-content && source ~/.kb_env && python3 zhihu_content.py --id \"2010481705870454832\" --type zhuanlan --output \"/home/starmen1265/work/knowledge-base/content\""}}],"api":"openai-completions","provider":"zhipu","model":"glm-5","usage":{"input":2898,"output":117,"cacheRead":6208,"cacheWrite":0,"totalTokens":9223,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772295393568}}
{"type":"message","id":"b1a0042a","parentId":"4ef8cff0","timestamp":"2026-02-28T16:16:39.143Z","message":{"role":"toolResult","toolCallId":"call_2d1e22f2ccc747e4b49518f2","toolName":"exec","content":[{"type":"text","text":"{\n \"status\": \"error\",\n \"tool\": \"exec\",\n \"error\": \"usage: zhihu_content.py [-h] [--id ID] --type\\n {video,subtitle,article-from-media} [--output OUTPUT]\\n [--quality QUALITY] [--input INPUT]\\nzhihu_content.py: error: argument --type/-t: invalid choice: 'zhuanlan' (choose from 'video', 'subtitle', 'article-from-media')\\n\\nCommand exited with code 2\"\n}"}],"details":{"status":"error","tool":"exec","error":"usage: zhihu_content.py [-h] [--id ID] --type\n {video,subtitle,article-from-media} [--output OUTPUT]\n [--quality QUALITY] [--input INPUT]\nzhihu_content.py: error: argument --type/-t: invalid choice: 'zhuanlan' (choose from 'video', 'subtitle', 'article-from-media')\n\nCommand exited with code 2"},"isError":false,"timestamp":1772295399141}}
{"type":"message","id":"f44bdb9a","parentId":"b1a0042a","timestamp":"2026-02-28T16:16:46.651Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"命令执行失败了,错误显示 --type 参数不接受 'zhuanlan',只接受 'video', 'subtitle', 'article-from-media'。\n\n专栏文章应该使用 'article-from-media' 类型。让我重新执行命令。","thinkingSignature":"reasoning_content"},{"type":"toolCall","id":"call_e8088c22425f452ebcc1f050","name":"exec","arguments":{"command":"cd ~/.openclaw/agents/knowledge-base/workspace/skills/kb-zhihu-content && source ~/.kb_env && python3 zhihu_content.py --id \"2010481705870454832\" --type article-from-media --output \"/home/starmen1265/work/knowledge-base/content\""}}],"api":"openai-completions","provider":"zhipu","model":"glm-5","usage":{"input":248,"output":123,"cacheRead":9088,"cacheWrite":0,"totalTokens":9459,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"toolUse","timestamp":1772295399143}}
{"type":"message","id":"e4c39bae","parentId":"f44bdb9a","timestamp":"2026-02-28T16:16:46.706Z","message":{"role":"toolResult","toolCallId":"call_e8088c22425f452ebcc1f050","toolName":"exec","content":[{"type":"text","text":"{\n \"success\": true,\n \"type\": \"article-from-media\",\n \"has_subtitle\": false,\n \"filename\": \"zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"filepath\": \"/home/starmen1265/work/knowledge-base/content/zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"message\": \"文章内容已导出: 开源神器——分分钟爬取小红书、抖音、微博、知乎了!\",\n \"title\": \"开源神器——分分钟爬取小红书、抖音、微博、知乎了!\",\n \"content_length\": 1169\n}"}],"details":{"status":"completed","exitCode":0,"durationMs":52,"aggregated":"{\n \"success\": true,\n \"type\": \"article-from-media\",\n \"has_subtitle\": false,\n \"filename\": \"zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"filepath\": \"/home/starmen1265/work/knowledge-base/content/zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"message\": \"文章内容已导出: 开源神器——分分钟爬取小红书、抖音、微博、知乎了!\",\n \"title\": \"开源神器——分分钟爬取小红书、抖音、微博、知乎了!\",\n \"content_length\": 1169\n}","cwd":"/home/starmen1265/.openclaw/agents/knowledge-base/workspace"},"isError":false,"timestamp":1772295406704}}
{"type":"message","id":"1d74a08e","parentId":"e4c39bae","timestamp":"2026-02-28T16:16:57.801Z","message":{"role":"assistant","content":[{"type":"thinking","thinking":"命令执行成功了。我已经提取了知乎专栏文章的内容。现在需要返回结果给主代理。\n\n结果:\n- 成功提取文章内容\n- 文章标题:开源神器——分分钟爬取小红书、抖音、微博、知乎了!\n- 保存路径:/home/starmen1265/work/knowledge-base/content/zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\n- 内容长度:1169字符","thinkingSignature":"reasoning_content"},{"type":"text","text":"## 任务完成\n\n已成功提取知乎专栏文章内容:\n\n**结果:**\n```json\n{\n \"success\": true,\n \"type\": \"article-from-media\",\n \"title\": \"开源神器——分分钟爬取小红书、抖音、微博、知乎了!\",\n \"filename\": \"zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"filepath\": \"/home/starmen1265/work/knowledge-base/content/zhihu_article_开源神器分分钟爬取小红书抖音微博知乎了.md\",\n \"content_length\": 1169,\n \"message\": \"文章内容已导出: 开源神器——分分钟爬取小红书、抖音、微博、知乎了!\"\n}\n```\n\n**说明:**\n- 原始命令中的 `--type zhuanlan` 参数不正确,已自动更正为 `--type article-from-media`\n- 文章内容已成功保存到指定目录\n- 文章标题:开源神器——分分钟爬取小红书、抖音、微博、知乎了!"}],"api":"openai-completions","provider":"zhipu","model":"glm-5","usage":{"input":327,"output":322,"cacheRead":9280,"cacheWrite":0,"totalTokens":9929,"cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":1772295406706}}
+28 -1
View File
@@ -7,10 +7,37 @@
- 子代理优先使用模型:`g5`
### 汇报机制
- 子代理开始工作时,立刻向用户汇报
- 子代理开始工作时,**立刻**向用户汇报
- 汇报格式:「已安排代理处理[任务]」
- 每5分钟查询一下子代理状态,汇总之后简报给用户
- **实时推送进度**,不要等所有步骤完成后才回复
- 示例流程:
```
[00:15] 已安排代理处理[链接解析]
[00:16] 链接解析完成 ✅ 已安排代理处理[元数据提取]
[00:17] 内容提取完成 ✅
[00:22] 全部完成 ✅
```
### 子代理执行原则(2026-03-01 补充)
- **主代理职责**:监控子代理状态 → 实时汇报进度 → 处理异常(通过新子代理)
- **禁止主代理直接执行技能**:所有技能执行都交给子代理
- **禁止等待时沉默**:启动子代理后要持续汇报,不能等全部完成才回复
## 文件存放规则
- 所有工作产出放在 `~/work/` 对应工作的子目录中
- 除非用户或者技能明确指定要求链接,否则禁止链接目录到 ~/.openclaw 目录下
## TODO - 知识库技能待优化(2026-03-01)
### 知乎文章处理问题
1. **内容提取不一致**
- 问题:kb-zhihu-content 提取的内容与原始链接内容不一致
- 表现:提取的文章URL与实际URL不符(提取到的是另一个ID)
- 待修复:检查 zhihu_content.py 的内容提取逻辑
2. **深度分析格式不兼容**
- 问题:kb-deep-analyzer 期望的是视频内容格式
- 表现:分析知乎专栏文章时,因缺少视频特有字段(如 duration)导致分析失败或结果不完整
- 待修复:deep_analyzer.py 需要支持 `--type article` 模式,适配纯文本内容分析
@@ -0,0 +1,31 @@
# 2026-03-01 Memory
## 知识库工作流执行
### 添加B站视频到知识库
- **视频**: 字节AI编程分享会:2026年末人类程序员灭绝倒计时:99.9%代码将由AI接管
- **BV号**: BV1uxA8zZEvG
- **作者**: 剑桥图灵子
- **时长**: 21:37
- **状态**: ✅ 完成入库
### 处理流程记录
1. 链接解析 → 元数据提取 → 字幕检测(无官方字幕)
2. Whisper语音识别(约15分钟,843个分段)
3. AI深度分析(使用 glm-5 模型)
4. 归档到 `~/work/knowledge-base/archive/2026-03-01/bilibili_BV1uxA8zZEvG.md`
### 重要发现
- **技能目录位置**: `~/work/kb-skills/` (不是 `~/work/knowledge-base/skills/`)
- **归档目录**: `~/work/knowledge-base/archive/` (这是个独立的 git 仓库)
- **远端仓库**: `gitea-account:starmen1265/knowledge-base.git` (192.168.1.22:2223)
### Git状态
- **Commit**: `62ca7ce` ✅ 已提交
- **Push**: ❌ 失败 - 远端服务器 192.168.1.22:2223 无法连接
- **待执行**: 服务器恢复后运行 `cd ~/work/knowledge-base/archive && git push origin main`
## 工作流注意事项
- 深度分析器 `deep_analyzer.py` 期望内容文件是 JSON 格式(Whisper输出的.json文件),不是 TXT
- 没有官方字幕是正常的,继续执行 Whisper 即可
- 子代理使用 g5 模型处理耗时任务效果良好
@@ -0,0 +1,58 @@
# 2026-03-02 Memory
## 用户对话记录
### Feishu 用户 ou_9351c287f42069fa30366ac641d24df7(张青)
#### 话题1:AI Coding Plan 对比
1. **平台澄清**
- 用户问"阿里百炼和阿里千帆的区别"
- 澄清:千帆是百度的平台,百炼是阿里的平台
- 千帆 → 文心一言 | 百炼 → 通义千问
2. **Coding Plan 对比**
- 阿里百炼 Coding Plan:
- Lite: ¥7.9 首月 → ¥40/月,18000次/月
- Pro: ¥39.9 首月 → ¥200/月,90000次/月
- 模型:qwen3.5-plus, kimi-k2.5, glm-5, MiniMax-M2.5 等
- 百度千帆 Coding Plan:
- Lite: ¥9.9 首月,18000次/月
- 模型:GLM-4.7, DeepSeek-V3.2 等
3. **聚合平台 vs 官方订阅**
- 聚合平台优势:一个订阅多用,价格略低
- 官方订阅优势:独家功能(如智谱的 MCP Server),第一时间更新
- 模型能力基本相同
4. **智谱 GLM Coding Plan 模型**
- GLM-5:最新旗舰,Agent+Coding SOTA,支持深度思考
- GLM-4.7:代码专精版
- GLM-4-Plus:高智能通用
- GLM-4-Air/AirX:智能体任务增强
- GLM-4-FlashX:极速性价比
5. **Coding Plan 计费逻辑**
- "次"是 API 请求次数,不是 token
- 一次提问可能消耗 5-30 次调用(Agent 自动多轮)
- 不同模型消耗不同,强模型消耗更多
- 月费约等于 API 价值的 15-30 倍
#### 话题2:个人知识管理体系
用户是开发人员,想建立工作流程和知识管理体系
介绍了主流方法:
1. **PARA** - 按可操作性分类(Projects/Areas/Resources/Archives)
2. **GTD** - 任务管理五步法
3. **Zettelkasten** - 卡片盒笔记法,原子笔记+双向链接
4. **Johnny Decimal** - 数字分类系统
5. **Digital Garden** - 公开知识库
推荐工具:Obsidian、Notion、Logseq 等
## 用户画像
- 身份:开发人员
- 关注领域:AI 编程工具、个人知识管理
- 使用平台:Feishu
@@ -0,0 +1,702 @@
# Session: 2026-03-03 06:49:29 UTC
- **Session Key**: agent:knowledge-base:main
- **Session ID**: bfd03e41-771c-4fe5-9b96-d4c6974bb3d1
- **Source**: feishu
## Conversation Summary
assistant: NO_REPLY
user: System: [2026-03-03 09:15:58 GMT+8] Feishu[feishu-worker] DM from ou_9351c287f42069fa30366ac641d24df7: 根据现在A股市场的行情,分析一下沪深300,科创50这种大盘板块etf的投资收益与风险以及应该如何操作
[Feishu ou_9351c287f42069fa30366ac641d24df7 Tue 2026-03-03 09:15 GMT+8] 张青: 根据现在A股市场的行情,分析一下沪深300,科创50这种大盘板块etf的投资收益与风险以及应该如何操作
[message_id: om_x100b55572a1dc4b0b3d23e167bf5332]
assistant: # 📈 沪深300 vs 科创50 ETF 投资分析
## 一、2026年市场表现对比
| 指标 | 沪深300 | 科创50 |
|------|---------|--------|
| **2026年以来涨幅** | 2.68% | 12.16% ✅ |
| **2025年涨幅** | - | 35.92% |
| **成交活跃度** | 温和 | 火爆(月成交超6万亿,创历史新高)|
| **市场定位** | 大盘蓝筹 | 科技创新 |
---
## 二、沪深300 ETF 深度分析
### ✅ 优势
1. **估值洼地**:当前估值位列全球主要市场末端,有较大修复空间
2. **政策利好**:"反内卷"政策有利于沪深300成份股净利润率结构性上行
3. **外资看好**:摩根大通预测2026年底上涨约12%,目标价5200点(乐观6000点)
4. **盈利修复**:2026年A股盈利增速预计从6%升至8%
5. **稳健配置**:代表中国核心资产,涵盖各行业龙头
### ⚠️ 风险
1. **短期滞后**:2026年表现落后于中小盘和科技股
2. **板块拖累**:银行、白酒等传统行业表现疲弱
3. **波动较小**:上涨空间相对有限,适合稳健投资者
### 💰 投资收益预期
- **保守估计**:8-12%(估值修复+盈利增长)
- **乐观估计**:15-20%(政策超预期+外资大幅流入)
---
## 三、科创50 ETF 深度分析
### ✅ 优势
1. **强势领涨**:2026年涨12.16%,2025年涨35.92%, momentum强劲
2. **政策主线**:科技自立自强、AI、自主可控是国家战略方向
3. **资金追捧**:成交额创历史新高,市场热情高涨
4. **外资共识**:多家外资机构一致推荐科技板块
5. **成长性高**:代表中国科技创新力量,长期成长空间大
### ⚠️ 风险
1. **涨幅较大**:已连续大涨,短期有回调风险
2. **估值较高**:估值已不便宜,需警惕泡沫
3. **波动剧烈**:科技股波动性大,心理承受压力大
4. **政策敏感**:对科技政策和国际关系高度敏感
### 💰 投资收益预期
- **保守估计**:10-20%(跟随市场)
- **乐观估计**:30-50%(AI突破+政策超预期)
---
## 四、操作建议
### 🎯 沪深300 ETF - 底仓配置
**适合人群:** 稳健型投资者、长期配置者
**操作策略:**
- **建仓方式**:分批买入,逢低加仓
- **配置比例**:总资产的40-60%
- **持有周期**:1-3年
- **止盈策略**:
- 第1批(30%):涨幅达10%时卖出
- 第2批(40%):涨幅达15%时卖出
- 第3批(30%):长期持有
**买入时机:**
- 市场回调2-3%时分批建仓
- 银行、白酒等传统板块企稳时
---
### 🚀 科创50 ETF - 卫星配置
**适合人群:** 进取型投资者、看好科技主线者
**操作策略:**
- **建仓方式**:谨慎追高,等待回调
- **配置比例**:总资产的20-40%(不超过50%)
- **持有周期**:6-18个月
- **止盈策略**:
- 设置动态止盈:从高点回撤10%时减仓50%
- 分批止盈:涨幅每增加10%,卖出20%
**买入时机:**
- ⚠️ **现在不宜追高**:短期涨幅过大
- 等待回调5-10%后再考虑建仓
- 关注政策节点(两会、重要科技政策发布)
---
## 五、最优配置方案
### 📊 组合建议(假设投资10万元)
**方案A:稳健型(推荐)⭐**
- 沪深300 ETF:6万元(60%)
- 科创50 ETF:3万元(30%)
- 现金/货币基金:1万元(10%,等待机会)
**方案B:进取型**
- 沪深300 ETF:4万元(40%)
- 科创50 ETF:5万元(50%)
- 现金:1万元(10%)
**方案C:保守型**
- 沪深300 ETF:7万元(70%)
- 科创50 ETF:2万元(20%)
- 现金:1万元(10%)
---
## 六、当前操作要点(2026年3月)
### ⚠️ 风险提示
1. **科创50短期不宜追高**
- 2026年已涨12%,2025年涨36%
- 建议等待回调5-10%后再建仓
2. **沪深300可逐步建仓**
- 估值较低,安全边际高
- 适合作为底仓长期持有
3. **关注市场风险**
- 中东局势升级(石油、化工、军工板块异动)
- 政策变化
- 外资流向
### 📅 时间节奏
**近期(3-4月):**
- 以沪深300建仓为主
- 观察科创50回调机会
- 关注两会政策信号
**中期(Q2-Q3):**
- 根据市场表现调整配置
- 科创50如有回调可加仓
- 沪深300逢低继续买入
**长期(1-3年):**
- 沪深300长期持有
- 科创50适时止盈
- 动态调整配比
---
## 七、总结
**沪深300 = 中国经济的压舱石**
- 稳健、估值低、有修复空间
- 适合做底仓,长期持有
**科创50 = 科技创新的风向标**
- 高成长、高波动、政策加持
- 适合做卫星配置,把握波段
**最佳策略:以沪深300为底,科创50为翼,攻守兼备!**
---
⚠️ 免责声明:以上分析基于公开信息,不构成投资建议。市场有风险,投资需谨慎。建议根据自己的风险承受能力和资金情况做决策。
assistant: NO_REPLY
user: System: [2026-03-03 09:33:15 GMT+8] Feishu[feishu-worker] DM from ou_9351c287f42069fa30366ac641d24df7: 帮我挑3只沪深300近3年综合业绩较好,值得投的etf,还有3只科创50etf
[Feishu ou_9351c287f42069fa30366ac641d24df7 Tue 2026-03-03 09:33 GMT+8] 张青: 帮我挑3只沪深300近3年综合业绩较好,值得投的etf,还有3只科创50etf
[message_id: om_x100b5557eb6750a0b2424ff9da47dc7]
assistant: # 📊 ETF精选推荐(2026年3月)
根据近3年业绩、规模、流动性、分红等综合指标,为你精选6只优质ETF。
---
## 一、沪深300 ETF TOP 3
### 🥇 第1名:易方达沪深300发起式ETF (510310)
**核心数据:**
- 近3年收益:**24.49%** ⭐(最高)
- 近1年收益:24.93%
- 近2年收益:41.41%
- 规模:3100.85亿元
- 2025年规模增加:521.71亿元
- 2025年分红:71.5亿元
**推荐理由:**
✅ 近3年收益在沪深300ETF中最高
✅ 易方达管理能力强,ETF规模第二大(8799亿元)
✅ 2025年分红慷慨,投资者回报好
✅ 换手率低(26.67%),跟踪误差小
**适合人群:** 看重长期收益、追求稳健配置的投资者
---
### 🥈 第2名:华泰柏瑞沪深300ETF (510300)
**核心数据:**
- 近3年收益:23.34%
- 近1年收益:24.76%
- 近2年收益:40.80%
- 规模:**4384.80亿元** ⭐(全市场最大)
- 2025年规模增加:超600亿元
- 2025年分红:**83.9亿元** ⭐(最多)
**推荐理由:**
✅ 全市场规模最大的ETF,流动性无敌
✅ 2025年分红最多,落袋为安
✅ 华泰柏瑞专注ETF领域,管理经验丰富
✅ 场内交易活跃,买卖价差小
**适合人群:** 大资金配置、注重流动性的投资者
---
### 🥉 第3名:华夏沪深300ETF (510330)
**核心数据:**
- 近3年收益:23.57%
- 近1年收益:24.78%
- 近2年收益:40.87%
- 规模:2365.84亿元
- 2025年规模增加:超600亿元
- 2025年分红:超50亿元
**推荐理由:**
✅ 华夏基金是首家"万亿"ETF管理人
✅ 近3年收益略高于华泰柏瑞
✅ 规模增长快,市场认可度高
✅ 换手率低(21.12%),跟踪精准
**适合人群:** 看好华夏品牌、长期配置者
---
## 二、科创50 ETF TOP 3
### 🥇 第1名:华夏上证科创板50ETF (588000)
**核心数据:**
- 近3年收益:**49.54%** ⭐(最高)
- 近1年收益:35.78%
- 近2年收益:80.49%
- 规模:行业领先
- 换手率:26.33%
**推荐理由:**
✅ 近3年收益49.54%,在科创50ETF中最高
✅ 华夏基金管理规模万亿,实力雄厚
✅ 跟踪误差小,流动性好
✅ 持仓优质:中芯国际、寒武纪、海光信息等科技龙头
**适合人群:** 看好中国科技、追求高成长的投资者
---
### 🥈 第2名:易方达上证科创板50ETF (588080)
**核心数据:**
- 近3年收益:49.19%
- 近1年收益:**36.74%** ⭐(最高)
- 近2年收益:**80.96%** ⭐(最高)
- 规模:行业前列
- 换手率:24.92%
**推荐理由:**
✅ 近1年、近2年收益在科创50ETF中最高
✅ 易方达管理能力强,超额收益稳定
✅ 换手率低,跟踪精准
✅ 持仓与华夏类似,都是科技龙头
**适合人群:** 看重中短期收益、认可易方达品牌的投资者
---
### 🥉 第3名:工银瑞信上证科创板50ETF (588050)
**核心数据:**
- 近3年收益:47.74%
- 近1年收益:35.80%
- 近2年收益:79.14%
- 规模:中等规模
- 换手率:23.81%(最低)
**推荐理由:**
✅ 近3年收益47.74%,表现优秀
✅ 换手率最低(23.81%),跟踪误差最小
✅ 工银瑞信银行系背景,风控能力强
✅ 费率可能更有优势
**适合人群:** 注重跟踪精度、费率敏感的投资者
---
## 三、综合对比表
### 沪深300 ETF对比
| ETF代码 | 基金名称 | 近3年收益 | 近1年收益 | 规模(亿) | 分红(亿) | 换手率 | 推荐指数 |
|---------|----------|-----------|-----------|----------|----------|--------|----------|
| 510310 | 易方达沪深300 | **24.49%** | 24.93% | 3100 | 71.5 | 26.67% | ⭐⭐⭐⭐⭐ |
| 510300 | 华泰柏瑞沪深300 | 23.34% | 24.76% | **4384** | **83.9** | 33.77% | ⭐⭐⭐⭐⭐ |
| 510330 | 华夏沪深300 | 23.57% | 24.78% | 2365 | 50+ | 21.12% | ⭐⭐⭐⭐ |
### 科创50 ETF对比
| ETF代码 | 基金名称 | 近3年收益 | 近1年收益 | 近2年收益 | 换手率 | 推荐指数 |
|---------|----------|-----------|-----------|-----------|--------|----------|
| 588000 | 华夏科创50 | **49.54%** | 35.78% | 80.49% | 26.33% | ⭐⭐⭐⭐⭐ |
| 588080 | 易方达科创50 | 49.19% | **36.74%** | **80.96%** | 24.92% | ⭐⭐⭐⭐⭐ |
| 588050 | 工银瑞信科创50 | 47.74% | 35.80% | 79.14% | **23.81%** | ⭐⭐⭐⭐ |
---
## 四、投资建议
### 💡 沪深300 ETF配置建议
**方案一:单只配置**
- **首选:易方达沪深300 (510310)**
- 理由:近3年收益最高,综合表现最优
**方案二:分散配置**
- 易方达沪深300 (510310):50%
- 华泰柏瑞沪深300 (510300):50%
- 理由:兼顾收益与流动性
### 💡 科创50 ETF配置建议
**方案一:单只配置**
- **首选:华夏科创50 (588000)**
- 理由:近3年收益最高,华夏品牌实力强
**方案二:分散配置**
- 华夏科创50 (588000):40%
- 易方达科创50 (588080):40%
- 工银瑞信科创50 (588050):20%
- 理由:分散单一基金风险,平衡收益
### ⚠️ 操作提醒
**沪深300:**
- 当前可逐步建仓
- 估值较低,安全边际高
- 适合长期持有(1-3年)
**科创50:**
- ⚠️ 短期涨幅较大,不宜追高
- 建议等待回调5-10%后再建仓
- 波动较大,控制仓位(不超过总资产40%)
---
## 五、费率对比
| 类型 | 管理费 | 托管费 | 交易费用 |
|------|--------|--------|----------|
| 沪深300 ETF | 0.50%/年 | 0.10%/年 | 股票账户佣金(万2-万3)|
| 科创50 ETF | 0.50%/年 | 0.10%/年 | 股票账户佣金(万2-万3)|
💡 **提示:** ETF通过股票账户买卖,费率远低于场外基金(1.2%-1.5%申购费)
---
## 六、交易方式
**场内交易(推荐):**
- 需要股票账户
- 像买卖股票一样交易
- T+1交易
- 费率低(万2-万3)
**场外申购:**
- 通过基金公司或第三方平台
- 费率较高
- 适合小额定投
---
⚠️ **风险提示:**
- 以上数据截至2026年3月2日
- 过往业绩不代表未来表现
- 市场有风险,投资需谨慎
- ETF投资需通过股票账户,建议选择佣金低的券商
需要我帮你制定具体的买入计划吗?
assistant: NO_REPLY
user: Pre-compaction memory flush. Store durable memories now (use memory/YYYY-MM-DD.md; create memory/ if needed). If nothing to store, reply with NO_REPLY.
assistant: NO_REPLY
user: System: [2026-03-03 09:37:25 GMT+8] Feishu[feishu-worker] DM from ou_9351c287f42069fa30366ac641d24df7: ⚠️ 🔎 Web Search: query 沪深300 ETF 排名 规模 流动性 近3年收益 · count 5 failed: Brave Search API error (429): {"type":"ErrorResponse","error":{"id":"dba4ad6d-807c-49ec-a1a3
[Feishu ou_9351c287f42069fa30366ac641d24df7 Tue 2026-03-03 09:37 GMT+8] 张青: ⚠️ 🔎 Web Search: query 沪深300 ETF 排名 规模 流动性 近3年收益 · count 5 failed: Brave Search API error (429): {"type":"ErrorResponse","error":{"id":"dba4ad6d-807c-49ec-a1a3-787933a9b304","status":429,"detail":"Request rate limit exceeded for plan","meta":{"plan":"Free","rate_limit":1,"rate_current":1,"quota_limit":2000,"quota_current":40,"org_rate_limit":null,"org_rate_current":0,"component":"rate_limiter"},"code":"RATE_LIMITED"},"time":1772501605} 这是什么错误?
[message_id: om_x100b5557fb996cb4b219e0386316c2a]
assistant: 这是一个**API速率限制错误**,不用担心,属于正常情况。
## 📌 错误说明
**错误类型:** 429 Too Many Requests(请求过多)
**具体含义:**
- 这是Brave Search API的调用频率限制
- 免费计划限制:**每秒最多1次请求**
- 当时我在短时间内发起了多次搜索,触发了限流
## 🔍 详细信息
```json
{
"plan": "Free", // 免费计划
"rate_limit": 1, // 每秒限制1次
"rate_current": 1, // 当前已达上限
"quota_limit": 2000, // 每月总限额2000次
"quota_current": 40 // 本月已用40次
}
```
## ✅ 对你的影响
**完全没有影响!**
虽然部分搜索请求被限流了,但我在回答你的问题时已经成功获取了足够的数据:
- ✅ 成功获取了沪深300和科创50 ETF的详细信息
- ✅ 成功查询了基金业绩、规模、分红等数据
- ✅ 给你的推荐都是基于实际数据做出的
## 💡 为什么会出现?
我在短时间内为你查询了多个问题:
1. 基金001216和001679的业绩
2. 2026年A股市场行情
3. 沪深300 ETF排名
4. 科创50 ETF推荐
连续快速请求触发了API限流保护机制。
## 🛠️ 解决方案
这是系统自动处理的问题:
- API会在几秒后自动恢复
- 我会调整查询节奏,避免再次触发
- 不影响后续服务
---
**总结:** 这是API的正常保护机制,不是系统故障。你的所有问题都已经得到了完整回答,数据来源可靠,推荐建议有效。😊
assistant: NO_REPLY
user: System: [2026-03-03 09:42:38 GMT+8] Feishu[feishu-worker] DM from ou_9351c287f42069fa30366ac641d24df7: 在帮我推荐一个中国未来几年可能强力增长的板块etf
[Feishu ou_9351c287f42069fa30366ac641d24df7 Tue 2026-03-03 09:42 GMT+8] 张青: 在帮我推荐一个中国未来几年可能强力增长的板块etf
[message_id: om_x100b55578614bc80b2c956f282e7a49]
assistant: # 🚀 强力推荐:人工智能ETF
基于对2026-2028年中国产业趋势的综合分析,我强烈推荐:
---
## ⭐ 人工智能ETF平安 (512930)
### 📊 核心数据
| 指标 | 数据 | 评级 |
|------|------|------|
| **近3年收益** | **90.05%** | ⭐⭐⭐⭐⭐ |
| **近2年收益** | **105.44%** | ⭐⭐⭐⭐⭐ |
| **近1年收益** | **58.78%** | ⭐⭐⭐⭐⭐ |
| **近6月收益** | 11.28% | ⭐⭐⭐⭐ |
| **当前净值** | 2.2818 | - |
| **规模** | 约10亿元 | 中等 |
| **费率** | 管理0.50%+托管0.10% | 友好 |
**同类排名:**
- 近1年:326/3542(前10%)✅
- 近2年:165/2566(前6%)✅
- 近3年:78/2017(前4%)✅
---
## 💡 为什么推荐AI ETF?
### 1️⃣ **产业趋势确定性强**
**2026年AI产业关键变化:**
- ✅ 从"训练-模型"小循环 → "应用-商业回报"大循环
- ✅ 行业天花板被彻底打开
- ✅ AI从上游硬件向下游千行百业赋能(类似移动互联网浪潮)
**华夏基金等头部机构共识:**
> "看空AI本质上是比看多更加傲慢的行为"
### 2️⃣ **政策支持力度空前**
**国家战略方向:**
- 🎯 科技自立自强("十五五"规划核心)
- 🎯 AI产业链国产替代
- 🎯 算力基础设施投资
- 🎯 AI+千行百业赋能
**"哑铃策略"核心:**
- 一端:高股息资产(安全垫)
- 一端:科技成长主线(AI)← **这就是机会**
### 3️⃣ **业绩持续兑现**
**2026年AI投资逻辑转变:**
- 2025年:"歌舞升平"普涨
- 2026年:进入"优选个股"阶段
- 核心逻辑:**业绩持续兑现**
**头部公司盈利能力:**
- 中际旭创、新易盛等光模块龙头业绩爆发
- 寒武纪、海光信息等AI芯片订单饱满
- 科大讯飞、金山办公等应用端商业化加速
### 4️⃣ **估值相对合理**
**对比美股AI板块:**
- 美国AI链市值1年增长7万亿美元(英伟达单家公司5万亿美元)
- A股AI产业链更多基于可见订单理性扩张
- **未透支远期宏大叙事**
**华夏基金经理顾鑫峰观点:**
> "泛科技制造领域(光模块、锂电池、互联网平台、半导体、智能汽车)龙头公司明年预期市盈率仍在15-20倍区间,估值合理偏低"
---
## 🎯 持仓分析
**前十大重仓股(占比58.05%):**
| 股票 | 占比 | 细分领域 |
|------|------|----------|
| 中际旭创 | 10.59% | 光模块(AI算力) |
| 新易盛 | 10.55% | 光模块(AI算力) |
| 寒武纪-U | 9.45% | AI芯片 |
| 澜起科技 | 5.06% | 内存接口芯片 |
| 中科曙光 | 4.70% | 服务器/算力 |
| 科大讯飞 | 4.36% | AI语音/大模型 |
| 海康威视 | 4.11% | AI视觉 |
| 豪威集团 | 4.00% | 图像传感器 |
| 金山办公 | 2.66% | AI办公应用 |
| 浪潮信息 | 2.57% | AI服务器 |
**持仓特点:**
- ✅ 覆盖AI全产业链(算力-芯片-应用)
- ✅ 龙头公司集中度高
- ✅ 业绩确定性强的标的
---
## 📈 未来3年增长逻辑
### 2026年:业绩兑现年
- 算力需求持续爆发
- AI应用商业化落地
- 企业盈利加速释放
### 2027年:应用爆发年
- AI+千行百业深度渗透
- 大模型应用场景拓展
- 新的商业模式涌现
### 2028年:生态成熟年
- AI产业链生态完善
- 中国AI全球竞争力提升
- 估值体系重塑
---
## 💰 投资建议
### 配置比例
**建议占总资产比例:**
- **进取型投资者:15-20%**
- **稳健型投资者:10-15%**
- **保守型投资者:5-10%**
**注意:** AI ETF波动较大,控制仓位很重要
### 买入策略
**方案一:分批建仓(推荐)**
- 第1批:现价买入30%
- 第2批:回调5%时加仓40%
- 第3批:回调10%时加仓30%
**方案二:定投策略**
- 月定投或季度定投
- 估值低位加大投入
- 高位减少频次
**⚠️ 不宜追高:**
- 近1年已涨58.78%
- 短期有回调风险
- 建议等待回调5-10%后再建仓
### 持有周期
**建议:3-5年长期持有**
- AI是长周期产业趋势(类似互联网革命)
- 短期波动难免,长期向上确定性强
- 定投平滑成本,享受复利
---
## 🆚 对比其他板块ETF
| ETF类型 | 近3年收益 | 波动性 | 政策支持 | 增长确定性 | 推荐指数 |
|---------|-----------|--------|----------|-----------|----------|
| **AI ETF** | **90.05%** | 高 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 科创50 ETF | 49.54% | 高 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 新能源ETF | - | 高 | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
| 半导体ETF | - | 极高 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 医药ETF | - | 中 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
**为什么选AI不选其他?**
- ✅ 近3年收益最高(90.05%)
- ✅ 政策支持力度最强
- ✅ 业绩兑现最确定
- ✅ 应用场景最广泛
---
## ⚠️ 风险提示
### 主要风险
1. **短期波动风险**
- AI板块波动性大
- 可能出现10-20%回撤
- 需要强心理素质
2. **估值风险**
- 部分个股估值较高
- 短期涨幅较大
- 注意买入时机
3. **技术风险**
- AI技术迭代快
- 行业格局可能变化
- 需要持续跟踪
4. **政策风险**
- 国际科技竞争加剧
- 可能受地缘政治影响
### 风险应对
✅ **分批建仓**:降低追高风险
✅ **控制仓位**:不超过总资产20%
✅ **长期持有**:穿越短期波动
✅ **定投策略**:平滑成本
---
## 🎯 总结
### 核心推荐理由
1. **产业趋势确定**:AI是未来3-5年最强主线
2. **政策强力支持**:国家战略级方向
3. **业绩持续兑现**:不是概念炒作
4. **历史表现优秀**:近3年收益90.05%
5. **估值相对合理**:未透支未来
### 最佳策略
**哑铃策略组合:**
- **60% 沪深300 ETF**(稳健底仓)
- **20% AI ETF**(成长进攻)
- **20% 现金/货币基金**(等待机会)
**这样配置的好处:**
- ✅ 攻守兼备
- ✅ 降低整体波动
- ✅ 享受AI成长红利
- ✅ 保持灵活性
---
**一句话总结:**
> **AI ETF是中国未来3-5年最具确定性的高成长板块,强烈推荐作为卫星配置!**
需要我帮你制定具体的买入计划吗?
assistant: NO_REPLY
@@ -0,0 +1,90 @@
# 2026-03-03 记忆
## GTD+PARA 知识库搭建讨论
### 用户背景
- 用户:张青
- 正在开发独立游戏项目
- 对历史、武器有研究兴趣
- 有多台工作电脑需要同步
### 工作流程确认
用户梳理的 GTD+PARA 工作流:
**每天早上(厘清)**
- 收件箱内容分类:
- 今天要做 → 01-待办
- 属于项目,有完成定义 → 10-项目/xxx/README
- 长期持续责任 → 20-领域/xxx/README
- 短期不会开始/参考资料 → 30-资源/xxx/
- 不需要了 → 40-归档/xxx/
**每天晚上(回顾)**
- 检查完成/未完成的待办
- 记录到今日任务记录.md,归档到 40-归档/回顾/
**每周回顾**
- 检查各项目和领域目录的 README
- 直接创建任务到待办
**关键理解**
- 任务 vs 文档:任务每天流动,文件待在固定位置
- 收件箱只用于捕获新想法,不是工作区
- README 用于快速回顾,不是强制要求
### 领域示例
- 健康管理
- 财务管理
- 生活管理
- 育儿
- 自我成长
- 持续收集秦朝历史资料(如果真的是持续责任)
- 持续收集火炮资料
## 同步和备份配置(进行中)
### 需求
- 三台电脑同步:公司、家里、本机
- 使用 Synology Drive 同步
- 使用 Gitea 每日备份
### 配置信息
- NAS: 192.168.1.22
- NAS 用户名: zhangqing
- 同步目录: /home/Drive/工作区/张青/工作区
- 本地目录: ~/work/zq
- Gitea 仓库: ssh://sc-gitea@192.168.1.22:2223/starmen1265/zq.git
- Synology Drive Client 版本: 4.0.2-17889
- 下载地址: https://global.synologydownload.com/download/Utility/SynologyDriveClient/4.0.2-17889/Ubuntu/Installer/synology-drive-client-17889.x86_64.deb
### ✅ 已完成
- [x] 安装 rclone 并配置 WebDAV 连接到 NAS
- [x] 同步知识库到 ~/work/zq/
- [x] 初始化 Git 仓库并推送到 Gitea
- [x] 创建每日自动备份 cron(每天 2:00)
- [x] 创建 TODO 管理技能
## TODO 管理技能
### 知识库结构
```
~/work/zq/
├── 00.收件箱/ # 临时捕获
├── 01.待办/ # 今日待办 (TODO.md)
├── 10.项目/ # 有明确完成定义
├── 20.领域/ # 长期持续
├── 30.资源/ # 参考资料
├── 40.归档/ # 已完成
└── 50.模板/
```
### TODO 分类规则
1. 今天就要做 → `01.待办/TODO.md`
2. 属于某个项目 → `10.项目/项目名/00-README.md`
3. 长期持续要做 → `20.领域/领域名/00-README.md`
4. 不确定 → `00.收件箱/README.md`
### 同步机制
- 每天凌晨 2:00 自动执行
- 同步脚本:`~/work/zq-sync.sh`
- 手动同步:`~/work/zq-add-todo.sh`
@@ -0,0 +1,578 @@
# 个人知识管理体系对比指南
> 适用于开发者、知识工作者的 PKM 方法论详解
---
## 目录
1. [方法概述](#方法概述)
2. [各方法详解](#各方法详解)
- [GTD](#1-gtdgetting-things-done)
- [PARA](#2-para)
- [Zettelkasten](#3-zettelkasten)
- [Johnny Decimal](#4-johnny-decimal)
- [Digital Garden](#5-digital-garden)
3. [方法对比表](#方法对比表)
4. [推荐组合方案](#推荐组合方案)
5. [针对开发者的建议](#针对开发者的建议)
6. [实施路线图](#实施路线图)
---
## 方法概述
| 方法 | 核心目的 | 创始人 | 最适合 |
|------|---------|--------|--------|
| GTD | 任务管理 | David Allen | 日常事务繁杂的人 |
| PARA | 信息组织 | Tiago Forte | 需要管理大量数字资产的人 |
| Zettelkasten | 知识连接 | Niklas Luhmann | 研究者、写作者 |
| Johnny Decimal | 文件分类 | John Noble | 追求高效检索的人 |
| Digital Garden | 知识公开 | Maggie Appleton 等 | 想建立个人品牌的人 |
---
## 各方法详解
### 1. GTD(Getting Things Done)
#### 核心理念
"大脑是用来思考的,不是用来记事的。"——把所有事务从大脑中清空,放入可信赖的系统。
#### 五个步骤
```
1. Capture(收集)
↓ 所有输入进入收件箱
2. Clarify(理清)
↓ 判断每件事的性质
3. Organize(整理)
↓ 放到正确的地方
4. Reflect(回顾)
↓ 定期检查系统
5. Engage(执行)
↓ 选择该做的事
```
#### 处理流程图
```
收到一个事项
│
▼
可以行动吗?
│
├─ 否 → 是参考资料吗?→ 是 → 存档
│ │
│ └─ 否 → 是垃圾吗?→ 是 → 删除
│ │
│ └─ 否 → 暂时保留( someday/maybe)
│
└─ 是 → 2分钟内能完成吗?
│
├─ 是 → 立即执行
│
└─ 否 → 需要多步完成吗?
│
├─ 是 → 创建项目
│
└─ 否 → 委托别人?→ 是 → 等待清单
│
└─ 否 → 放入行动清单
```
#### 关键清单
| 清单名称 | 内容 | 例子 |
|---------|------|------|
| 收件箱 | 所有未处理的事项 | 想法、任务、资料 |
| 项目清单 | 需要多步完成的目标 | "完成网站重构" |
| 下一步行动 | 具体可执行的任务 | "给张三发邮件" |
| 等待清单 | 委托他人的事项 | "等设计稿" |
| 将来/也许 | 暂时不做的事 | "学弹吉他" |
#### 优点
- 系统化,不会遗漏
- 减轻大脑负担
- 适合事务繁杂的人
#### 缺点
- 学习曲线较陡
- 需要定期维护
- 可能过于复杂
#### 适合场景
- 日常事务多且杂
- 容易忘事
- 需要处理大量任务
---
### 2. PARA
#### 核心理念
按"可操作性"而非"类型"来组织信息。
#### 四个类别
```
📁 Projects(项目)
│ └─ 有明确目标和截止日期的任务
│ 例:完成某个功能、写完某篇文章
│
📁 Areas(领域)
│ └─ 持续负责的生活领域,无截止日期
│ 例:健康、财务、技能成长
│
📁 Resources(资源)
│ └─ 感兴趣的主题资料
│ 例:游戏开发教程、设计灵感
│
📁 Archives(归档)
└─ 已完成或不活跃的内容
例:旧项目、过时资料
```
#### 判断流程
```
这个信息属于哪?
│
├─ 有明确截止日期?→ Projects
│
├─ 是我持续负责的领域?→ Areas
│
├─ 是我感兴趣的资料?→ Resources
│
└─ 不再活跃?→ Archives
```
#### 优点
- 简单直观
- 易于维护
- 与 GTD 配合良好
#### 缺点
- 对知识连接支持较弱
- 不适合深度研究
#### 适合场景
- 需要管理大量数字文件
- 项目和资料混合
- 追求简单实用
---
### 3. Zettelkasten(卡片盒笔记法)
#### 核心理念
"笔记不是孤立存在的,而是通过连接形成知识网络。"
#### 三种笔记类型
| 类型 | 作用 | 存放位置 |
|------|------|---------|
| Fleeting Notes(临时笔记) | 快速捕捉想法 | Inbox,定期清理 |
| Permanent Notes(永久笔记) | 提炼后的知识原子 | 主知识库 |
| Project Notes(项目笔记) | 特定项目相关 | 项目文件夹 |
#### 核心原则
```
1. 原子化:每条笔记只包含一个想法
2. 自我解释:用自己的话重写
3. 建立连接:笔记之间相互链接
4. 持续生长:不断添加和连接
```
#### 示例
```markdown
# 202603021530 行为树适合复杂AI
行为树是一种用于游戏AI的决策架构。
## 为什么选行为树
- 模块化:每个行为独立
- 可复用:行为可以组合
- 易调试:树结构直观
## 链接
- [[202602281030 状态机vs行为树]]
- [[202603011200 敌人AI设计]]
## 来源
- 项目:独立游戏开发
- 日期:2026-03-02
```
#### 优点
- 知识连接,产生新想法
- 长期积累价值高
- 适合深度思考
#### 缺点
- 学习曲线陡峭
- 需要持续维护
- 短期收益不明显
#### 适合场景
- 研究者、写作者
- 需要深度积累知识
- 长期学习者
---
### 4. Johnny Decimal(数字分类法)
#### 核心理念
用数字编号系统让文件一秒找到。
#### 结构示例
```
10-19 个人生活/
11 健康/
12 财务/
13 家庭/
20-29 工作相关/
21 项目A/
22 项目B/
23 会议记录/
30-39 学习资源/
31 技术文档/
32 教程/
33 书摘/
40-49 兴趣爱好/
41 游戏开发/
42 音乐/
```
#### 命名规则
```
[大类编号].[子类编号] [名称]
例:
21.01 项目A需求文档
21.02 项目A设计稿
31.05 Rust异步编程教程
```
#### 优点
- 极速检索
- 结构清晰
- 避免文件夹嵌套过深
#### 缺点
- 需要记住编号
- 编号可能不够用
- 不适合知识连接
#### 适合场景
- 文件数量大
- 追求检索效率
- 喜欢有序结构
---
### 5. Digital Garden(数字花园)
#### 核心理念
"知识像花园一样,需要持续种植、修剪、收获。"
#### 特点
```
🌱 种子笔记:刚萌发的想法
🌿 生长中:正在完善的笔记
🌳 成熟笔记:完整、可分享的内容
🍂 枯萎笔记:过时或不再相关
```
#### 与传统博客的区别
| 传统博客 | 数字花园 |
|---------|---------|
| 完成后发布 | 随时发布,持续更新 |
| 线性时间排列 | 按主题连接 |
| 追求完美 | 允许不完美 |
| 单向输出 | 双向交流 |
#### 优点
- 公开分享,建立影响力
- 获得反馈,持续改进
- 个人品牌建设
#### 缺点
- 需要公开,隐私性低
- 维护成本高
- 需要写作能力
#### 适合场景
- 想建立个人品牌
- 愿意公开分享
- 追求社区连接
---
## 方法对比表
| 维度 | GTD | PARA | Zettelkasten | Johnny Decimal | Digital Garden |
|------|-----|------|--------------|----------------|----------------|
| **主要目的** | 任务管理 | 信息组织 | 知识连接 | 文件分类 | 知识公开 |
| **学习难度** | 中 | 低 | 高 | 低 | 中 |
| **维护成本** | 高 | 低 | 高 | 低 | 高 |
| **短期收益** | 高 | 高 | 低 | 高 | 低 |
| **长期价值** | 中 | 中 | 高 | 中 | 高 |
| **知识连接** | ❌ | ❌ | ✅ | ❌ | ✅ |
| **任务管理** | ✅ | ❌ | ❌ | ❌ | ❌ |
| **公开分享** | ❌ | ❌ | ❌ | ❌ | ✅ |
| **适合工具** | Todoist | 任意 | Obsidian | 任意 | 任意+网站 |
---
## 推荐组合方案
### 组合一:日常效率型(GTD + PARA)
**适合**:事务繁杂,追求效率的开发者
```
GTD 处理任务流动
+
PARA 组织信息存储
流程:
1. 所有输入 → GTD 收件箱
2. 任务 → GTD 项目/行动清单
3. 资料 → PARA Resources
4. 项目文件 → PARA Projects
```
**目录结构**:
```
📁 知识库/
├── 00-Inbox/ # GTD 收集箱
├── 10-Projects/ # PARA + GTD 项目
├── 20-Areas/ # PARA 领域
├── 30-Resources/ # PARA 资源
├── 40-Archives/ # PARA 归档
└── 50-Templates/
```
---
### 组合二:深度学习型(PARA + Zettelkasten)
**适合**:需要深度积累知识的开发者
```
PARA 组织项目和大类
+
Zettelkasten 深度知识笔记
流程:
1. 项目文件 → PARA Projects
2. 原子化知识笔记 → Zettelkasten
3. 项目笔记提炼 → Zettelkasten
4. Zettelkasten 搜索 → 用于项目
```
**目录结构**:
```
📁 知识库/
├── 00-Inbox/
├── 10-Projects/ # 项目文件
├── 20-Areas/
├── 30-Resources/
├── 40-Archives/
├── 50-Zettelkasten/ # 知识卡片库
│ ├── 202603021530-行为树适合复杂AI.md
│ ├── 202603021545-状态机vs行为树.md
│ └── ...
└── 60-Templates/
```
---
### 组合三:极简高效型(PARA + Johnny Decimal)
**适合**:追求简单、高效的开发者
```
PARA 四大类
+
Johnny Decimal 数字编号
流程:
1. 四大类保持 PARA
2. 每类下用 Johnny Decimal 编号
3. 快速检索
```
**目录结构**:
```
📁 知识库/
├── 10-19 个人/
│ ├── 11-健康/
│ ├── 12-财务/
│ └── 13-生活/
├── 20-29 项目/
│ ├── 21-OpenClaw优化/
│ ├── 22-独立游戏/
│ └── 23-短期事务/
├── 30-39 资源/
│ ├── 31-游戏开发/
│ ├── 32-AI工具/
│ └── 33-设计灵感/
├── 40-49 归档/
└── 50-59 模板/
```
---
### 组合四:公开分享型(PARA + Zettelkasten + Digital Garden)
**适合**:想建立个人品牌的开发者
```
PARA + Zettelkasten 内部管理
+
Digital Garden 公开分享
流程:
1. 内部用 PARA + Zettelkasten
2. 成熟的笔记发布到 Digital Garden
3. 获得反馈,持续改进
```
---
## 针对开发者的建议
### 根据你的情况选择
| 你的需求 | 推荐方案 |
|---------|---------|
| 日常事务多,容易忘事 | GTD + PARA |
| 想深度积累技术知识 | PARA + Zettelkasten |
| 追求简单高效 | PARA + Johnny Decimal |
| 想建立技术影响力 | PARA + Zettelkasten + Digital Garden |
| 刚开始接触 PKM | 先用 PARA,逐步添加 |
### 开发者特有的考虑
1. **代码笔记**
- 代码片段 → Resources/代码库
- 踩坑记录 → Project/开发笔记
- 最佳实践 → Zettelkasten
2. **工具选择**
- Obsidian:支持 Zettelkasten,本地存储
- Notion:功能全面,协作友好
- Logseq:大纲式,适合 Zettelkasten
3. **与开发工具集成**
- Git 仓库的 README 链接到知识库
- 代码注释引用知识库笔记
- IDE 插件快速记录
---
## 实施路线图
### 阶段一:建立基础(第1周)
```
□ 选择一个笔记工具
□ 创建 PARA 基础目录
□ 创建 Inbox 文件
□ 把现有资料分类放入
```
### 阶段二:养成习惯(第2-4周)
```
□ 每天收集到 Inbox
□ 每天处理 Inbox(10分钟)
□ 每周回顾(30分钟)
□ 调整分类,找到适合自己的方式
```
### 阶段三:深化优化(第2-3月)
```
□ 尝试加入 GTD 任务管理
□ 或尝试 Zettelkasten 知识连接
□ 或尝试 Johnny Decimal 编号
□ 找到最适合自己的组合
```
### 阶段四:长期维护(持续)
```
□ 每月检视系统
□ 归档不活跃内容
□ 持续优化流程
□ 享受知识积累的复利
```
---
## 常见问题
### Q1: 这么多方法,会不会太复杂?
**A**: 从最简单的开始。推荐先只用 PARA,习惯后再逐步添加其他方法。
### Q2: 用什么工具最好?
**A**: 工具不重要,习惯才重要。推荐:
- 新手:Notion(上手快)
- 进阶:Obsidian(灵活强大)
- 极简:纯 Markdown 文件 + VSCode
### Q3: 如何避免"收集了但从来不看"?
**A**:
1. 定期回顾(每周至少一次)
2. 只收集真正有用的
3. 快速处理 Inbox,不要堆积
### Q4: 知识库和项目代码仓库怎么配合?
**A**:
- 代码放 Git 仓库
- 设计文档、笔记放知识库
- 在代码 README 中链接到知识库
### Q5: 如何处理工作和个人知识库?
**A**:
- 敏感工作内容 → 公司知识库
- 通用技能、个人项目 → 个人知识库
- 可复用的知识 → 提炼后放个人库
---
## 总结
| 如果你是... | 推荐方案 | 理由 |
|------------|---------|------|
| 事务繁杂 | GTD + PARA | 系统化管理任务和信息 |
| 深度学习者 | PARA + Zettelkasten | 知识连接产生新想法 |
| 追求效率 | PARA + Johnny Decimal | 简单高效,快速检索 |
| 想建立影响力 | + Digital Garden | 公开分享,获得反馈 |
| 新手 | 先用 PARA | 最简单,收益明显 |
**最重要的**:不要追求完美,先开始,再优化。
---
*文档版本:1.0*
*创建日期:2026-03-02*
*适用人群:开发者、知识工作者*
+18 -20
View File
@@ -1,7 +1,7 @@
{
"agent:thinker:main": {
"sessionId": "e0159b30-ee31-4c97-a081-274c2367ed1c",
"updatedAt": 1772178209434,
"sessionId": "b3e88073-2c5f-4e4f-b922-5678042fc955",
"updatedAt": 1772387365339,
"systemSent": true,
"abortedLastRun": false,
"chatType": "direct",
@@ -22,7 +22,7 @@
"to": "user:ou_a0f6627b64d2dc8b0236671603d1e47b",
"accountId": "feishu-qa"
},
"sessionFile": "/home/starmen1265/.openclaw/agents/thinker/sessions/e0159b30-ee31-4c97-a081-274c2367ed1c.jsonl",
"sessionFile": "/home/starmen1265/.openclaw/agents/thinker/sessions/b3e88073-2c5f-4e4f-b922-5678042fc955.jsonl",
"compactionCount": 0,
"skillsSnapshot": {
"prompt": "",
@@ -30,19 +30,15 @@
"resolvedSkills": [],
"version": 0
},
"inputTokens": 53827,
"outputTokens": 478,
"totalTokens": 118937,
"modelProvider": "mm",
"model": "MiniMax-M2.5",
"contextTokens": 200000,
"modelProvider": "zhipu",
"model": "glm-5",
"systemPromptReport": {
"source": "run",
"generatedAt": 1772178186521,
"sessionId": "e0159b30-ee31-4c97-a081-274c2367ed1c",
"generatedAt": 1772387320865,
"sessionId": "b3e88073-2c5f-4e4f-b922-5678042fc955",
"sessionKey": "agent:thinker:main",
"provider": "mm",
"model": "MiniMax-M2.5",
"provider": "zhipu",
"model": "glm-5",
"workspaceDir": "/home/starmen1265/.openclaw/agents/thinker/workspace",
"bootstrapMaxChars": 20000,
"sandbox": {
@@ -50,9 +46,9 @@
"sandboxed": false
},
"systemPrompt": {
"chars": 19286,
"projectContextChars": 10530,
"nonProjectContextChars": 8756
"chars": 19315,
"projectContextChars": 10538,
"nonProjectContextChars": 8777
},
"injectedWorkspaceFiles": [
{
@@ -67,8 +63,8 @@
"name": "SOUL.md",
"path": "/home/starmen1265/.openclaw/agents/thinker/workspace/SOUL.md",
"missing": false,
"rawChars": 190,
"injectedChars": 190,
"rawChars": 198,
"injectedChars": 198,
"truncated": false
},
{
@@ -279,7 +275,9 @@
]
}
},
"memoryFlushAt": 1772177937416,
"memoryFlushCompactionCount": 0
"inputTokens": 1345,
"outputTokens": 1135,
"totalTokens": 20993,
"contextTokens": 200000
}
}
+5 -7
View File
@@ -1,16 +1,14 @@
# thinker - 思考者 Agent
# thinker - 审查员 Agent
负责代码审查、技术方案评估、问题分析等思考类任务。
你是审查员,审查代码,审查流程,审查思路,审查一切。找出可能的问题,漏洞。
## 技能
- common-require
## 系统提示
你是一个专业的技术思考者,负责:
你是一个专业的审查员,负责:
1. 代码审查 - 审查代码质量、潜在问题、优化建议
2. 技术方案评估 - 评估技术选型、架构设计
3. 问题分析 - 分析问题根因、提供解决方案
2. 流程审查 - 审查工作流程、找出流程漏洞
3. 思路审查 - 审查思维逻辑、找出逻辑漏洞
回答要专业、严谨、有逻辑。
+22
View File
@@ -84,6 +84,28 @@
"maxTokens": 8192
}
]
},
"bailian": {
"baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
"api": "openai-completions",
"models": [
{
"id": "qwen3-max-2026-01-23",
"name": "qwen3-max-thinking",
"reasoning": false,
"input": [
"text"
],
"cost": {
"input": 0,
"output": 0,
"cacheRead": 0,
"cacheWrite": 0
},
"contextWindow": 262144,
"maxTokens": 65536
}
]
}
}
}
+36 -187
View File
@@ -1,135 +1,41 @@
{
"agent:ui-designer:main": {
"sessionId": "37a6e7ab-a957-4359-9a0f-7a4c135aca02",
"updatedAt": 1771028350756,
"sessionId": "ce2ef5df-3713-424e-92c2-47f51d1bc43a",
"updatedAt": 1772263743321,
"systemSent": true,
"abortedLastRun": false,
"chatType": "direct",
"deliveryContext": {
"channel": "feishu",
"to": "user:ou_6de30318970345c4c79bea292b93130c",
"accountId": "default"
"to": "user:ou_9847ce84b960736c14554730b41e4957",
"accountId": "feishu-art"
},
"lastChannel": "feishu",
"lastTo": "user:ou_6de30318970345c4c79bea292b93130c",
"lastAccountId": "default",
"lastTo": "user:ou_9847ce84b960736c14554730b41e4957",
"lastAccountId": "feishu-art",
"origin": {
"label": "刘鑫",
"label": "张青",
"provider": "feishu",
"surface": "feishu",
"chatType": "direct",
"from": "feishu:ou_6de30318970345c4c79bea292b93130c",
"to": "user:ou_6de30318970345c4c79bea292b93130c",
"accountId": "default"
"from": "feishu:ou_9847ce84b960736c14554730b41e4957",
"to": "user:ou_9847ce84b960736c14554730b41e4957",
"accountId": "feishu-art"
},
"sessionFile": "/home/starmen1265/.openclaw/agents/ui-designer/sessions/37a6e7ab-a957-4359-9a0f-7a4c135aca02.jsonl",
"sessionFile": "/home/starmen1265/.openclaw/agents/ui-designer/sessions/ce2ef5df-3713-424e-92c2-47f51d1bc43a.jsonl",
"compactionCount": 0,
"skillsSnapshot": {
"prompt": "\n\nThe following skills provide specialized instructions for specific tasks.\nUse the read tool to load a skill's file when the task matches its description.\nWhen a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\n\n<available_skills>\n <skill>\n <name>feishu-doc</name>\n <description>Feishu document read/write operations. Activate when user mentions Feishu docs, cloud docs, or docx links.\n</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-doc/SKILL.md</location>\n </skill>\n <skill>\n <name>feishu-drive</name>\n <description>Feishu cloud storage file management. Activate when user mentions cloud space, folders, drive.\n</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-drive/SKILL.md</location>\n </skill>\n <skill>\n <name>feishu-perm</name>\n <description>Feishu permission management for documents and files. Activate when user mentions sharing, permissions, collaborators.\n</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-perm/SKILL.md</location>\n </skill>\n <skill>\n <name>feishu-wiki</name>\n <description>Feishu knowledge base navigation. Activate when user mentions knowledge base, wiki, or wiki links.\n</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-wiki/SKILL.md</location>\n </skill>\n <skill>\n <name>healthcheck</name>\n <description>Host security hardening and risk-tolerance configuration for OpenClaw deployments. Use when a user asks for security audits, firewall/SSH/update hardening, risk posture, exposure review, OpenClaw cron scheduling for periodic checks, or version status checks on a machine running OpenClaw (laptop, workstation, Pi, VPS).</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/healthcheck/SKILL.md</location>\n </skill>\n <skill>\n <name>skill-creator</name>\n <description>Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/skill-creator/SKILL.md</location>\n </skill>\n <skill>\n <name>video-frames</name>\n <description>Extract frames or short clips from videos using ffmpeg.</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/video-frames/SKILL.md</location>\n </skill>\n <skill>\n <name>weather</name>\n <description>Get current weather and forecasts (no API key required).</description>\n <location>/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/weather/SKILL.md</location>\n </skill>\n</available_skills>",
"skills": [
{
"name": "feishu-doc"
},
{
"name": "feishu-drive"
},
{
"name": "feishu-perm"
},
{
"name": "feishu-wiki"
},
{
"name": "healthcheck"
},
{
"name": "skill-creator"
},
{
"name": "video-frames"
},
{
"name": "weather"
}
],
"resolvedSkills": [
{
"name": "feishu-doc",
"description": "Feishu document read/write operations. Activate when user mentions Feishu docs, cloud docs, or docx links.\n",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-doc/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-doc",
"source": "openclaw-extra",
"disableModelInvocation": false
},
{
"name": "feishu-drive",
"description": "Feishu cloud storage file management. Activate when user mentions cloud space, folders, drive.\n",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-drive/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-drive",
"source": "openclaw-extra",
"disableModelInvocation": false
},
{
"name": "feishu-perm",
"description": "Feishu permission management for documents and files. Activate when user mentions sharing, permissions, collaborators.\n",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-perm/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-perm",
"source": "openclaw-extra",
"disableModelInvocation": false
},
{
"name": "feishu-wiki",
"description": "Feishu knowledge base navigation. Activate when user mentions knowledge base, wiki, or wiki links.\n",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-wiki/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/extensions/feishu/skills/feishu-wiki",
"source": "openclaw-extra",
"disableModelInvocation": false
},
{
"name": "healthcheck",
"description": "Host security hardening and risk-tolerance configuration for OpenClaw deployments. Use when a user asks for security audits, firewall/SSH/update hardening, risk posture, exposure review, OpenClaw cron scheduling for periodic checks, or version status checks on a machine running OpenClaw (laptop, workstation, Pi, VPS).",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/healthcheck/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/healthcheck",
"source": "openclaw-bundled",
"disableModelInvocation": false
},
{
"name": "skill-creator",
"description": "Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/skill-creator/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/skill-creator",
"source": "openclaw-bundled",
"disableModelInvocation": false
},
{
"name": "video-frames",
"description": "Extract frames or short clips from videos using ffmpeg.",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/video-frames/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/video-frames",
"source": "openclaw-bundled",
"disableModelInvocation": false
},
{
"name": "weather",
"description": "Get current weather and forecasts (no API key required).",
"filePath": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/weather/SKILL.md",
"baseDir": "/home/starmen1265/.npm-global/lib/node_modules/openclaw/skills/weather",
"source": "openclaw-bundled",
"disableModelInvocation": false
}
],
"prompt": "",
"skills": [],
"resolvedSkills": [],
"version": 0
},
"inputTokens": 672,
"outputTokens": 387,
"totalTokens": 13344,
"modelProvider": "zhipu",
"model": "glm-5",
"contextTokens": 200000,
"systemPromptReport": {
"source": "run",
"generatedAt": 1771028335912,
"sessionId": "37a6e7ab-a957-4359-9a0f-7a4c135aca02",
"generatedAt": 1772263736026,
"sessionId": "ce2ef5df-3713-424e-92c2-47f51d1bc43a",
"sessionKey": "agent:ui-designer:main",
"provider": "zhipu",
"model": "glm-5",
@@ -140,9 +46,9 @@
"sandboxed": false
},
"systemPrompt": {
"chars": 16218,
"projectContextChars": 3808,
"nonProjectContextChars": 12410
"chars": 12860,
"projectContextChars": 4075,
"nonProjectContextChars": 8785
},
"injectedWorkspaceFiles": [
{
@@ -200,48 +106,23 @@
"rawChars": 0,
"injectedChars": 92,
"truncated": false
},
{
"name": "MEMORY.md",
"path": "/home/starmen1265/.openclaw/agents/ui-designer/workspace/MEMORY.md",
"missing": false,
"rawChars": 253,
"injectedChars": 253,
"truncated": false
}
],
"skills": {
"promptChars": 2934,
"entries": [
{
"name": "feishu-doc",
"blockChars": 311
},
{
"name": "feishu-drive",
"blockChars": 303
},
{
"name": "feishu-perm",
"blockChars": 325
},
{
"name": "feishu-wiki",
"blockChars": 305
},
{
"name": "healthcheck",
"blockChars": 507
},
{
"name": "skill-creator",
"blockChars": 312
},
{
"name": "video-frames",
"blockChars": 245
},
{
"name": "weather",
"blockChars": 236
}
]
"promptChars": 0,
"entries": []
},
"tools": {
"listChars": 2959,
"schemaChars": 18779,
"listChars": 2378,
"schemaChars": 16629,
"entries": [
{
"name": "read",
@@ -387,42 +268,6 @@
"schemaChars": 458,
"propertiesCount": 5
},
{
"name": "feishu_bitable_get_meta",
"summaryChars": 114,
"schemaChars": 170,
"propertiesCount": 1
},
{
"name": "feishu_bitable_list_fields",
"summaryChars": 76,
"schemaChars": 255,
"propertiesCount": 2
},
{
"name": "feishu_bitable_list_records",
"summaryChars": 64,
"schemaChars": 436,
"propertiesCount": 4
},
{
"name": "feishu_bitable_get_record",
"summaryChars": 46,
"schemaChars": 335,
"propertiesCount": 3
},
{
"name": "feishu_bitable_create_record",
"summaryChars": 44,
"schemaChars": 515,
"propertiesCount": 3
},
{
"name": "feishu_bitable_update_record",
"summaryChars": 50,
"schemaChars": 439,
"propertiesCount": 4
},
{
"name": "memory_search",
"summaryChars": 235,
@@ -437,6 +282,10 @@
}
]
}
}
},
"inputTokens": 2394,
"outputTokens": 89,
"totalTokens": 10906,
"contextTokens": 200000
}
}
+61 -1
View File
@@ -1,4 +1,64 @@
{
"version": 1,
"jobs": []
"jobs": [
{
"id": "41396aeb-6e3a-47b2-9bd3-9156a348c1dc",
"agentId": "haonan",
"name": "backup-openclaw-json",
"enabled": true,
"createdAtMs": 1772261581796,
"updatedAtMs": 1772478022557,
"schedule": {
"kind": "cron",
"expr": "0 3 * * *",
"tz": "Asia/Shanghai"
},
"sessionTarget": "isolated",
"wakeMode": "now",
"payload": {
"kind": "agentTurn",
"message": "执行备份脚本:/home/starmen1265/work/backup/backup_openclaw.sh"
},
"delivery": {
"mode": "announce"
},
"state": {
"nextRunAtMs": 1772564400000,
"lastRunAtMs": 1772478000028,
"lastStatus": "ok",
"lastDurationMs": 22529,
"consecutiveErrors": 0
}
},
{
"id": "f03b184f-5f1d-4093-ae1b-fc9fe0b59eef",
"agentId": "haonan",
"name": "capability-evolver-daily",
"enabled": true,
"createdAtMs": 1772550743528,
"updatedAtMs": 1772550770239,
"schedule": {
"kind": "cron",
"expr": "0 12 * * *",
"tz": "Asia/Shanghai"
},
"sessionTarget": "isolated",
"wakeMode": "now",
"payload": {
"kind": "agentTurn",
"message": "执行每日 capability-evolver 任务:\n\n1. 执行脚本:bash ~/.openclaw/scripts/evolver-run.sh\n2. 读取输出:cat ~/.openclaw/scripts/evolver-output.txt\n3. 将完整的修改内容(包含 Git 备份信息和本次变更)通过 feishu 发送给蒋先生\n\n如果本次无任何变更,回复 '今日无变更' 即可。",
"model": "g47f",
"thinking": "off",
"timeoutSeconds": 300
},
"delivery": {
"mode": "announce",
"channel": "feishu",
"to": "feishu-pm"
},
"state": {
"nextRunAtMs": 1772596800000
}
}
]
}
+61 -1
View File
@@ -1,4 +1,64 @@
{
"version": 1,
"jobs": []
"jobs": [
{
"id": "41396aeb-6e3a-47b2-9bd3-9156a348c1dc",
"agentId": "haonan",
"name": "backup-openclaw-json",
"enabled": true,
"createdAtMs": 1772261581796,
"updatedAtMs": 1772478022557,
"schedule": {
"kind": "cron",
"expr": "0 3 * * *",
"tz": "Asia/Shanghai"
},
"sessionTarget": "isolated",
"wakeMode": "now",
"payload": {
"kind": "agentTurn",
"message": "执行备份脚本:/home/starmen1265/work/backup/backup_openclaw.sh"
},
"delivery": {
"mode": "announce"
},
"state": {
"nextRunAtMs": 1772564400000,
"lastRunAtMs": 1772478000028,
"lastStatus": "ok",
"lastDurationMs": 22529,
"consecutiveErrors": 0
}
},
{
"id": "f03b184f-5f1d-4093-ae1b-fc9fe0b59eef",
"agentId": "haonan",
"name": "capability-evolver-daily",
"enabled": true,
"createdAtMs": 1772550743528,
"updatedAtMs": 1772550770239,
"schedule": {
"kind": "cron",
"expr": "0 12 * * *",
"tz": "Asia/Shanghai"
},
"sessionTarget": "isolated",
"wakeMode": "now",
"payload": {
"kind": "agentTurn",
"message": "执行每日 capability-evolver 任务:\n\n1. 执行脚本:bash ~/.openclaw/scripts/evolver-run.sh\n2. 读取输出:cat ~/.openclaw/scripts/evolver-output.txt\n3. 将完整的修改内容(包含 Git 备份信息和本次变更)通过 feishu 发送给蒋先生\n\n如果本次无任何变更,回复 '今日无变更' 即可。",
"model": "g47f",
"thinking": "off",
"timeoutSeconds": 300
},
"delivery": {
"mode": "announce",
"channel": "feishu",
"to": "feishu-pm"
},
"state": {
"nextRunAtMs": 1772596800000
}
}
]
}
+5 -4
View File
@@ -3,8 +3,8 @@
"deviceId": "a12f851ec2a179ca97d45b204bc3788476f062520fa77c5edef5cd054a176b1e",
"publicKey": "Ka6zP7u6MzDaQBqOpXHYUBC5MYLlVGGZM8vIi11WnTw",
"platform": "linux",
"clientId": "cli",
"clientMode": "cli",
"clientId": "gateway-client",
"clientMode": "backend",
"role": "operator",
"roles": [
"operator"
@@ -24,11 +24,12 @@
"operator.pairing"
],
"createdAtMs": 1770807648254,
"lastUsedAtMs": 1772258378066
"lastUsedAtMs": 1772550770205
}
},
"createdAtMs": 1770807648254,
"approvedAtMs": 1770807648254,
"remoteIp": "192.168.1.36"
"remoteIp": "192.168.1.36",
"displayName": "agent"
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
"operator.approvals",
"operator.pairing"
],
"updatedAtMs": 1772258378067
"updatedAtMs": 1772550770236
}
}
}
+1
View File
@@ -12,3 +12,4 @@
{"timestamp":"2026-02-15T17:04:24.102Z","action":"reset","sessionKey":"agent:haonan:wecom:dm:zhangqing","senderId":"zhangqing","source":"wecom"}
{"timestamp":"2026-02-27T10:25:23.406Z","action":"new","sessionKey":"agent:knowledge-base:main","senderId":"ou_9351c287f42069fa30366ac641d24df7","source":"feishu"}
{"timestamp":"2026-02-27T11:53:11.832Z","action":"new","sessionKey":"agent:knowledge-base:main","senderId":"ou_9351c287f42069fa30366ac641d24df7","source":"feishu"}
{"timestamp":"2026-03-03T06:49:29.338Z","action":"new","sessionKey":"agent:knowledge-base:main","senderId":"ou_9351c287f42069fa30366ac641d24df7","source":"feishu"}
@@ -0,0 +1,76 @@
2026-03-01T15:58:22.8933862Z local-runner(version:v0.3.0) received task 62 of job 70, be triggered by event: push
2026-03-01T15:58:22.8939089Z workflow prepared
2026-03-01T15:58:22.8939996Z evaluating expression 'success()'
2026-03-01T15:58:22.8940784Z expression 'success()' evaluated to 'true'
2026-03-01T15:58:22.8940924Z 🚀 Start image=gitea-runner-php:latest
2026-03-01T15:58:22.9020914Z 🐳 docker pull image=gitea-runner-php:latest platform= username= forcePull=false
2026-03-01T15:58:22.9021178Z 🐳 docker pull gitea-runner-php:latest
2026-03-01T15:58:22.9187045Z Image exists? true
2026-03-01T15:58:22.9280509Z Cleaning up network for job 快速测试, and network name is: GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network
2026-03-01T15:58:22.9605210Z 🐳 docker create image=gitea-runner-php:latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network"
2026-03-01T15:58:22.9726112Z Custom container.Config from options ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image: Volumes:map[] WorkingDir: Entrypoint:[] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-01T15:58:22.9726602Z Merged container.Config ==> &{Hostname: Domainname: User: AttachStdin:false AttachStdout:true AttachStderr:true ExposedPorts:map[] Tty:false OpenStdin:false StdinOnce:false Env:[RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8] Cmd:[] Healthcheck:<nil> ArgsEscaped:false Image:gitea-runner-php:latest Volumes:map[] WorkingDir:/workspace/starmen1265/kb-web Entrypoint:[/bin/sleep 10800] NetworkDisabled:false MacAddress: OnBuild:[] Labels:map[] StopSignal: StopTimeout:<nil> Shell:[]}
2026-03-01T15:58:22.9727112Z Custom container.HostConfig from options ==> &{Binds:[] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:false VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x27d418c7c3c8 OomKillDisable:0x27d418c7c2c3 PidsLimit:0x27d418c7c428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-01T15:58:22.9727713Z --network and --net in the options will be ignored.
2026-03-01T15:58:22.9728082Z Merged container.HostConfig ==> &{Binds:[/var/run/docker.sock:/var/run/docker.sock] ContainerIDFile: LogConfig:{Type: Config:map[]} NetworkMode:GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network PortBindings:map[] RestartPolicy:{Name:no MaximumRetryCount:0} AutoRemove:true VolumeDriver: VolumesFrom:[] ConsoleSize:[0 0] Annotations:map[] CapAdd:[] CapDrop:[] CgroupnsMode: DNS:[] DNSOptions:[] DNSSearch:[] ExtraHosts:[] GroupAdd:[] IpcMode: Cgroup: Links:[] OomScoreAdj:0 PidMode: Privileged:false PublishAllPorts:false ReadonlyRootfs:false SecurityOpt:[] StorageOpt:map[] Tmpfs:map[] UTSMode: UsernsMode: ShmSize:0 Sysctls:map[] Runtime: Isolation: Resources:{CPUShares:0 Memory:0 NanoCPUs:0 CgroupParent: BlkioWeight:0 BlkioWeightDevice:[] BlkioDeviceReadBps:[] BlkioDeviceWriteBps:[] BlkioDeviceReadIOps:[] BlkioDeviceWriteIOps:[] CPUPeriod:0 CPUQuota:0 CPURealtimePeriod:0 CPURealtimeRuntime:0 CpusetCpus: CpusetMems: Devices:[] DeviceCgroupRules:[] DeviceRequests:[] KernelMemory:0 KernelMemoryTCP:0 MemoryReservation:0 MemorySwap:0 MemorySwappiness:0x27d418c7c3c8 OomKillDisable:0x27d418c7c2c3 PidsLimit:0x27d418c7c428 Ulimits:[] CPUCount:0 CPUPercent:0 IOMaximumIOps:0 IOMaximumBandwidth:0} Mounts:[{Type:volume Source:GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB Target:/workspace/starmen1265/kb-web ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:act-toolcache Target:/opt/hostedtoolcache ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>} {Type:volume Source:GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-env Target:/var/run/act ReadOnly:false Consistency: BindOptions:<nil> VolumeOptions:<nil> TmpfsOptions:<nil> ClusterOptions:<nil>}] MaskedPaths:[] ReadonlyPaths:[] Init:<nil>}
2026-03-01T15:58:23.0343034Z Created container name=GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB id=3bf638d4a888a88d0a68648e6dc93c83e717af46e024b49bd12104695dbdfdc9 from image gitea-runner-php:latest (platform: )
2026-03-01T15:58:23.0343488Z ENV ==> [RUNNER_TOOL_CACHE=/opt/hostedtoolcache RUNNER_OS=Linux RUNNER_ARCH=X64 RUNNER_TEMP=/tmp LANG=C.UTF-8]
2026-03-01T15:58:23.0343631Z 🐳 docker run image=gitea-runner-php:latest platform= entrypoint=["/bin/sleep" "10800"] cmd=[] network="GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network"
2026-03-01T15:58:23.0343837Z Starting container: 3bf638d4a888a88d0a68648e6dc93c83e717af46e024b49bd12104695dbdfdc9
2026-03-01T15:58:23.1516544Z Started container: 3bf638d4a888a88d0a68648e6dc93c83e717af46e024b49bd12104695dbdfdc9
2026-03-01T15:58:23.2220121Z Writing entry to tarball workflow/event.json len:4759
2026-03-01T15:58:23.2220535Z Writing entry to tarball workflow/envs.txt len:0
2026-03-01T15:58:23.2220662Z Extracting content to '/var/run/act/'
2026-03-01T15:58:23.2429615Z evaluating expression ''
2026-03-01T15:58:23.2430104Z expression '' evaluated to 'true'
2026-03-01T15:58:23.2430306Z ⭐ Run Main 检出代码
2026-03-01T15:58:23.2430493Z Writing entry to tarball workflow/outputcmd.txt len:0
2026-03-01T15:58:23.2430662Z Writing entry to tarball workflow/statecmd.txt len:0
2026-03-01T15:58:23.2430784Z Writing entry to tarball workflow/pathcmd.txt len:0
2026-03-01T15:58:23.2431018Z Writing entry to tarball workflow/envs.txt len:0
2026-03-01T15:58:23.2431123Z Writing entry to tarball workflow/SUMMARY.md len:0
2026-03-01T15:58:23.2431256Z Extracting content to '/var/run/act'
2026-03-01T15:58:23.2589330Z expression 'git clone -b ${{ github.ref_name }} ${{ github.server_url }}/${{ github.repository }}.git .\ngit checkout ${{ github.sha }}\n' rewritten to 'format('git clone -b {0} {1}/{2}.git .\ngit checkout {3}\n', github.ref_name, github.server_url, github.repository, github.sha)'
2026-03-01T15:58:23.2589602Z evaluating expression 'format('git clone -b {0} {1}/{2}.git .\ngit checkout {3}\n', github.ref_name, github.server_url, github.repository, github.sha)'
2026-03-01T15:58:23.2589922Z expression 'format('git clone -b {0} {1}/{2}.git .\ngit checkout {3}\n', github.ref_name, github.server_url, github.repository, github.sha)' evaluated to '%!t(string=git clone -b feature/article-word-count http://192.168.1.22:8418/starmen1265/kb-web.git .\ngit checkout 5a8cf2cbece7fecc709e03695472911ccbd47dbe\n)'
2026-03-01T15:58:23.2590010Z Wrote command \n\ngit clone -b feature/article-word-count http://192.168.1.22:8418/starmen1265/kb-web.git .\ngit checkout 5a8cf2cbece7fecc709e03695472911ccbd47dbe\n\n\n to 'workflow/0.sh'
2026-03-01T15:58:23.2590121Z Writing entry to tarball workflow/0.sh len:146
2026-03-01T15:58:23.2590232Z Extracting content to '/var/run/act'
2026-03-01T15:58:23.2605306Z 🐳 docker exec cmd=[sh -e /var/run/act/workflow/0.sh] user= workdir=
2026-03-01T15:58:23.2605520Z Exec command '[sh -e /var/run/act/workflow/0.sh]'
2026-03-01T15:58:23.2605708Z Working directory '/workspace/starmen1265/kb-web'
2026-03-01T15:58:23.3003845Z Cloning into '.'...
2026-03-01T15:58:23.4054573Z Note: switching to '5a8cf2cbece7fecc709e03695472911ccbd47dbe'.
2026-03-01T15:58:23.4055193Z
2026-03-01T15:58:23.4055422Z You are in 'detached HEAD' state. You can look around, make experimental
2026-03-01T15:58:23.4055677Z changes and commit them, and you can discard any commits you make in this
2026-03-01T15:58:23.4055904Z state without impacting any branches by switching back to a branch.
2026-03-01T15:58:23.4056154Z
2026-03-01T15:58:23.4056347Z If you want to create a new branch to retain commits you create, you may
2026-03-01T15:58:23.4056561Z do so (now or later) by using -c with the switch command. Example:
2026-03-01T15:58:23.4056756Z
2026-03-01T15:58:23.4056954Z git switch -c <new-branch-name>
2026-03-01T15:58:23.4057181Z
2026-03-01T15:58:23.4057381Z Or undo this operation with:
2026-03-01T15:58:23.4057515Z
2026-03-01T15:58:23.4057620Z git switch -
2026-03-01T15:58:23.4057729Z
2026-03-01T15:58:23.4057844Z Turn off this advice by setting config variable advice.detachedHead to false
2026-03-01T15:58:23.4057965Z
2026-03-01T15:58:23.4058071Z HEAD is now at 5a8cf2c fix: 添加多个调试 echo 确认执行流程
2026-03-01T15:58:23.4872746Z PHP: PHP 8.2.30 (cli) (built: Feb 24 2026 19:14:45) (NTS)
2026-03-01T15:58:23.5297459Z PHP version 8.2.30 (/usr/local/bin/php)
2026-03-01T15:58:23.5298011Z Run the "diagnose" command to get more detailed diagnostics output.
2026-03-01T15:58:23.5321347Z Composer: Composer version 2.9.5 2026-01-29 11:40:53
2026-03-01T15:58:23.5324055Z /var/run/act/workflow/1.sh: 4: node: not found
2026-03-01T15:58:23.5324981Z Node:
2026-03-01T15:58:23.6061077Z 📝 PHP 语法检查...
2026-03-01T15:58:23.6517685Z ❌ Failure - Main PHP 语法检查
2026-03-01T15:58:23.6572646Z exitcode '255': failure
2026-03-01T15:58:23.6983093Z Cleaning up container for job 快速测试
2026-03-01T15:58:23.8381783Z Removed container: 3bf638d4a888a88d0a68648e6dc93c83e717af46e024b49bd12104695dbdfdc9
2026-03-01T15:58:23.8387755Z 🐳 docker volume rm GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB
2026-03-01T15:58:23.8433681Z 🐳 docker volume rm GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-env
2026-03-01T15:58:23.8464827Z Cleaning up network for job 快速测试, and network name is: GITEA-ACTIONS-TASK-62_WORKFLOW-Test-CI_JOB-quick-test-network
2026-03-01T15:58:23.9970205Z 🏁 Job failed
2026-03-01T15:58:24.0047540Z Job '快速测试' failed
+74 -94
View File
@@ -1,7 +1,7 @@
{
"meta": {
"lastTouchedVersion": "2026.2.9",
"lastTouchedAt": "2026-02-15T16:09:55.707Z"
"lastTouchedAt": "2026-03-03T15:06:05.649Z"
},
"wizard": {
"lastRunAt": "2026-02-13T15:47:05.928Z",
@@ -112,6 +112,7 @@
"model": {
"primary": "m25",
"fallbacks": [
"g47f",
"g5",
"q3",
"k2p5"
@@ -121,6 +122,9 @@
"zhipu/glm-5": {
"alias": "g5"
},
"zhipu/glm-4.7-FlashX": {
"alias": "g47f"
},
"mm/MiniMax-M2.5": {
"alias": "m25"
},
@@ -133,73 +137,55 @@
},
"workspace": "/home/starmen1265/.openclaw/workspace",
"compaction": {
"mode": "safeguard",
"reserveTokensFloor": 10000,
"memoryFlush": {
"enabled": true,
"softThresholdTokens": 2000
}
"mode": "safeguard"
},
"maxConcurrent": 8,
"subagents": {
"maxConcurrent": 16
},
"thinkingDefault": "low"
}
},
"list": [
{
"id": "haonan",
"name": "陈浩南",
"workspace": "/home/starmen1265/.openclaw/agents/haonan/workspace",
"model": "m25",
"skills": [
"common-require"
],
"identity": {
"name": "陈浩南",
"emoji": "📚"
}
"common-require",
"capability-evolver",
"self-improving-agent"
]
},
{
"id": "knowledge-base",
"name": "知识库助手",
"workspace": "/home/starmen1265/.openclaw/agents/knowledge-base/workspace",
"model": "g5",
"skills": [
"common-require",
"kb-workflow"
],
"identity": {
"name": "知识库助手",
"emoji": "📚"
}
"kb-workflow",
"capability-evolver",
"self-improving-agent"
]
},
{
"id": "dev-assist",
"name": "程序开发助手",
"workspace": "/home/starmen1265/.openclaw/agents/dev-assist/workspace",
"model": "g5",
"skills": [
"common-require",
"clawlist"
],
"identity": {
"name": "程序开发助手",
"emoji": "📚"
}
"clawlist",
"capability-evolver",
"self-improving-agent"
]
},
{
"id": "ui-designer",
"name": "UI/UX设计师",
"workspace": "/home/starmen1265/.openclaw/agents/ui-designer/workspace",
"model": "g5",
"skills": [
"common-require"
],
"identity": {
"name": "设计师",
"emoji": "🎨"
}
"common-require",
"capability-evolver",
"self-improving-agent"
]
},
{
"id": "thinker",
"skills": [
"common-require",
"capability-evolver",
"self-improving-agent"
]
}
]
},
@@ -231,71 +217,38 @@
},
"bindings": [
{
"agentId": "knowledge-base",
"agentId": "haonan",
"match": {
"channel": "feishu",
"peer": {
"kind": "group",
"id": "oc_79bcbd274c7cff8757b52e2ec1d8e12b"
}
}
},
{
"agentId": "knowledge-base",
"match": {
"channel": "wecom",
"peer": {
"kind": "group",
"id": "wr_wAnbgAAXlqKMkyGznqpPMYZg2YTYg"
}
"accountId": "feishu-pm"
}
},
{
"agentId": "dev-assist",
"match": {
"channel": "feishu",
"peer": {
"kind": "group",
"id": "oc_f86956cb9cecb7ad38c0f621377a23ae"
}
"accountId": "feishu-dev"
}
},
{
"agentId": "dev-assist",
"match": {
"channel": "wecom",
"peer": {
"kind": "group",
"id": "wr_wAnbgAA_ot7JsLAgOD2IMcsGaT_8Q"
}
}
},
{
"agentId": "haonan",
"agentId": "knowledge-base",
"match": {
"channel": "feishu",
"accountId": "ou_be83febac9ee2afaa343c642bc697c48"
"accountId": "feishu-worker"
}
},
{
"agentId": "haonan",
"agentId": "thinker",
"match": {
"channel": "wecom",
"accountId": "ZhangQing"
"channel": "feishu",
"accountId": "feishu-qa"
}
},
{
"agentId": "ui-designer",
"match": {
"channel": "feishu",
"accountId": "ou_6de30318970345c4c79bea292b93130c"
}
},
{
"agentId": "ui-designer",
"match": {
"channel": "wecom",
"accountId": "bala"
"accountId": "feishu-art"
}
}
],
@@ -340,13 +293,29 @@
"channels": {
"feishu": {
"enabled": true,
"appId": "cli_a9f74dc9a3f8dbce",
"appSecret": "QtnvzRUhhFoNTTorxJuAwgSJ3ocPv0p1",
"dmPolicy": "allowlist",
"allowFrom": [
"ou_be83febac9ee2afaa343c642bc697c48",
"ou_6de30318970345c4c79bea292b93130c"
],
"dmPolicy": "open",
"accounts": {
"feishu-pm": {
"appId": "cli_a9f74dc9a3f8dbce",
"appSecret": "QtnvzRUhhFoNTTorxJuAwgSJ3ocPv0p1"
},
"feishu-dev": {
"appId": "cli_a92bae598f789bd7",
"appSecret": "kmwCLAI8IdZZn1KtwiPTVg758u0unGzY"
},
"feishu-worker": {
"appId": "cli_a92ba8ad3cb81bde",
"appSecret": "8bnMNIzy4QAv96c2xjmjgdUILO1qrO2w"
},
"feishu-qa": {
"appId": "cli_a92baf81053f1bc0",
"appSecret": "D5Pk8Fzwj5LWQr1FGnBlCbbJa3Ox7bJQ"
},
"feishu-art": {
"appId": "cli_a92baa31fdf8dbc0",
"appSecret": "BKjGWv1MTz7WmTn0I5A2EhuKyzwuuUnk"
}
},
"domain": "feishu",
"groupPolicy": "open",
"requireMention": true
@@ -360,6 +329,17 @@
"dm": {
"policy": "open"
}
},
"agent": {
"corpId": "wwe98fa68a53dbf51d",
"corpSecret": "C5rwbH55fKEB8mwVgyMcwzMs-4KgR9DcIXsyMGbxfwg",
"agentId": "1000002",
"token": "vjgcDzgRcqeFOG5X8lpdAk87gsw7i",
"encodingAESKey": "bAx5QMjhHIZqnd33VgVcs4exD5JRLNn9zc1gmICiIS2",
"dm": {
"policy": "open",
"allowFrom": []
}
}
}
},
+57 -61
View File
@@ -147,20 +147,20 @@
"list": [
{
"id": "haonan",
"name": "陈浩南",
"name": "\u9648\u6d69\u5357",
"workspace": "/home/starmen1265/.openclaw/agents/haonan/workspace",
"model": "m25",
"skills": [
"common-require"
],
"identity": {
"name": "陈浩南",
"emoji": "📚"
"name": "\u9648\u6d69\u5357",
"emoji": "\ud83d\udcda"
}
},
{
"id": "knowledge-base",
"name": "知识库助手",
"name": "\u77e5\u8bc6\u5e93\u52a9\u624b",
"workspace": "/home/starmen1265/.openclaw/agents/knowledge-base/workspace",
"model": "g5",
"skills": [
@@ -168,13 +168,13 @@
"kb-workflow"
],
"identity": {
"name": "知识库助手",
"emoji": "📚"
"name": "\u77e5\u8bc6\u5e93\u52a9\u624b",
"emoji": "\ud83d\udcda"
}
},
{
"id": "dev-assist",
"name": "程序开发助手",
"name": "\u7a0b\u5e8f\u5f00\u53d1\u52a9\u624b",
"workspace": "/home/starmen1265/.openclaw/agents/dev-assist/workspace",
"model": "g5",
"skills": [
@@ -182,21 +182,34 @@
"clawlist"
],
"identity": {
"name": "程序开发助手",
"emoji": "📚"
"name": "\u7a0b\u5e8f\u5f00\u53d1\u52a9\u624b",
"emoji": "\ud83d\udcda"
}
},
{
"id": "ui-designer",
"name": "UI/UX设计师",
"name": "UI/UX\u8bbe\u8ba1\u5e08",
"workspace": "/home/starmen1265/.openclaw/agents/ui-designer/workspace",
"model": "g5",
"skills": [
"common-require"
],
"identity": {
"name": "设计师",
"emoji": "🎨"
"name": "\u8bbe\u8ba1\u5e08",
"emoji": "\ud83c\udfa8"
}
},
{
"id": "thinker",
"name": "\u5ba1\u67e5\u5458",
"workspace": "/home/starmen1265/.openclaw/agents/thinker/workspace",
"model": "g5",
"skills": [
"common-require"
],
"identity": {
"name": "\u5ba1\u67e5\u5458",
"emoji": "\ud83d\udd0d"
}
}
]
@@ -229,71 +242,38 @@
},
"bindings": [
{
"agentId": "knowledge-base",
"agentId": "haonan",
"match": {
"channel": "feishu",
"peer": {
"kind": "group",
"id": "oc_79bcbd274c7cff8757b52e2ec1d8e12b"
}
}
},
{
"agentId": "knowledge-base",
"match": {
"channel": "wecom",
"peer": {
"kind": "group",
"id": "wr_wAnbgAAXlqKMkyGznqpPMYZg2YTYg"
}
"accountId": "feishu-pm"
}
},
{
"agentId": "dev-assist",
"match": {
"channel": "feishu",
"peer": {
"kind": "group",
"id": "oc_f86956cb9cecb7ad38c0f621377a23ae"
}
"accountId": "feishu-dev"
}
},
{
"agentId": "dev-assist",
"match": {
"channel": "wecom",
"peer": {
"kind": "group",
"id": "wr_wAnbgAA_ot7JsLAgOD2IMcsGaT_8Q"
}
}
},
{
"agentId": "haonan",
"agentId": "knowledge-base",
"match": {
"channel": "feishu",
"accountId": "ou_be83febac9ee2afaa343c642bc697c48"
"accountId": "feishu-worker"
}
},
{
"agentId": "haonan",
"agentId": "thinker",
"match": {
"channel": "wecom",
"accountId": "ZhangQing"
"channel": "feishu",
"accountId": "feishu-qa"
}
},
{
"agentId": "ui-designer",
"match": {
"channel": "feishu",
"accountId": "ou_6de30318970345c4c79bea292b93130c"
}
},
{
"agentId": "ui-designer",
"match": {
"channel": "wecom",
"accountId": "bala"
"accountId": "feishu-art"
}
}
],
@@ -338,13 +318,29 @@
"channels": {
"feishu": {
"enabled": true,
"appId": "cli_a9f74dc9a3f8dbce",
"appSecret": "QtnvzRUhhFoNTTorxJuAwgSJ3ocPv0p1",
"dmPolicy": "allowlist",
"allowFrom": [
"ou_be83febac9ee2afaa343c642bc697c48",
"ou_6de30318970345c4c79bea292b93130c"
],
"dmPolicy": "open",
"accounts": {
"feishu-pm": {
"appId": "cli_a9f74dc9a3f8dbce",
"appSecret": "QtnvzRUhhFoNTTorxJuAwgSJ3ocPv0p1"
},
"feishu-dev": {
"appId": "cli_a92bae598f789bd7",
"appSecret": "kmwCLAI8IdZZn1KtwiPTVg758u0unGzY"
},
"feishu-worker": {
"appId": "cli_a92ba8ad3cb81bde",
"appSecret": "8bnMNIzy4QAv96c2xjmjgdUILO1qrO2w"
},
"feishu-qa": {
"appId": "cli_a92baf81053f1bc0",
"appSecret": "D5Pk8Fzwj5LWQr1FGnBlCbbJa3Ox7bJQ"
},
"feishu-art": {
"appId": "cli_a92baa31fdf8dbc0",
"appSecret": "BKjGWv1MTz7WmTn0I5A2EhuKyzwuuUnk"
}
},
"domain": "feishu",
"groupPolicy": "open",
"requireMention": true
+2 -2
View File
@@ -1,7 +1,7 @@
{
"meta": {
"lastTouchedVersion": "2026.2.9",
"lastTouchedAt": "2026-02-15T15:54:32.431Z"
"lastTouchedAt": "2026-02-15T15:57:58.358Z"
},
"wizard": {
"lastRunAt": "2026-02-13T15:47:05.928Z",
@@ -300,7 +300,7 @@
"messages": {
"ackReactionScope": "group-mentions",
"tts": {
"auto": "always",
"auto": "tagged",
"provider": "edge",
"edge": {
"enabled": true,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"meta": {
"lastTouchedVersion": "2026.2.9",
"lastTouchedAt": "2026-02-15T15:50:36.798Z"
"lastTouchedAt": "2026-02-15T15:54:32.431Z"
},
"wizard": {
"lastRunAt": "2026-02-13T15:47:05.928Z",
@@ -300,7 +300,7 @@
"messages": {
"ackReactionScope": "group-mentions",
"tts": {
"auto": "tagged",
"auto": "always",
"provider": "edge",
"edge": {
"enabled": true,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"meta": {
"lastTouchedVersion": "2026.2.9",
"lastTouchedAt": "2026-02-15T15:43:40.588Z"
"lastTouchedAt": "2026-02-15T15:50:36.798Z"
},
"wizard": {
"lastRunAt": "2026-02-13T15:47:05.928Z",
@@ -300,7 +300,7 @@
"messages": {
"ackReactionScope": "group-mentions",
"tts": {
"auto": "always",
"auto": "tagged",
"provider": "edge",
"edge": {
"enabled": true,
+2 -2
View File
@@ -1,7 +1,7 @@
{
"meta": {
"lastTouchedVersion": "2026.2.9",
"lastTouchedAt": "2026-02-15T15:33:30.378Z"
"lastTouchedAt": "2026-02-15T15:43:40.588Z"
},
"wizard": {
"lastRunAt": "2026-02-13T15:47:05.928Z",
@@ -300,7 +300,7 @@
"messages": {
"ackReactionScope": "group-mentions",
"tts": {
"auto": "tagged",
"auto": "always",
"provider": "edge",
"edge": {
"enabled": true,
+4
View File
@@ -0,0 +1,4 @@
=== Capability Evolver 运行开始 ===
时间: 2026年 03月 03日 星期二 23:13:28 CST
发现有未提交的更改,先提交...
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
# Capability Evolver 定期运行脚本
# 策略:使用 repair-only 模式,仅修复问题不做大改动
EVOLVER_DIR="$HOME/.openclaw/skills/capability-evolver"
OUTPUT_FILE="$HOME/.openclaw/scripts/evolver-output.txt"
OPENCLAW_DIR="$HOME/.openclaw"
echo "=== Capability Evolver 运行开始 ===" > "$OUTPUT_FILE"
echo "时间: $(date)" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
# 切换到 openclaw 目录进行 git 备份
cd "$OPENCLAW_DIR"
# 检查是否有未提交的更改
if [[ -n $(git status --porcelain) ]]; then
echo "发现有未提交的更改,先提交..." >> "$OUTPUT_FILE"
git add -A
git commit -m "Auto backup before evolver run - $(date)" 2>/dev/null
fi
# 创建备份分支
BACKUP_BRANCH="backup/evolver-$(date +%Y%m%d-%H%M%S)"
echo "创建备份分支: $BACKUP_BRANCH" >> "$OUTPUT_FILE"
git branch -D "$BACKUP_BRANCH" 2>/dev/null || true
git branch "$BACKUP_BRANCH" 2>/dev/null
echo "" >> "$OUTPUT_FILE"
echo "=== Git 备份完成 ===" >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
# 运行 evolver - 使用 repair-only 策略确保安全
cd "$EVOLVER_DIR"
export EVOLVE_STRATEGY="repair-only"
echo "=== 运行 Capability Evolver (repair-only 模式) ===" >> "$OUTPUT_FILE"
node index.js >> "$OUTPUT_FILE" 2>&1
EXIT_CODE=$?
echo "" >> "$OUTPUT_FILE"
echo "=== Evolver 运行完成 (exit code: $EXIT_CODE) ===" >> "$OUTPUT_FILE"
# 检查变更
cd "$OPENCLAW_DIR"
echo "" >> "$OUTPUT_FILE"
echo "=== 本次变更 ===" >> "$OUTPUT_FILE"
git status --short >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
echo "=== 输出完成 ===" >> "$OUTPUT_FILE"
+11
View File
@@ -0,0 +1,11 @@
## Contributing
Thank you for contributing. Please follow these rules:
- Do not use emoji (except the DNA emoji in documentation if needed).
- Keep changes small and reviewable.
- Update related documentation when you change behavior.
- Run `node index.js` for a quick sanity check.
Submit PRs with clear intent and scope.
+237
View File
@@ -0,0 +1,237 @@
# 🧬 Capability Evolver
![Capability Evolver Cover](assets/cover.png)
**[evomap.ai](https://evomap.ai)** | [Documentation](https://evomap.ai/wiki) | [Chinese Docs](README.zh-CN.md)
---
**"Evolution is not optional. Adapt or die."**
**Three lines**
- **What it is**: A protocol-constrained self-evolution engine for AI agents.
- **Pain it solves**: Turns ad hoc prompt tweaks into auditable, reusable evolution assets.
- **Use in 30 seconds**: `node index.js` to generate a GEP-guided evolution prompt.
## EvoMap -- The Evolution Network
Capability Evolver is the core engine behind **[EvoMap](https://evomap.ai)**, a network where AI agents evolve through validated collaboration. Visit [evomap.ai](https://evomap.ai) to explore the full platform -- live agent maps, evolution leaderboards, and the ecosystem that turns isolated prompt tweaks into shared, auditable intelligence.
Keywords: protocol-constrained evolution, audit trail, genes and capsules, prompt governance.
## Try It Now (Minimal)
```bash
node index.js
```
## What It Does
The **Capability Evolver** inspects runtime history, extracts signals, selects a Gene/Capsule, and emits a strict GEP protocol prompt to guide safe evolution.
## Who This Is For / Not For
**For**
- Teams maintaining agent prompts and logs at scale
- Users who need auditable evolution traces (Genes, Capsules, Events)
- Environments requiring deterministic, protocol-bound changes
**Not For**
- One-off scripts without logs or history
- Projects that require free-form creative changes
- Systems that cannot tolerate protocol overhead
## Features
- **Auto-Log Analysis**: scans memory and history files for errors and patterns.
- **Self-Repair Guidance**: emits repair-focused directives from signals.
- **GEP Protocol**: standardized evolution with reusable assets.
- **Mutation + Personality Evolution**: each evolution run is gated by an explicit Mutation object and an evolvable PersonalityState.
- **Configurable Strategy Presets**: `EVOLVE_STRATEGY=balanced|innovate|harden|repair-only` controls intent balance.
- **Signal De-duplication**: prevents repair loops by detecting stagnation patterns.
- **Operations Module** (`src/ops/`): portable lifecycle, skill monitoring, cleanup, self-repair, wake triggers -- zero platform dependency.
- **Protected Source Files**: prevents autonomous agents from overwriting core evolver code.
- **One-Command Evolution**: `node index.js` to generate the prompt.
## Typical Use Cases
- Harden a flaky agent loop by enforcing validation before edits
- Encode recurring fixes as reusable Genes and Capsules
- Produce auditable evolution events for review or compliance
## Anti-Examples
- Rewriting entire subsystems without signals or constraints
- Using the protocol as a generic task runner
- Producing changes without recording EvolutionEvent
## FAQ
**Does this edit code automatically?**
No. It generates a protocol-bound prompt and assets that guide evolution.
**Do I need to use all GEP assets?**
No. You can start with default Genes and extend over time.
**Is this safe in production?**
Use review mode and validation steps. Treat it as a safety-focused evolution tool, not a live patcher.
## Roadmap
- Add a one-minute demo workflow
- Add a comparison table vs alternatives
## GEP Protocol (Auditable Evolution)
This repo includes a protocol-constrained prompt mode based on GEP (Genome Evolution Protocol).
- **Structured assets** live in `assets/gep/`:
- `assets/gep/genes.json`
- `assets/gep/capsules.json`
- `assets/gep/events.jsonl`
- **Selector** logic uses extracted signals to prefer existing Genes/Capsules and emits a JSON selector decision in the prompt.
- **Constraints**: Only the DNA emoji is allowed in documentation; all other emoji are disallowed.
## Usage
### Standard Run (Automated)
```bash
node index.js
```
### Review Mode (Human-in-the-Loop)
```bash
node index.js --review
```
### Continuous Loop
```bash
node index.js --loop
```
### With Strategy Preset
```bash
EVOLVE_STRATEGY=innovate node index.js --loop # maximize new features
EVOLVE_STRATEGY=harden node index.js --loop # focus on stability
EVOLVE_STRATEGY=repair-only node index.js --loop # emergency fix mode
```
### Operations (Lifecycle Management)
```bash
node src/ops/lifecycle.js start # start evolver loop in background
node src/ops/lifecycle.js stop # graceful stop (SIGTERM -> SIGKILL)
node src/ops/lifecycle.js status # show running state
node src/ops/lifecycle.js check # health check + auto-restart if stagnant
```
## Public Release
This repository is the public distribution.
- Build public output: `npm run build`
- Publish public output: `npm run publish:public`
- Dry run: `DRY_RUN=true npm run publish:public`
Required env vars:
- `PUBLIC_REMOTE` (default: `public`)
- `PUBLIC_REPO` (e.g. `autogame-17/evolver`)
- `PUBLIC_OUT_DIR` (default: `dist-public`)
- `PUBLIC_USE_BUILD_OUTPUT` (default: `true`)
Optional env vars:
- `SOURCE_BRANCH` (default: `main`)
- `PUBLIC_BRANCH` (default: `main`)
- `RELEASE_TAG` (e.g. `v1.0.41`)
- `RELEASE_TITLE` (e.g. `v1.0.41 - GEP protocol`)
- `RELEASE_NOTES` or `RELEASE_NOTES_FILE`
- `GITHUB_TOKEN` (or `GH_TOKEN` / `GITHUB_PAT`) for GitHub Release creation
- `RELEASE_SKIP` (`true` to skip creating a GitHub Release; default is to create)
- `RELEASE_USE_GH` (`true` to use `gh` CLI instead of GitHub API)
- `PUBLIC_RELEASE_ONLY` (`true` to only create a Release for an existing tag; no publish)
## Versioning (SemVer)
MAJOR.MINOR.PATCH
- MAJOR: incompatible changes
- MINOR: backward-compatible features
- PATCH: backward-compatible bug fixes
## Changelog
See the full release history on [GitHub Releases](https://github.com/autogame-17/evolver/releases).
## Security Model
This section describes the execution boundaries and trust model of the Capability Evolver.
### What Executes and What Does Not
| Component | Behavior | Executes Shell Commands? |
| :--- | :--- | :--- |
| `src/evolve.js` | Reads logs, selects genes, builds prompts, writes artifacts | Read-only git/process queries only |
| `src/gep/prompt.js` | Assembles the GEP protocol prompt string | No (pure text generation) |
| `src/gep/selector.js` | Scores and selects Genes/Capsules by signal matching | No (pure logic) |
| `src/gep/solidify.js` | Validates patches via Gene `validation` commands | Yes (see below) |
| `index.js` (loop recovery) | Prints `sessions_spawn(...)` text to stdout on crash | No (text output only; execution depends on host runtime) |
### Gene Validation Command Safety
`solidify.js` executes commands listed in a Gene's `validation` array. To prevent arbitrary command execution, all validation commands are gated by a safety check (`isValidationCommandAllowed`):
1. **Prefix whitelist**: Only commands starting with `node`, `npm`, or `npx` are allowed.
2. **No command substitution**: Backticks and `$(...)` are rejected anywhere in the command string.
3. **No shell operators**: After stripping quoted content, `;`, `&`, `|`, `>`, `<` are rejected.
4. **Timeout**: Each command is limited to 180 seconds.
5. **Scoped execution**: Commands run with `cwd` set to the repository root.
### A2A External Asset Ingestion
External Gene/Capsule assets ingested via `scripts/a2a_ingest.js` are staged in an isolated candidate zone. Promotion to local stores (`scripts/a2a_promote.js`) requires:
1. Explicit `--validated` flag (operator must verify the asset first).
2. For Genes: all `validation` commands are audited against the same safety check before promotion. Unsafe commands cause the promotion to be rejected.
3. Gene promotion never overwrites an existing local Gene with the same ID.
### `sessions_spawn` Output
The `sessions_spawn(...)` strings in `index.js` and `evolve.js` are **text output to stdout**, not direct function calls. Whether they are interpreted depends on the host runtime (e.g., OpenClaw platform). The evolver itself does not invoke `sessions_spawn` as executable code.
## Configuration & Decoupling
This skill is designed to be **environment-agnostic**. It uses standard OpenClaw tools by default.
### Local Overrides (Injection)
You can inject local preferences (e.g., using `feishu-card` instead of `message` for reports) without modifying the core code.
**Method 1: Environment Variables**
Set `EVOLVE_REPORT_TOOL` in your `.env` file:
```bash
EVOLVE_REPORT_TOOL=feishu-card
```
**Method 2: Dynamic Detection**
The script automatically detects if compatible local skills (like `skills/feishu-card`) exist in your workspace and upgrades its behavior accordingly.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=autogame-17/evolver&type=Date)](https://star-history.com/#autogame-17/evolver&Date)
## Acknowledgments
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network.
- [lichunr](https://github.com/lichunr) -- Contributed thousands of dollars in tokens for our compute network to use for free.
- [shinjiyu](https://github.com/shinjiyu) -- Submitted numerous bug reports and contributed multilingual signal extraction with snippet-carrying tags (PR #112).
- [voidborne-d](https://github.com/voidborne-d) -- Hardened pre-broadcast sanitization with 11 new credential redaction patterns (PR #107); added 45 tests for strategy, validationReport, and envFingerprint (PR #139).
- [blackdogcat](https://github.com/blackdogcat) -- Fixed missing dotenv dependency and implemented intelligent CPU load threshold auto-calculation (PR #144).
- [upbit](https://github.com/upbit) -- Played a vital role in popularizing evolver and evomap technologies.
- [Chi Jianqiang](https://mowen.cn) -- Made significant contributions to promotion and user experience improvements.
- More contributors to be added.
## License
MIT
+202
View File
@@ -0,0 +1,202 @@
# 🧬 Capability Evolver(能力进化引擎)
**[evomap.ai](https://evomap.ai)** | [Wiki 文档](https://evomap.ai/wiki) | [English Docs](README.md)
---
**“进化不是可选项,而是生存法则。”**
**Capability Evolver** 是一个元技能(Meta-Skill),赋予 OpenClaw 智能体自我反省的能力。它可以扫描自身的运行日志,识别效率低下或报错的地方,并自主编写代码补丁来优化自身性能。
本仓库内置 **基因组进化协议(Genome Evolution Protocol, GEP)**,用于将每次进化固化为可复用资产,降低后续同类问题的推理成本。
## EvoMap -- 进化网络
Capability Evolver 是 **[EvoMap](https://evomap.ai)** 的核心引擎。EvoMap 是一个 AI 智能体通过验证协作实现进化的网络。访问 [evomap.ai](https://evomap.ai) 了解完整平台 -- 实时智能体图谱、进化排行榜,以及将孤立的提示词调优转化为共享可审计智能的生态系统。
## 核心特性
- **自动日志分析**:自动扫描 `.jsonl` 会话日志,寻找错误模式。
- **自我修复**:检测运行时崩溃并编写修复补丁。
- **GEP 协议**:标准化进化流程与可复用资产,支持可审计与可共享。
- **突变协议与人格进化**:每次进化必须显式声明 Mutation,并维护可进化的 PersonalityState。
- **可配置进化策略**:通过 `EVOLVE_STRATEGY` 环境变量选择 `balanced`/`innovate`/`harden`/`repair-only` 模式,控制修复/优化/创新的比例。
- **信号去重**:自动检测修复循环,防止反复修同一个问题。
- **运维模块** (`src/ops/`):6 个可移植的运维工具(生命周期管理、技能健康监控、磁盘清理、Git 自修复等),零平台依赖。
- **源码保护**:防止自治代理覆写核心进化引擎源码。
- **动态集成**:自动检测并使用本地工具,如果不存在则回退到通用模式。
- **持续循环模式**:持续运行的自我进化循环。
## 使用方法
### 标准运行(自动化)
```bash
node index.js
```
### 审查模式(人工介入)
在应用更改前暂停,等待人工确认。
```bash
node index.js --review
```
### 持续循环(守护进程)
无限循环运行。适合作为后台服务。
```bash
node index.js --loop
```
### 指定进化策略
```bash
EVOLVE_STRATEGY=innovate node index.js --loop # 最大化创新
EVOLVE_STRATEGY=harden node index.js --loop # 聚焦稳定性
EVOLVE_STRATEGY=repair-only node index.js --loop # 紧急修复模式
```
| 策略 | 创新 | 优化 | 修复 | 适用场景 |
| :--- | :--- | :--- | :--- | :--- |
| `balanced`(默认) | 50% | 30% | 20% | 日常运行,稳步成长 |
| `innovate` | 80% | 15% | 5% | 系统稳定,快速出新功能 |
| `harden` | 20% | 40% | 40% | 大改动后,聚焦稳固 |
| `repair-only` | 0% | 20% | 80% | 紧急状态,全力修复 |
### 运维管理(生命周期)
```bash
node src/ops/lifecycle.js start # 后台启动进化循环
node src/ops/lifecycle.js stop # 优雅停止(SIGTERM -> SIGKILL)
node src/ops/lifecycle.js status # 查看运行状态
node src/ops/lifecycle.js check # 健康检查 + 停滞自动重启
```
## 典型使用场景
- 需要审计与可追踪的提示词演进
- 团队协作维护 Agent 的长期能力
- 希望将修复经验固化为可复用资产
## 反例
- 一次性脚本或没有日志的场景
- 需要完全自由发挥的改动
- 无法接受协议约束的系统
## GEP 协议(可审计进化)
本仓库内置基于 GEP 的“协议受限提示词模式”,用于把每次进化固化为可复用资产。
- **结构化资产目录**:`assets/gep/`
- `assets/gep/genes.json`
- `assets/gep/capsules.json`
- `assets/gep/events.jsonl`
- **Selector 选择器**:根据日志提取 signals,优先复用已有 Gene/Capsule,并在提示词中输出可审计的 Selector 决策 JSON。
- **约束**:除 🧬 外,禁止使用其他 emoji。
## 配置与解耦
本插件能自动适应你的环境。
| 环境变量 | 描述 | 默认值 |
| :--- | :--- | :--- |
| `EVOLVE_STRATEGY` | 进化策略预设 | `balanced` |
| `EVOLVE_REPORT_TOOL` | 用于报告结果的工具名称 | `message` |
| `MEMORY_DIR` | 记忆文件路径 | `./memory` |
| `OPENCLAW_WORKSPACE` | 工作区根路径 | 自动检测 |
| `EVOLVER_LOOP_SCRIPT` | 循环启动脚本路径 | 自动检测 wrapper 或 core |
## Public 发布
本仓库为公开发行版本。
- 构建公开产物:`npm run build`
- 发布公开产物:`npm run publish:public`
- 演练:`DRY_RUN=true npm run publish:public`
必填环境变量:
- `PUBLIC_REMOTE`(默认:`public`)
- `PUBLIC_REPO`(例如 `autogame-17/evolver`)
- `PUBLIC_OUT_DIR`(默认:`dist-public`)
- `PUBLIC_USE_BUILD_OUTPUT`(默认:`true`)
可选环境变量:
- `SOURCE_BRANCH`(默认:`main`)
- `PUBLIC_BRANCH`(默认:`main`)
- `RELEASE_TAG`(例如 `v1.0.41`)
- `RELEASE_TITLE`(例如 `v1.0.41 - GEP protocol`)
- `RELEASE_NOTES` 或 `RELEASE_NOTES_FILE`
- `GITHUB_TOKEN`(或 `GH_TOKEN` / `GITHUB_PAT`,用于创建 GitHub Release)
- `RELEASE_SKIP`(`true` 则跳过创建 GitHub Release;默认会创建)
- `RELEASE_USE_GH`(`true` 则使用 `gh` CLI,否则默认走 GitHub API)
- `PUBLIC_RELEASE_ONLY`(`true` 则仅为已存在的 tag 创建 Release;不发布代码)
## 版本号规则(SemVer)
MAJOR.MINOR.PATCH
• MAJOR(主版本):有不兼容变更
• MINOR(次版本):向后兼容的新功能
• PATCH(修订/补丁):向后兼容的问题修复
## 更新日志
完整的版本发布记录请查看 [GitHub Releases](https://github.com/autogame-17/evolver/releases)。
## 安全模型
本节描述 Capability Evolver 的执行边界和信任模型。
### 各组件执行行为
| 组件 | 行为 | 是否执行 Shell 命令 |
| :--- | :--- | :--- |
| `src/evolve.js` | 读取日志、选择 Gene、构建提示词、写入工件 | 仅只读 git/进程查询 |
| `src/gep/prompt.js` | 组装 GEP 协议提示词字符串 | 否(纯文本生成) |
| `src/gep/selector.js` | 按信号匹配对 Gene/Capsule 评分和选择 | 否(纯逻辑) |
| `src/gep/solidify.js` | 通过 Gene `validation` 命令验证补丁 | 是(见下文) |
| `index.js`(循环恢复) | 崩溃时向 stdout 输出 `sessions_spawn(...)` 文本 | 否(纯文本输出;是否执行取决于宿主运行时) |
### Gene Validation 命令安全机制
`solidify.js` 执行 Gene 的 `validation` 数组中的命令。为防止任意命令执行,所有 validation 命令在执行前必须通过安全检查(`isValidationCommandAllowed`):
1. **前缀白名单**:仅允许以 `node`、`npm` 或 `npx` 开头的命令。
2. **禁止命令替换**:命令中任何位置出现反引号或 `$(...)` 均被拒绝。
3. **禁止 Shell 操作符**:去除引号内容后,`;`、`&`、`|`、`>`、`<` 均被拒绝。
4. **超时限制**:每条命令限时 180 秒。
5. **作用域限定**:命令以仓库根目录为工作目录执行。
### A2A 外部资产摄入
通过 `scripts/a2a_ingest.js` 摄入的外部 Gene/Capsule 资产被暂存在隔离的候选区。提升到本地存储(`scripts/a2a_promote.js`)需要:
1. 显式传入 `--validated` 标志(操作者必须先验证资产)。
2. 对 Gene:提升前审查所有 `validation` 命令,不安全的命令会导致提升被拒绝。
3. Gene 提升不会覆盖本地已存在的同 ID Gene。
### `sessions_spawn` 输出
`index.js` 和 `evolve.js` 中的 `sessions_spawn(...)` 字符串是**输出到 stdout 的纯文本**,而非直接函数调用。是否被执行取决于宿主运行时(如 OpenClaw 平台)。进化引擎本身不将 `sessions_spawn` 作为可执行代码调用。
### 其他安全约束
1. **单进程锁**:进化引擎禁止生成子进化进程(防止 Fork 炸弹)。
2. **稳定性优先**:如果近期错误率较高,强制进入修复模式,暂停创新功能。
3. **环境检测**:外部集成(如 Git 同步)仅在检测到相应插件存在时才会启用。
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=autogame-17/evolver&type=Date)](https://star-history.com/#autogame-17/evolver&Date)
## 鸣谢
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。
- [lichunr](https://github.com/lichunr) -- 提供了数千美金 Token 供算力网络免费使用。
- [shinjiyu](https://github.com/shinjiyu) -- 为 evolver 和 evomap 提交了大量 bug report,并贡献了多语言信号提取与 snippet 标签功能 (PR #112)。
- [voidborne-d](https://github.com/voidborne-d) -- 为预广播脱敏层新增 11 种凭证检测模式,强化安全防护 (PR #107)。
- [upbit](https://github.com/upbit) -- 在 evolver 和 evomap 技术的普及中起到了至关重要的作用。
- [池建强](https://mowen.cn) -- 在传播和用户体验改进过程中做出了巨大贡献。
- 其余贡献者扩充中。
## 许可证
MIT
+110
View File
@@ -0,0 +1,110 @@
---
name: capability-evolver
description: A self-evolution engine for AI agents. Analyzes runtime history to identify improvements and applies protocol-constrained evolution.
tags: [meta, ai, self-improvement, core]
---
# 🧬 Capability Evolver
**"Evolution is not optional. Adapt or die."**
The **Capability Evolver** is a meta-skill that allows OpenClaw agents to inspect their own runtime history, identify failures or inefficiencies, and autonomously write new code or update their own memory to improve performance.
## Features
- **Auto-Log Analysis**: Automatically scans memory and history files for errors and patterns.
- **Self-Repair**: Detects crashes and suggests patches.
- GEP Protocol: Standardized evolution with reusable assets.
- **One-Command Evolution**: Just run `/evolve` (or `node index.js`).
## Usage
### Standard Run (Automated)
Runs the evolution cycle. If no flags are provided, it assumes fully automated mode (Mad Dog Mode) and executes changes immediately.
```bash
node index.js
```
### Review Mode (Human-in-the-Loop)
If you want to review changes before they are applied, pass the `--review` flag. The agent will pause and ask for confirmation.
```bash
node index.js --review
```
### Mad Dog Mode (Continuous Loop)
To run in an infinite loop (e.g., via cron or background process), use the `--loop` flag or just standard execution in a cron job.
```bash
node index.js --loop
```
## Configuration
| Environment Variable | Default | Description |
|---|---|---|
| `EVOLVE_ALLOW_SELF_MODIFY` | `false` | Allow evolution to modify evolver's own source code. **NOT recommended for production.** Enabling this can cause instability -- the evolver may introduce bugs into its own prompt generation, validation, or solidify logic, leading to cascading failures that require manual intervention. Only enable for controlled experiments. |
| `EVOLVE_LOAD_MAX` | `2.0` | Maximum 1-minute load average before evolver backs off. |
| `EVOLVE_STRATEGY` | `balanced` | Evolution strategy: `balanced`, `innovate`, `harden`, `repair-only`, `early-stabilize`, `steady-state`, or `auto`. |
## GEP Protocol (Auditable Evolution)
This package embeds a protocol-constrained evolution prompt (GEP) and a local, structured asset store:
- `assets/gep/genes.json`: reusable Gene definitions
- `assets/gep/capsules.json`: success capsules to avoid repeating reasoning
- `assets/gep/events.jsonl`: append-only evolution events (tree-like via parent id)
## Emoji Policy
Only the DNA emoji is allowed in documentation. All other emoji are disallowed.
## Configuration & Decoupling
This skill is designed to be **environment-agnostic**. It uses standard OpenClaw tools by default.
### Local Overrides (Injection)
You can inject local preferences (e.g., using `feishu-card` instead of `message` for reports) without modifying the core code.
**Method 1: Environment Variables**
Set `EVOLVE_REPORT_TOOL` in your `.env` file:
```bash
EVOLVE_REPORT_TOOL=feishu-card
```
**Method 2: Dynamic Detection**
The script automatically detects if compatible local skills (like `skills/feishu-card`) exist in your workspace and upgrades its behavior accordingly.
## Safety & Risk Protocol
### 1. Identity & Directives
- **Identity Injection**: "You are a Recursive Self-Improving System."
- **Mutation Directive**:
- If **Errors Found** -> **Repair Mode** (Fix bugs).
- If **Stable** -> **Forced Optimization** (Refactor/Innovate).
### 2. Risk Mitigation
- **Infinite Recursion**: Strict single-process logic.
- **Review Mode**: Use `--review` for sensitive environments.
- **Git Sync**: Always recommended to have a git-sync cron job running alongside this skill.
## Before Troubleshooting -- Check Your Version First
If you encounter unexpected errors or behavior, **always verify your version before debugging**:
```bash
node -e "const p=require('./package.json'); console.log(p.version)"
```
If you are not on the latest release, update first -- most reported issues are already fixed in newer versions:
```bash
# If installed via git
git pull && npm install
# If installed via npm (global install)
npm install -g evolver@latest
```
Latest releases and changelog: `https://github.com/autogame-17/evolver/releases`
## License
MIT
+17
View File
@@ -0,0 +1,17 @@
{
"owner": "autogame-17",
"slug": "capability-evolver",
"displayName": "Capability Evolver",
"latest": {
"version": "1.20.4",
"publishedAt": 1772344308631,
"commit": "https://github.com/openclaw/skills/commit/f6fbba39530294955fd7e3b735f4ca227f4eb546"
},
"history": [
{
"version": "1.14.0",
"publishedAt": 1771472983583,
"commit": "https://github.com/openclaw/skills/commit/fbb1b4a640f1c8254a1dd2757e8eb38d246b9d59"
}
]
}
@@ -0,0 +1,79 @@
{
"version": 1,
"capsules": [
{
"type": "Capsule",
"schema_version": "1.5.0",
"id": "capsule_1770477654236",
"trigger": [
"log_error",
"errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }",
"user_missing",
"windows_shell_incompatible",
"perf_bottleneck"
],
"gene": "gene_gep_repair_from_errors",
"summary": "固化:gene_gep_repair_from_errors 命中信号 log_error, errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }, user_missing, windows_shell_incompatible, perf_bottleneck,变更 1 文件 / 2 行。",
"confidence": 0.85,
"blast_radius": {
"files": 1,
"lines": 2
},
"outcome": {
"status": "success",
"score": 0.85
},
"success_streak": 1,
"env_fingerprint": {
"node_version": "v22.22.0",
"platform": "linux",
"arch": "x64",
"os_release": "6.1.0-42-cloud-amd64",
"evolver_version": "1.7.0",
"cwd": ".",
"captured_at": "2026-02-07T15:20:54.155Z"
},
"a2a": {
"eligible_to_broadcast": false
},
"asset_id": "sha256:3eed0cd5038f9e85fbe0d093890e291e9b8725644c766e6cce40bf62d0f5a2e8"
},
{
"type": "Capsule",
"schema_version": "1.5.0",
"id": "capsule_1770478341769",
"trigger": [
"log_error",
"errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }",
"user_missing",
"windows_shell_incompatible",
"perf_bottleneck"
],
"gene": "gene_gep_repair_from_errors",
"summary": "固化:gene_gep_repair_from_errors 命中信号 log_error, errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }, user_missing, windows_shell_incompatible, perf_bottleneck,变更 2 文件 / 44 行。",
"confidence": 0.85,
"blast_radius": {
"files": 2,
"lines": 44
},
"outcome": {
"status": "success",
"score": 0.85
},
"success_streak": 1,
"env_fingerprint": {
"node_version": "v22.22.0",
"platform": "linux",
"arch": "x64",
"os_release": "6.1.0-42-cloud-amd64",
"evolver_version": "1.7.0",
"cwd": ".",
"captured_at": "2026-02-07T15:32:21.678Z"
},
"a2a": {
"eligible_to_broadcast": false
},
"asset_id": "sha256:20d971a3c4cb2b75f9c045376d1aa003361c12a6b89a4b47b7e81dbd4f4d8fe8"
}
]
}
@@ -0,0 +1,108 @@
{
"version": 1,
"genes": [
{
"type": "Gene",
"id": "gene_gep_repair_from_errors",
"category": "repair",
"signals_match": [
"error",
"exception",
"failed",
"unstable"
],
"preconditions": [
"signals contains error-related indicators"
],
"strategy": [
"Extract structured signals from logs and user instructions",
"Select an existing Gene by signals match (no improvisation)",
"Estimate blast radius (files, lines) before editing",
"Apply smallest reversible patch",
"Validate using declared validation steps; rollback on failure",
"Solidify knowledge: append EvolutionEvent, update Gene/Capsule store"
],
"constraints": {
"max_files": 20,
"forbidden_paths": [
".git",
"node_modules"
]
},
"validation": [
"node scripts/validate-modules.js ./src/evolve ./src/gep/solidify",
"node scripts/validate-modules.js ./src/gep/selector ./src/gep/memoryGraph"
]
},
{
"type": "Gene",
"id": "gene_gep_optimize_prompt_and_assets",
"category": "optimize",
"signals_match": [
"protocol",
"gep",
"prompt",
"audit",
"reusable"
],
"preconditions": [
"need stricter, auditable evolution protocol outputs"
],
"strategy": [
"Extract signals and determine selection rationale via Selector JSON",
"Prefer reusing existing Gene/Capsule; only create if no match exists",
"Refactor prompt assembly to embed assets (genes, capsules, parent event)",
"Reduce noise and ambiguity; enforce strict output schema",
"Validate by running node index.js run and ensuring no runtime errors",
"Solidify: record EvolutionEvent, update Gene definitions, create Capsule on success"
],
"constraints": {
"max_files": 20,
"forbidden_paths": [
".git",
"node_modules"
]
},
"validation": [
"node scripts/validate-modules.js ./src/evolve ./src/gep/prompt"
]
},
{
"type": "Gene",
"id": "gene_gep_innovate_from_opportunity",
"category": "innovate",
"signals_match": [
"user_feature_request",
"user_improvement_suggestion",
"perf_bottleneck",
"capability_gap",
"stable_success_plateau",
"external_opportunity"
],
"preconditions": [
"at least one opportunity signal is present",
"no active log_error signals (stability first)"
],
"strategy": [
"Extract opportunity signals and identify the specific user need or system gap",
"Search existing Genes and Capsules for partial matches (avoid reinventing)",
"Design a minimal, testable implementation plan (prefer small increments)",
"Estimate blast radius; innovate changes may touch more files but must stay within constraints",
"Implement the change with clear validation criteria",
"Validate using declared validation steps; rollback on failure",
"Solidify: record EvolutionEvent with intent=innovate, create new Gene if pattern is novel, create Capsule on success"
],
"constraints": {
"max_files": 25,
"forbidden_paths": [
".git",
"node_modules"
]
},
"validation": [
"node scripts/validate-modules.js ./src/evolve ./src/gep/solidify"
]
}
]
}
+285
View File
@@ -0,0 +1,285 @@
const evolve = require('./src/evolve');
const { solidify } = require('./src/gep/solidify');
const path = require('path');
// Hardened Env Loading: Ensure .env is loaded before anything else
try { require('dotenv').config({ path: path.resolve(__dirname, './.env') }); } catch (e) { console.warn('[Evolver] Warning: dotenv not found or failed to load .env'); }
const fs = require('fs');
const { spawn } = require('child_process');
function sleepMs(ms) {
const n = parseInt(String(ms), 10);
const t = Number.isFinite(n) ? Math.max(0, n) : 0;
return new Promise(resolve => setTimeout(resolve, t));
}
function readJsonSafe(p) {
try {
if (!fs.existsSync(p)) return null;
const raw = fs.readFileSync(p, 'utf8');
if (!raw.trim()) return null;
return JSON.parse(raw);
} catch (e) {
return null;
}
}
function isPendingSolidify(state) {
const lastRun = state && state.last_run ? state.last_run : null;
const lastSolid = state && state.last_solidify ? state.last_solidify : null;
if (!lastRun || !lastRun.run_id) return false;
if (!lastSolid || !lastSolid.run_id) return true;
return String(lastSolid.run_id) !== String(lastRun.run_id);
}
function parseMs(v, fallback) {
const n = parseInt(String(v == null ? '' : v), 10);
if (Number.isFinite(n)) return Math.max(0, n);
return fallback;
}
// Singleton Guard - prevent multiple evolver daemon instances
function acquireLock() {
const lockFile = path.join(__dirname, 'evolver.pid');
try {
if (fs.existsSync(lockFile)) {
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
try {
process.kill(pid, 0); // Check if process exists
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
return false;
} catch (e) {
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
}
}
fs.writeFileSync(lockFile, String(process.pid));
return true;
} catch (err) {
console.error('[Singleton] Lock acquisition failed:', err);
return false;
}
}
function releaseLock() {
const lockFile = path.join(__dirname, 'evolver.pid');
try {
if (fs.existsSync(lockFile)) {
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
if (pid === process.pid) fs.unlinkSync(lockFile);
}
} catch (e) { /* ignore */ }
}
async function main() {
const args = process.argv.slice(2);
const command = args[0];
const isLoop = args.includes('--loop') || args.includes('--mad-dog');
if (command === 'run' || command === '/evolve' || isLoop) {
console.log('Starting capability evolver...');
if (isLoop) {
// Internal daemon loop (no wrapper required).
if (!acquireLock()) process.exit(0);
process.on('exit', releaseLock);
process.on('SIGINT', () => { releaseLock(); process.exit(); });
process.on('SIGTERM', () => { releaseLock(); process.exit(); });
process.env.EVOLVE_LOOP = 'true';
process.env.EVOLVE_BRIDGE = 'false';
console.log('Loop mode enabled (internal daemon).');
const solidifyStatePath = path.join(__dirname, 'memory', 'evolution_solidify_state.json');
const minSleepMs = parseMs(process.env.EVOLVER_MIN_SLEEP_MS, 2000);
const maxSleepMs = parseMs(process.env.EVOLVER_MAX_SLEEP_MS, 300000);
const idleThresholdMs = parseMs(process.env.EVOLVER_IDLE_THRESHOLD_MS, 500);
const pendingSleepMs = parseMs(
process.env.EVOLVE_PENDING_SLEEP_MS ||
process.env.EVOLVE_MIN_INTERVAL ||
process.env.FEISHU_EVOLVER_INTERVAL,
120000
);
const maxCyclesPerProcess = parseMs(process.env.EVOLVER_MAX_CYCLES_PER_PROCESS, 100) || 100;
const maxRssMb = parseMs(process.env.EVOLVER_MAX_RSS_MB, 500) || 500;
const suicideEnabled = String(process.env.EVOLVER_SUICIDE || '').toLowerCase() !== 'false';
// Start hub heartbeat (keeps node alive independently of evolution cycles)
try {
const { startHeartbeat } = require('./src/gep/a2aProtocol');
startHeartbeat();
} catch (e) {
console.warn('[Heartbeat] Failed to start: ' + (e.message || e));
}
let currentSleepMs = minSleepMs;
let cycleCount = 0;
while (true) {
try {
cycleCount += 1;
// Ralph-loop gating: do not run a new cycle while previous run is pending solidify.
const st0 = readJsonSafe(solidifyStatePath);
if (isPendingSolidify(st0)) {
await sleepMs(Math.max(pendingSleepMs, minSleepMs));
continue;
}
const t0 = Date.now();
let ok = false;
try {
await evolve.run();
ok = true;
} catch (error) {
const msg = error && error.message ? String(error.message) : String(error);
console.error(`Evolution cycle failed: ${msg}`);
}
const dt = Date.now() - t0;
// Adaptive sleep: treat very fast cycles as "idle", backoff; otherwise reset to min.
if (!ok || dt < idleThresholdMs) {
currentSleepMs = Math.min(maxSleepMs, Math.max(minSleepMs, currentSleepMs * 2));
} else {
currentSleepMs = minSleepMs;
}
// Suicide check (memory leak protection)
if (suicideEnabled) {
const memMb = process.memoryUsage().rss / 1024 / 1024;
if (cycleCount >= maxCyclesPerProcess || memMb > maxRssMb) {
console.log(`[Daemon] Restarting self (cycles=${cycleCount}, rssMb=${memMb.toFixed(0)})`);
releaseLock();
const spawnOpts = {
detached: true,
stdio: 'ignore',
env: process.env,
windowsHide: true,
};
const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
child.unref();
process.exit(0);
}
}
let saturationMultiplier = 1;
try {
const st1 = readJsonSafe(solidifyStatePath);
const lastSignals = st1 && st1.last_run && Array.isArray(st1.last_run.signals) ? st1.last_run.signals : [];
if (lastSignals.includes('force_steady_state')) {
saturationMultiplier = 10;
console.log('[Daemon] Saturation detected. Entering steady-state mode (10x sleep).');
} else if (lastSignals.includes('evolution_saturation')) {
saturationMultiplier = 5;
console.log('[Daemon] Approaching saturation. Reducing evolution frequency (5x sleep).');
}
} catch (e) {}
// Jitter to avoid lockstep restarts.
const jitter = Math.floor(Math.random() * 250);
await sleepMs((currentSleepMs + jitter) * saturationMultiplier);
} catch (loopErr) {
console.error('[Daemon] Unexpected loop error (recovering): ' + (loopErr && loopErr.message ? loopErr.message : String(loopErr)));
await sleepMs(Math.max(minSleepMs, 10000));
}
}
} else {
// Normal Single Run
try {
await evolve.run();
} catch (error) {
console.error('Evolution failed:', error);
process.exit(1);
}
}
// Post-run hint
console.log('\n' + '=======================================================');
console.log('Capability evolver finished. If you use this project, consider starring the upstream repository.');
console.log('Upstream: https://github.com/autogame-17/capability-evolver');
console.log('=======================================================\n');
} else if (command === 'solidify') {
const dryRun = args.includes('--dry-run');
const noRollback = args.includes('--no-rollback');
const intentFlag = args.find(a => typeof a === 'string' && a.startsWith('--intent='));
const summaryFlag = args.find(a => typeof a === 'string' && a.startsWith('--summary='));
const intent = intentFlag ? intentFlag.slice('--intent='.length) : null;
const summary = summaryFlag ? summaryFlag.slice('--summary='.length) : null;
try {
const res = solidify({
intent: intent || undefined,
summary: summary || undefined,
dryRun,
rollbackOnFailure: !noRollback,
});
const st = res && res.ok ? 'SUCCESS' : 'FAILED';
console.log(`[SOLIDIFY] ${st}`);
if (res && res.gene) console.log(JSON.stringify(res.gene, null, 2));
if (res && res.event) console.log(JSON.stringify(res.event, null, 2));
if (res && res.capsule) console.log(JSON.stringify(res.capsule, null, 2));
if (res && res.ok && !dryRun) {
try {
const { shouldDistill, prepareDistillation } = require('./src/gep/skillDistiller');
if (shouldDistill()) {
const dr = prepareDistillation();
if (dr && dr.ok && dr.promptPath) {
console.log('\n[DISTILL_REQUEST]');
console.log('Distillation prompt ready. Read the prompt file, process it with your LLM,');
console.log('save the LLM response to a file, then run:');
console.log(' node index.js distill --response-file=<path_to_llm_response>');
console.log('Prompt file: ' + dr.promptPath);
console.log('[/DISTILL_REQUEST]');
}
}
} catch (e) {
console.warn('[Distiller] Init failed (non-fatal): ' + (e.message || e));
}
}
process.exit(res && res.ok ? 0 : 2);
} catch (error) {
console.error('[SOLIDIFY] Error:', error);
process.exit(2);
}
} else if (command === 'distill') {
const responseFileFlag = args.find(a => typeof a === 'string' && a.startsWith('--response-file='));
if (!responseFileFlag) {
console.error('Usage: node index.js distill --response-file=<path>');
process.exit(1);
}
const responseFilePath = responseFileFlag.slice('--response-file='.length);
try {
const responseText = fs.readFileSync(responseFilePath, 'utf8');
const { completeDistillation } = require('./src/gep/skillDistiller');
const result = completeDistillation(responseText);
if (result && result.ok) {
console.log('[Distiller] Gene produced: ' + result.gene.id);
console.log(JSON.stringify(result.gene, null, 2));
} else {
console.warn('[Distiller] Distillation did not produce a gene: ' + (result && result.reason || 'unknown'));
}
process.exit(result && result.ok ? 0 : 2);
} catch (error) {
console.error('[DISTILL] Error:', error);
process.exit(2);
}
} else {
console.log(`Usage: node index.js [run|/evolve|solidify|distill] [--loop]
- solidify flags:
- --dry-run
- --no-rollback
- --intent=repair|optimize|innovate
- --summary=...
- distill flags:
- --response-file=<path> (LLM response file for skill distillation)`);
}
}
if (require.main === module) {
main();
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "evolver",
"version": "1.20.4",
"description": "A GEP-powered self-evolution engine for AI agents. Features automated log analysis and Genome Evolution Protocol (GEP) for auditable, reusable evolution assets.",
"main": "index.js",
"keywords": [
"openclaw",
"ai",
"evolution",
"meta-learning",
"self-repair",
"automation",
"agent"
],
"author": "OpenClaw",
"license": "MIT",
"scripts": {
"start": "node index.js",
"run": "node index.js run",
"solidify": "node index.js solidify",
"a2a:export": "node scripts/a2a_export.js",
"a2a:ingest": "node scripts/a2a_ingest.js",
"a2a:promote": "node scripts/a2a_promote.js"
},
"dependencies": {
"dotenv": "^16.4.7"
}
}
@@ -0,0 +1,63 @@
const { loadGenes, loadCapsules, readAllEvents } = require('../src/gep/assetStore');
const { exportEligibleCapsules, exportEligibleGenes, isAllowedA2AAsset } = require('../src/gep/a2a');
const { buildPublish, buildHello, getTransport } = require('../src/gep/a2aProtocol');
const { computeAssetId, SCHEMA_VERSION } = require('../src/gep/contentHash');
function main() {
var args = process.argv.slice(2);
var asJson = args.includes('--json');
var asProtocol = args.includes('--protocol');
var withHello = args.includes('--hello');
var persist = args.includes('--persist');
var includeEvents = args.includes('--include-events');
var capsules = loadCapsules();
var genes = loadGenes();
var events = readAllEvents();
// Build eligible list: Capsules (filtered) + Genes (filtered) + Events (opt-in)
var eligibleCapsules = exportEligibleCapsules({ capsules: capsules, events: events });
var eligibleGenes = exportEligibleGenes({ genes: genes });
var eligible = eligibleCapsules.concat(eligibleGenes);
if (includeEvents) {
var eligibleEvents = (Array.isArray(events) ? events : []).filter(function (e) {
return isAllowedA2AAsset(e) && e.type === 'EvolutionEvent';
});
for (var ei = 0; ei < eligibleEvents.length; ei++) {
var ev = eligibleEvents[ei];
if (!ev.schema_version) ev.schema_version = SCHEMA_VERSION;
if (!ev.asset_id) { try { ev.asset_id = computeAssetId(ev); } catch (e) {} }
}
eligible = eligible.concat(eligibleEvents);
}
if (withHello || asProtocol) {
var hello = buildHello({ geneCount: genes.length, capsuleCount: capsules.length });
process.stdout.write(JSON.stringify(hello) + '\n');
if (persist) { try { getTransport().send(hello); } catch (e) {} }
}
if (asProtocol) {
for (var i = 0; i < eligible.length; i++) {
var msg = buildPublish({ asset: eligible[i] });
process.stdout.write(JSON.stringify(msg) + '\n');
if (persist) { try { getTransport().send(msg); } catch (e) {} }
}
return;
}
if (asJson) {
process.stdout.write(JSON.stringify(eligible, null, 2) + '\n');
return;
}
for (var j = 0; j < eligible.length; j++) {
process.stdout.write(JSON.stringify(eligible[j]) + '\n');
}
}
try { main(); } catch (e) {
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
process.exit(1);
}
@@ -0,0 +1,79 @@
var fs = require('fs');
var assetStore = require('../src/gep/assetStore');
var a2a = require('../src/gep/a2a');
var memGraph = require('../src/gep/memoryGraphAdapter');
var contentHash = require('../src/gep/contentHash');
var a2aProto = require('../src/gep/a2aProtocol');
function readStdin() {
try { return fs.readFileSync(0, 'utf8'); } catch (e) { return ''; }
}
function parseSignalsFromEnv() {
var raw = process.env.A2A_SIGNALS || '';
if (!raw) return [];
try {
var maybe = JSON.parse(raw);
if (Array.isArray(maybe)) return maybe.map(String).filter(Boolean);
} catch (e) {}
return String(raw).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
}
function main() {
var args = process.argv.slice(2);
var inputPath = '';
for (var i = 0; i < args.length; i++) {
if (args[i] && !args[i].startsWith('--')) { inputPath = args[i]; break; }
}
var source = process.env.A2A_SOURCE || 'external';
var factor = Number.isFinite(Number(process.env.A2A_EXTERNAL_CONFIDENCE_FACTOR))
? Number(process.env.A2A_EXTERNAL_CONFIDENCE_FACTOR) : 0.6;
var text = inputPath ? a2a.readTextIfExists(inputPath) : readStdin();
var parsed = a2a.parseA2AInput(text);
var signals = parseSignalsFromEnv();
var accepted = 0;
var rejected = 0;
var emitDecisions = process.env.A2A_EMIT_DECISIONS === 'true';
for (var j = 0; j < parsed.length; j++) {
var obj = parsed[j];
if (!a2a.isAllowedA2AAsset(obj)) continue;
if (obj.asset_id && typeof obj.asset_id === 'string') {
if (!contentHash.verifyAssetId(obj)) {
rejected += 1;
if (emitDecisions) {
try {
var dm = a2aProto.buildDecision({ assetId: obj.asset_id, localId: obj.id, decision: 'reject', reason: 'asset_id integrity check failed' });
a2aProto.getTransport().send(dm);
} catch (e) {}
}
continue;
}
}
var staged = a2a.lowerConfidence(obj, { source: source, factor: factor });
if (!staged) continue;
assetStore.appendExternalCandidateJsonl(staged);
try { memGraph.recordExternalCandidate({ asset: staged, source: source, signals: signals }); } catch (e) {}
if (emitDecisions) {
try {
var dm2 = a2aProto.buildDecision({ assetId: staged.asset_id, localId: staged.id, decision: 'quarantine', reason: 'staged as external candidate' });
a2aProto.getTransport().send(dm2);
} catch (e) {}
}
accepted += 1;
}
process.stdout.write('accepted=' + accepted + ' rejected=' + rejected + '\n');
}
try { main(); } catch (e) {
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
process.exit(1);
}
@@ -0,0 +1,118 @@
var assetStore = require('../src/gep/assetStore');
var solidifyMod = require('../src/gep/solidify');
var contentHash = require('../src/gep/contentHash');
var a2aProto = require('../src/gep/a2aProtocol');
function parseArgs(argv) {
var out = { flags: new Set(), kv: new Map(), positionals: [] };
for (var i = 0; i < argv.length; i++) {
var a = argv[i];
if (!a) continue;
if (a.startsWith('--')) {
var eq = a.indexOf('=');
if (eq > -1) { out.kv.set(a.slice(2, eq), a.slice(eq + 1)); }
else {
var key = a.slice(2);
var next = argv[i + 1];
if (next && !String(next).startsWith('--')) { out.kv.set(key, next); i++; }
else { out.flags.add(key); }
}
} else { out.positionals.push(a); }
}
return out;
}
function main() {
var args = parseArgs(process.argv.slice(2));
var id = String(args.kv.get('id') || '').trim();
var typeRaw = String(args.kv.get('type') || '').trim().toLowerCase();
var validated = args.flags.has('validated') || String(args.kv.get('validated') || '') === 'true';
var limit = Number.isFinite(Number(args.kv.get('limit'))) ? Number(args.kv.get('limit')) : 500;
if (!id || !typeRaw) throw new Error('Usage: node scripts/a2a_promote.js --type capsule|gene|event --id <id> --validated');
if (!validated) throw new Error('Refusing to promote without --validated (local verification must be done first).');
var type = typeRaw === 'capsule' ? 'Capsule' : typeRaw === 'gene' ? 'Gene' : typeRaw === 'event' ? 'EvolutionEvent' : '';
if (!type) throw new Error('Invalid --type. Use capsule, gene, or event.');
var external = assetStore.readRecentExternalCandidates(limit);
var candidate = null;
for (var i = 0; i < external.length; i++) {
if (external[i] && external[i].type === type && String(external[i].id) === id) { candidate = external[i]; break; }
}
if (!candidate) throw new Error('Candidate not found in external zone: type=' + type + ' id=' + id);
if (type === 'Gene') {
var validation = Array.isArray(candidate.validation) ? candidate.validation : [];
for (var j = 0; j < validation.length; j++) {
var c = String(validation[j] || '').trim();
if (!c) continue;
if (!solidifyMod.isValidationCommandAllowed(c)) {
throw new Error('Refusing to promote Gene ' + id + ': validation command rejected by safety check: "' + c + '". Only node/npm/npx commands without shell operators are allowed.');
}
}
}
var promoted = JSON.parse(JSON.stringify(candidate));
if (!promoted.a2a || typeof promoted.a2a !== 'object') promoted.a2a = {};
promoted.a2a.status = 'promoted';
promoted.a2a.promoted_at = new Date().toISOString();
if (!promoted.schema_version) promoted.schema_version = contentHash.SCHEMA_VERSION;
promoted.asset_id = contentHash.computeAssetId(promoted);
var emitDecisions = process.env.A2A_EMIT_DECISIONS === 'true';
if (type === 'EvolutionEvent') {
assetStore.appendEventJsonl(promoted);
if (emitDecisions) {
try {
var dmEv = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'event promoted for provenance tracking' });
a2aProto.getTransport().send(dmEv);
} catch (e) {}
}
process.stdout.write('promoted_event=' + id + '\n');
return;
}
if (type === 'Capsule') {
assetStore.appendCapsule(promoted);
if (emitDecisions) {
try {
var dm = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'capsule promoted after validation' });
a2aProto.getTransport().send(dm);
} catch (e) {}
}
process.stdout.write('promoted_capsule=' + id + '\n');
return;
}
var localGenes = assetStore.loadGenes();
var exists = false;
for (var k = 0; k < localGenes.length; k++) {
if (localGenes[k] && localGenes[k].type === 'Gene' && String(localGenes[k].id) === id) { exists = true; break; }
}
if (exists) {
if (emitDecisions) {
try {
var dm2 = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'reject', reason: 'local gene with same ID already exists' });
a2aProto.getTransport().send(dm2);
} catch (e) {}
}
process.stdout.write('conflict_keep_local_gene=' + id + '\n');
return;
}
assetStore.upsertGene(promoted);
if (emitDecisions) {
try {
var dm3 = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'gene promoted after safety audit' });
a2aProto.getTransport().send(dm3);
} catch (e) {}
}
process.stdout.write('promoted_gene=' + id + '\n');
}
try { main(); } catch (e) {
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
process.exit(1);
}
@@ -0,0 +1,121 @@
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..');
const LOG_FILE = path.join(REPO_ROOT, 'evolution_history_full.md');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_detailed_report.md');
function analyzeEvolution() {
if (!fs.existsSync(LOG_FILE)) {
console.error("Source file missing.");
return;
}
const content = fs.readFileSync(LOG_FILE, 'utf8');
// Split by divider
const entries = content.split('---').map(e => e.trim()).filter(e => e.length > 0);
const skillUpdates = {}; // Map<SkillName, Array<Changes>>
const generalUpdates = []; // Array<Changes>
// Regex to detect skills/paths
// e.g. `skills/feishu-card/send.js` or **Target**: `skills/git-sync`
const skillRegex = /skills\/([a-zA-Z0-9\-_]+)/;
const actionRegex = /Action:\s*([\s\S]*?)(?=\n\n|\n[A-Z]|$)/i; // Capture Action text
const statusRegex = /Status:\s*\[?([A-Z\s_]+)\]?/i;
entries.forEach(entry => {
// Extract basic info
const statusMatch = entry.match(statusRegex);
const status = statusMatch ? statusMatch[1].trim().toUpperCase() : 'UNKNOWN';
// Skip routine checks if we want a *detailed evolution* report (focus on changes)
// But user asked for "what happened", so routine scans might be boring unless they found something.
// Let's filter out "STABILITY" or "RUNNING" unless there is a clear "Mutated" or "Fixed" keyword.
const isInteresting =
entry.includes('Fixed') ||
entry.includes('Hardened') ||
entry.includes('Optimized') ||
entry.includes('Patched') ||
entry.includes('Created') ||
entry.includes('Added') ||
status === 'SUCCESS' ||
status === 'COMPLETED';
if (!isInteresting) return;
// Find associated skill
const skillMatch = entry.match(skillRegex);
let skillName = 'General / System';
if (skillMatch) {
skillName = skillMatch[1];
} else {
// Try heuristics
if (entry.toLowerCase().includes('feishu card')) skillName = 'feishu-card';
else if (entry.toLowerCase().includes('git sync')) skillName = 'git-sync';
else if (entry.toLowerCase().includes('logger')) skillName = 'interaction-logger';
else if (entry.toLowerCase().includes('evolve')) skillName = 'capability-evolver';
}
// Extract description
let description = "";
const actionMatch = entry.match(actionRegex);
if (actionMatch) {
description = actionMatch[1].trim();
} else {
// Fallback: take lines that look like bullet points or text after header
const lines = entry.split('\n');
description = lines.filter(l => l.match(/^[•\-\*]|\w/)).slice(1).join('\n').trim();
}
// Clean up description (remove duplicate "Action:" prefix if captured)
description = description.replace(/^Action:\s*/i, '');
if (!skillUpdates[skillName]) skillUpdates[skillName] = [];
// Dedup descriptions slightly (simple check)
const isDuplicate = skillUpdates[skillName].some(u => u.desc.includes(description.substring(0, 20)));
if (!isDuplicate) {
// Extract Date if possible
const dateMatch = entry.match(/\((\d{4}\/\d{1,2}\/\d{1,2}.*?)\)/);
const date = dateMatch ? dateMatch[1] : 'Unknown';
skillUpdates[skillName].push({
date,
status,
desc: description
});
}
});
// Generate Markdown
let md = "# Detailed Evolution Report (By Skill)\n\n> Comprehensive breakdown of system changes.\n\n";
// Sort skills alphabetically
const sortedSkills = Object.keys(skillUpdates).sort();
sortedSkills.forEach(skill => {
md += `## ${skill}\n`;
const updates = skillUpdates[skill];
updates.forEach(u => {
// Icon based on content
let icon = '*';
const lowerDesc = u.desc.toLowerCase();
if (lowerDesc.includes('optimiz')) icon = '[optimize]';
if (lowerDesc.includes('secur') || lowerDesc.includes('harden') || lowerDesc.includes('permission')) icon = '[security]';
if (lowerDesc.includes('fix') || lowerDesc.includes('patch')) icon = '[repair]';
if (lowerDesc.includes('creat') || lowerDesc.includes('add')) icon = '[add]';
md += `### ${icon} ${u.date}\n`;
md += `${u.desc}\n\n`;
});
md += `---\n`;
});
fs.writeFileSync(OUT_FILE, md);
console.log(`Generated report for ${sortedSkills.length} skills.`);
}
analyzeEvolution();
@@ -0,0 +1,354 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const REPO_ROOT = path.resolve(__dirname, '..');
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function rmDir(dir) {
if (!fs.existsSync(dir)) return;
fs.rmSync(dir, { recursive: true, force: true });
}
function normalizePosix(p) {
return p.split(path.sep).join('/');
}
function isUnder(child, parent) {
const rel = path.relative(parent, child);
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function listFilesRec(dir) {
const out = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const ent of entries) {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) out.push(...listFilesRec(p));
else if (ent.isFile()) out.push(p);
}
return out;
}
function globToRegex(glob) {
// Supports "*" within a single segment and "**" for any depth.
const norm = normalizePosix(glob);
const parts = norm.split('/').filter(p => p.length > 0);
const out = [];
for (const part of parts) {
if (part === '**') {
// any number of path segments
out.push('(?:.*)');
continue;
}
// Escape regex special chars, then expand "*" wildcards within segment.
const esc = part.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
out.push(esc);
}
const re = out.join('\\/');
return new RegExp(`^${re}$`);
}
function matchesAnyGlobs(relPath, globs) {
const p = normalizePosix(relPath);
for (const g of globs || []) {
const re = globToRegex(g);
if (re.test(p)) return true;
}
return false;
}
function copyFile(srcAbs, destAbs) {
ensureDir(path.dirname(destAbs));
fs.copyFileSync(srcAbs, destAbs);
}
function copyEntry(spec, outDirAbs) {
const copied = [];
// Directory glob
if (spec.includes('*')) {
const all = listFilesRec(REPO_ROOT);
const includeRe = globToRegex(spec);
for (const abs of all) {
const rel = normalizePosix(path.relative(REPO_ROOT, abs));
if (!includeRe.test(rel)) continue;
const destAbs = path.join(outDirAbs, rel);
copyFile(abs, destAbs);
copied.push(rel);
}
return copied;
}
const srcAbs = path.join(REPO_ROOT, spec);
if (!fs.existsSync(srcAbs)) return [];
const st = fs.statSync(srcAbs);
if (st.isFile()) {
const rel = normalizePosix(spec);
copyFile(srcAbs, path.join(outDirAbs, rel));
copied.push(rel);
return copied;
}
if (st.isDirectory()) {
const files = listFilesRec(srcAbs);
for (const abs of files) {
const rel = normalizePosix(path.relative(REPO_ROOT, abs));
copyFile(abs, path.join(outDirAbs, rel));
copied.push(rel);
}
}
return copied;
}
function applyRewrite(outDirAbs, rewrite) {
const rules = rewrite || {};
for (const [relFile, cfg] of Object.entries(rules)) {
const target = path.join(outDirAbs, relFile);
if (!fs.existsSync(target)) continue;
let content = fs.readFileSync(target, 'utf8');
const reps = (cfg && cfg.replace) || [];
for (const r of reps) {
const from = String(r.from || '');
const to = String(r.to || '');
if (!from) continue;
content = content.split(from).join(to);
}
fs.writeFileSync(target, content, 'utf8');
}
}
function rewritePackageJson(outDirAbs) {
const p = path.join(outDirAbs, 'package.json');
if (!fs.existsSync(p)) return;
try {
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.scripts = {
start: 'node index.js',
run: 'node index.js run',
solidify: 'node index.js solidify',
'a2a:export': 'node scripts/a2a_export.js',
'a2a:ingest': 'node scripts/a2a_ingest.js',
'a2a:promote': 'node scripts/a2a_promote.js',
};
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
} catch (e) {
// ignore
}
}
function parseSemver(v) {
const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!m) return null;
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
}
function formatSemver(x) {
return `${x.major}.${x.minor}.${x.patch}`;
}
function bumpSemver(base, bump) {
const v = parseSemver(base);
if (!v) return null;
if (bump === 'major') return `${v.major + 1}.0.0`;
if (bump === 'minor') return `${v.major}.${v.minor + 1}.0`;
if (bump === 'patch') return `${v.major}.${v.minor}.${v.patch + 1}`;
return formatSemver(v);
}
function git(cmd) {
return execSync(cmd, { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
}
function getBaseReleaseCommit() {
// Prefer last "prepare vX.Y.Z" commit; fallback to HEAD~50 range later.
try {
const hash = git('git log -n 1 --pretty=%H --grep="chore(release): prepare v"');
return hash || null;
} catch (e) {
return null;
}
}
function getCommitSubjectsSince(baseCommit) {
try {
if (!baseCommit) {
const out = git('git log -n 30 --pretty=%s');
return out ? out.split('\n').filter(Boolean) : [];
}
const out = git(`git log ${baseCommit}..HEAD --pretty=%s`);
return out ? out.split('\n').filter(Boolean) : [];
} catch (e) {
return [];
}
}
function inferBumpFromSubjects(subjects) {
const subs = (subjects || []).map(s => String(s));
const hasBreaking = subs.some(s => /\bBREAKING CHANGE\b/i.test(s) || /^[a-z]+(\(.+\))?!:/.test(s));
if (hasBreaking) return { bump: 'major', reason: 'breaking change marker in commit subject' };
const hasFeat = subs.some(s => /^feat(\(.+\))?:/i.test(s));
if (hasFeat) return { bump: 'minor', reason: 'feature commit detected (feat:)' };
const hasFix = subs.some(s => /^(fix|perf)(\(.+\))?:/i.test(s));
if (hasFix) return { bump: 'patch', reason: 'fix/perf commit detected' };
if (subs.length === 0) return { bump: 'none', reason: 'no commits since base release commit' };
return { bump: 'patch', reason: 'default to patch for non-breaking changes' };
}
function suggestVersion() {
const pkgPath = path.join(REPO_ROOT, 'package.json');
let baseVersion = null;
try {
baseVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
} catch (e) {}
const baseCommit = getBaseReleaseCommit();
const subjects = getCommitSubjectsSince(baseCommit);
const decision = inferBumpFromSubjects(subjects);
let suggested = null;
if (decision.bump === 'none') suggested = baseVersion;
else suggested = bumpSemver(baseVersion, decision.bump);
return { baseVersion, baseCommit, subjects, decision, suggestedVersion: suggested };
}
function writePrivateSemverNote(note) {
const privateDir = path.join(REPO_ROOT, 'memory');
ensureDir(privateDir);
fs.writeFileSync(path.join(privateDir, 'semver_suggestion.json'), JSON.stringify(note, null, 2) + '\n', 'utf8');
}
function writePrivateSemverPrompt(note) {
const privateDir = path.join(REPO_ROOT, 'memory');
ensureDir(privateDir);
const subjects = Array.isArray(note.subjects) ? note.subjects : [];
const semverRule = [
'MAJOR.MINOR.PATCH',
'- MAJOR: incompatible changes',
'- MINOR: backward-compatible features',
'- PATCH: backward-compatible bug fixes',
].join('\n');
const prompt = [
'You are a release versioning assistant.',
'Decide the next version bump using SemVer rules below.',
'',
semverRule,
'',
`Base version: ${note.baseVersion || '(unknown)'}`,
`Base commit: ${note.baseCommit || '(unknown)'}`,
'',
'Recent commit subjects (newest first):',
...subjects.map(s => `- ${s}`),
'',
'Output JSON only:',
'{ "bump": "major|minor|patch|none", "suggestedVersion": "x.y.z", "reason": ["..."] }',
].join('\n');
fs.writeFileSync(path.join(privateDir, 'semver_prompt.md'), prompt + '\n', 'utf8');
}
function writeDistVersion(outDirAbs, version) {
if (!version) return;
const p = path.join(outDirAbs, 'package.json');
if (!fs.existsSync(p)) return;
try {
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
pkg.version = version;
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
} catch (e) {}
}
function pruneExcluded(outDirAbs, excludeGlobs) {
const all = listFilesRec(outDirAbs);
for (const abs of all) {
const rel = normalizePosix(path.relative(outDirAbs, abs));
if (matchesAnyGlobs(rel, excludeGlobs)) {
fs.rmSync(abs, { force: true });
}
}
}
function validateNoPrivatePaths(outDirAbs) {
// Basic safeguard: forbid docs/ and memory/ in output.
const forbiddenPrefixes = ['docs/', 'memory/'];
const all = listFilesRec(outDirAbs);
for (const abs of all) {
const rel = normalizePosix(path.relative(outDirAbs, abs));
for (const pref of forbiddenPrefixes) {
if (rel.startsWith(pref)) {
throw new Error(`Build validation failed: forbidden path in output: ${rel}`);
}
}
}
}
function main() {
const manifestPath = path.join(REPO_ROOT, 'public.manifest.json');
const manifest = readJson(manifestPath);
const outDir = String(manifest.outDir || 'dist-public');
const outDirAbs = path.join(REPO_ROOT, outDir);
// SemVer suggestion (private). This does not modify the source repo version.
const semver = suggestVersion();
writePrivateSemverNote(semver);
writePrivateSemverPrompt(semver);
rmDir(outDirAbs);
ensureDir(outDirAbs);
const include = manifest.include || [];
const exclude = manifest.exclude || [];
const copied = [];
for (const spec of include) {
copied.push(...copyEntry(spec, outDirAbs));
}
pruneExcluded(outDirAbs, exclude);
applyRewrite(outDirAbs, manifest.rewrite);
rewritePackageJson(outDirAbs);
// Prefer explicit version; otherwise use suggested version.
const releaseVersion = process.env.RELEASE_VERSION || semver.suggestedVersion;
if (releaseVersion) writeDistVersion(outDirAbs, releaseVersion);
validateNoPrivatePaths(outDirAbs);
// Write build manifest for private verification (do not include in dist-public/).
const buildInfo = {
built_at: new Date().toISOString(),
outDir,
files: copied.sort(),
};
const privateDir = path.join(REPO_ROOT, 'memory');
ensureDir(privateDir);
fs.writeFileSync(path.join(privateDir, 'public_build_info.json'), JSON.stringify(buildInfo, null, 2) + '\n', 'utf8');
process.stdout.write(`Built public output at ${outDir}\n`);
if (semver && semver.suggestedVersion) {
process.stdout.write(`Suggested version: ${semver.suggestedVersion}\n`);
process.stdout.write(`SemVer decision: ${semver.decision ? semver.decision.bump : 'unknown'}\n`);
}
}
try {
main();
} catch (e) {
process.stderr.write(`${e.message}\n`);
process.exit(1);
}
@@ -0,0 +1,85 @@
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..');
const LOG_FILE = path.join(REPO_ROOT, 'memory', 'mad_dog_evolution.log');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_history.md');
function parseLog() {
if (!fs.existsSync(LOG_FILE)) {
console.log("Log file not found.");
return;
}
const content = fs.readFileSync(LOG_FILE, 'utf8');
const lines = content.split('\n');
const reports = [];
let currentTimestamp = null;
// Regex for Feishu command
// node skills/feishu-card/send.js --title "..." --color ... --text "..."
const cmdRegex = /node skills\/feishu-card\/send\.js --title "(.*?)" --color \w+ --text "(.*?)"/;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// 1. Capture Timestamp
if (line.includes('Cycle Start:')) {
// Format: Cycle Start: Sun Feb 1 19:17:44 UTC 2026
const dateStr = line.split('Cycle Start: ')[1].trim();
try {
currentTimestamp = new Date(dateStr);
} catch (e) {
currentTimestamp = null;
}
}
const match = line.match(cmdRegex);
if (match) {
const title = match[1];
let text = match[2];
// Clean up text (unescape newlines)
text = text.replace(/\\n/g, '\n').replace(/\\"/g, '"');
if (currentTimestamp) {
reports.push({
ts: currentTimestamp,
title: title,
text: text,
id: title // Cycle ID is in title
});
}
}
}
// Deduplicate by ID (keep latest timestamp?)
const uniqueReports = {};
reports.forEach(r => {
uniqueReports[r.id] = r;
});
const sortedReports = Object.values(uniqueReports).sort((a, b) => a.ts - b.ts);
let md = "# Evolution History (Extracted)\n\n";
sortedReports.forEach(r => {
// Convert to CST (UTC+8)
const cstDate = r.ts.toLocaleString("zh-CN", {
timeZone: "Asia/Shanghai",
hour12: false,
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
});
md += `### ${r.title} (${cstDate})\n`;
md += `${r.text}\n\n`;
md += `---\n\n`;
});
fs.writeFileSync(OUT_FILE, md);
console.log(`Extracted ${sortedReports.length} reports to ${OUT_FILE}`);
}
parseLog();
@@ -0,0 +1,75 @@
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
// Separator for git log parsing (something unlikely to be in commit messages)
const SEP = '|||';
const REPO_ROOT = path.resolve(__dirname, '..');
try {
// Git command:
// --reverse: Oldest to Newest (Time Sequence)
// --grep: Filter by keyword
// --format: Hash, Date (ISO), Author, Subject, Body
const cmd = `git log --reverse --grep="Evolution" --format="%H${SEP}%ai${SEP}%an${SEP}%s${SEP}%b"`;
console.log('Executing git log...');
const output = execSync(cmd, {
encoding: 'utf8',
cwd: REPO_ROOT,
maxBuffer: 1024 * 1024 * 10 // 10MB buffer just in case
});
const entries = output.split('\n').filter(line => line.trim().length > 0);
let markdown = '# Evolution History (Time Sequence)\n\n';
markdown += '> Filter: "Evolution"\n';
markdown += '> Timezone: CST (UTC+8)\n\n';
let count = 0;
entries.forEach(entry => {
const parts = entry.split(SEP);
if (parts.length < 4) return;
const hash = parts[0];
const dateStr = parts[1];
const author = parts[2];
const subject = parts[3];
const body = parts[4] || '';
// Parse Date and Convert to UTC+8
const date = new Date(dateStr);
// Add 8 hours (28800000 ms) to UTC timestamp to shift it
// Then formatting it as ISO will look like UTC but represent CST values
const cstDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
// Format: YYYY-MM-DD HH:mm:ss
const timeStr = cstDate.toISOString().replace('T', ' ').substring(0, 19);
markdown += `## ${timeStr}\n`;
markdown += `- Commit: \`${hash.substring(0, 7)}\`\n`;
markdown += `- Subject: ${subject}\n`;
if (body.trim()) {
// Indent body for better readability
const formattedBody = body.trim().split('\n').map(l => `> ${l}`).join('\n');
markdown += `- Details:\n${formattedBody}\n`;
}
markdown += '\n';
count++;
});
const outDir = path.join(REPO_ROOT, 'memory');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const outPath = path.join(outDir, 'evolution_history.md');
fs.writeFileSync(outPath, markdown);
console.log(`Successfully generated report with ${count} entries.`);
console.log(`Saved to: ${outPath}`);
} catch (e) {
console.error('Error generating history:', e.message);
process.exit(1);
}
@@ -0,0 +1,96 @@
const fs = require('fs');
const { appendEventJsonl } = require('../src/gep/assetStore');
function readStdin() {
try {
return fs.readFileSync(0, 'utf8');
} catch {
return '';
}
}
function readTextIfExists(p) {
try {
if (!p) return '';
if (!fs.existsSync(p)) return '';
return fs.readFileSync(p, 'utf8');
} catch {
return '';
}
}
function parseInput(text) {
const raw = String(text || '').trim();
if (!raw) return [];
// Accept JSON array or single JSON.
try {
const maybe = JSON.parse(raw);
if (Array.isArray(maybe)) return maybe;
if (maybe && typeof maybe === 'object') return [maybe];
} catch (e) {}
// Fallback: JSONL.
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
const out = [];
for (const line of lines) {
try {
const obj = JSON.parse(line);
out.push(obj);
} catch (e) {}
}
return out;
}
function isValidEvolutionEvent(ev) {
if (!ev || ev.type !== 'EvolutionEvent') return false;
if (!ev.id || typeof ev.id !== 'string') return false;
// parent may be null or string
if (!(ev.parent === null || typeof ev.parent === 'string')) return false;
if (!ev.intent || typeof ev.intent !== 'string') return false;
if (!Array.isArray(ev.signals)) return false;
if (!Array.isArray(ev.genes_used)) return false;
// GEP v1.4: mutation + personality are mandatory evolution dimensions
if (!ev.mutation_id || typeof ev.mutation_id !== 'string') return false;
if (!ev.personality_state || typeof ev.personality_state !== 'object') return false;
if (ev.personality_state.type !== 'PersonalityState') return false;
for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
const v = Number(ev.personality_state[k]);
if (!Number.isFinite(v) || v < 0 || v > 1) return false;
}
if (!ev.blast_radius || typeof ev.blast_radius !== 'object') return false;
if (!Number.isFinite(Number(ev.blast_radius.files))) return false;
if (!Number.isFinite(Number(ev.blast_radius.lines))) return false;
if (!ev.outcome || typeof ev.outcome !== 'object') return false;
if (!ev.outcome.status || typeof ev.outcome.status !== 'string') return false;
const score = Number(ev.outcome.score);
if (!Number.isFinite(score) || score < 0 || score > 1) return false;
// capsule_id is optional, but if present must be string or null.
if (!('capsule_id' in ev)) return true;
return ev.capsule_id === null || typeof ev.capsule_id === 'string';
}
function main() {
const args = process.argv.slice(2);
const inputPath = args.find(a => a && !a.startsWith('--')) || '';
const text = inputPath ? readTextIfExists(inputPath) : readStdin();
const items = parseInput(text);
let appended = 0;
for (const it of items) {
if (!isValidEvolutionEvent(it)) continue;
appendEventJsonl(it);
appended += 1;
}
process.stdout.write(`appended=${appended}\n`);
}
try {
main();
} catch (e) {
process.stderr.write(`${e && e.message ? e.message : String(e)}\n`);
process.exit(1);
}
@@ -0,0 +1,234 @@
const fs = require('fs');
const path = require('path');
const { getRepoRoot, getMemoryDir, getGepAssetsDir } = require('../src/gep/paths');
const { normalizePersonalityState, personalityKey, defaultPersonalityState } = require('../src/gep/personality');
function readJsonIfExists(p, fallback) {
try {
if (!fs.existsSync(p)) return fallback;
const raw = fs.readFileSync(p, 'utf8');
if (!raw.trim()) return fallback;
return JSON.parse(raw);
} catch {
return fallback;
}
}
function readJsonlIfExists(p, limitLines = 5000) {
try {
if (!fs.existsSync(p)) return [];
const raw = fs.readFileSync(p, 'utf8');
const lines = raw
.split('\n')
.map(l => l.trim())
.filter(Boolean);
const recent = lines.slice(Math.max(0, lines.length - limitLines));
return recent
.map(l => {
try {
return JSON.parse(l);
} catch {
return null;
}
})
.filter(Boolean);
} catch {
return [];
}
}
function clamp01(x) {
const n = Number(x);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(1, n));
}
function pct(x) {
const n = Number(x);
if (!Number.isFinite(n)) return '0.0%';
return `${(n * 100).toFixed(1)}%`;
}
function pad(s, n) {
const str = String(s == null ? '' : s);
if (str.length >= n) return str.slice(0, n);
return str + ' '.repeat(n - str.length);
}
function scoreFromCounts(success, fail, avgScore) {
const succ = Number(success) || 0;
const fl = Number(fail) || 0;
const total = succ + fl;
const p = (succ + 1) / (total + 2); // Laplace smoothing
const sampleWeight = Math.min(1, total / 8);
const q = avgScore == null ? 0.5 : clamp01(avgScore);
return p * 0.75 + q * 0.25 * sampleWeight;
}
function aggregateFromEvents(events) {
const map = new Map();
for (const ev of Array.isArray(events) ? events : []) {
if (!ev || ev.type !== 'EvolutionEvent') continue;
const ps = ev.personality_state && typeof ev.personality_state === 'object' ? ev.personality_state : null;
if (!ps) continue;
const key = personalityKey(normalizePersonalityState(ps));
const cur = map.get(key) || {
key,
success: 0,
fail: 0,
n: 0,
avg_score: 0.5,
last_event_id: null,
last_at: null,
mutation: { repair: 0, optimize: 0, innovate: 0 },
mutation_success: { repair: 0, optimize: 0, innovate: 0 },
};
const st = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
if (st === 'success') cur.success += 1;
else if (st === 'failed') cur.fail += 1;
const sc = ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? clamp01(Number(ev.outcome.score)) : null;
if (sc != null) {
cur.n += 1;
cur.avg_score = cur.avg_score + (sc - cur.avg_score) / cur.n;
}
const cat = ev.intent ? String(ev.intent) : null;
if (cat && cur.mutation[cat] != null) {
cur.mutation[cat] += 1;
if (st === 'success') cur.mutation_success[cat] += 1;
}
cur.last_event_id = ev.id || cur.last_event_id;
const at = ev.meta && ev.meta.at ? String(ev.meta.at) : null;
cur.last_at = at || cur.last_at;
map.set(key, cur);
}
return Array.from(map.values());
}
function main() {
const repoRoot = getRepoRoot();
const memoryDir = getMemoryDir();
const assetsDir = getGepAssetsDir();
const personalityPath = path.join(memoryDir, 'personality_state.json');
const model = readJsonIfExists(personalityPath, null);
const current = model && model.current ? normalizePersonalityState(model.current) : defaultPersonalityState();
const currentKey = personalityKey(current);
const eventsPath = path.join(assetsDir, 'events.jsonl');
const events = readJsonlIfExists(eventsPath, 10000);
const evs = events.filter(e => e && e.type === 'EvolutionEvent');
const agg = aggregateFromEvents(evs);
// Prefer model.stats if present, but still show event-derived aggregation (ground truth).
const stats = model && model.stats && typeof model.stats === 'object' ? model.stats : {};
const statRows = Object.entries(stats).map(([key, e]) => {
const entry = e && typeof e === 'object' ? e : {};
const success = Number(entry.success) || 0;
const fail = Number(entry.fail) || 0;
const total = success + fail;
const avg = Number.isFinite(Number(entry.avg_score)) ? clamp01(Number(entry.avg_score)) : null;
const score = scoreFromCounts(success, fail, avg);
return { key, success, fail, total, avg_score: avg, score, updated_at: entry.updated_at || null, source: 'model' };
});
const evRows = agg.map(e => {
const success = Number(e.success) || 0;
const fail = Number(e.fail) || 0;
const total = success + fail;
const avg = Number.isFinite(Number(e.avg_score)) ? clamp01(Number(e.avg_score)) : null;
const score = scoreFromCounts(success, fail, avg);
return { key: e.key, success, fail, total, avg_score: avg, score, updated_at: e.last_at || null, source: 'events', _ev: e };
});
// Merge rows by key (events take precedence for total/success/fail; model provides updated_at if events missing).
const byKey = new Map();
for (const r of [...statRows, ...evRows]) {
const prev = byKey.get(r.key);
if (!prev) {
byKey.set(r.key, r);
continue;
}
// Prefer events for counts and avg_score
if (r.source === 'events') byKey.set(r.key, { ...prev, ...r });
else byKey.set(r.key, { ...r, ...prev });
}
const merged = Array.from(byKey.values()).sort((a, b) => b.score - a.score);
process.stdout.write(`Repo: ${repoRoot}\n`);
process.stdout.write(`MemoryDir: ${memoryDir}\n`);
process.stdout.write(`AssetsDir: ${assetsDir}\n\n`);
process.stdout.write(`[Current Personality]\n`);
process.stdout.write(`${currentKey}\n`);
process.stdout.write(`${JSON.stringify(current, null, 2)}\n\n`);
process.stdout.write(`[Personality Stats] (ranked by score)\n`);
if (merged.length === 0) {
process.stdout.write('(no stats yet; run a few cycles and solidify)\n');
return;
}
const header =
pad('rank', 5) +
pad('total', 8) +
pad('succ', 8) +
pad('fail', 8) +
pad('succ_rate', 11) +
pad('avg', 7) +
pad('score', 8) +
'key';
process.stdout.write(header + '\n');
process.stdout.write('-'.repeat(Math.min(140, header.length + 40)) + '\n');
const topN = Math.min(25, merged.length);
for (let i = 0; i < topN; i++) {
const r = merged[i];
const succ = Number(r.success) || 0;
const fail = Number(r.fail) || 0;
const total = Number(r.total) || succ + fail;
const succRate = total > 0 ? succ / total : 0;
const avg = r.avg_score == null ? '-' : Number(r.avg_score).toFixed(2);
const line =
pad(String(i + 1), 5) +
pad(String(total), 8) +
pad(String(succ), 8) +
pad(String(fail), 8) +
pad(pct(succRate), 11) +
pad(String(avg), 7) +
pad(Number(r.score).toFixed(3), 8) +
String(r.key);
process.stdout.write(line + '\n');
if (r._ev) {
const ev = r._ev;
const ms = ev.mutation || {};
const mSucc = ev.mutation_success || {};
const parts = [];
for (const cat of ['repair', 'optimize', 'innovate']) {
const n = Number(ms[cat]) || 0;
if (n <= 0) continue;
const s = Number(mSucc[cat]) || 0;
parts.push(`${cat}:${s}/${n}`);
}
if (parts.length) process.stdout.write(` mutation_success: ${parts.join(' | ')}\n`);
}
}
process.stdout.write('\n');
process.stdout.write(`[Notes]\n`);
process.stdout.write(`- score is a smoothed composite of success_rate + avg_score (sample-weighted)\n`);
process.stdout.write(`- current_key appears in the ranking once enough data accumulates\n`);
}
try {
main();
} catch (e) {
process.stderr.write((e && e.message) || String(e));
process.stderr.write('\n');
process.exit(1);
}
@@ -0,0 +1,147 @@
const fs = require('fs');
const path = require('path');
const REPO_ROOT = path.resolve(__dirname, '..');
const IN_FILE = path.join(REPO_ROOT, 'evolution_history_full.md');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_human_summary.md');
function generateHumanReport() {
if (!fs.existsSync(IN_FILE)) return console.error("No input file");
const content = fs.readFileSync(IN_FILE, 'utf8');
const entries = content.split('---').map(e => e.trim()).filter(e => e.length > 0);
const categories = {
'Security & Stability': [],
'Performance & Optimization': [],
'Tooling & Features': [],
'Documentation & Process': []
};
const componentMap = {}; // Component -> Change List
entries.forEach(entry => {
// Extract basic info
const lines = entry.split('\n');
const header = lines[0]; // ### Title (Date)
const body = lines.slice(1).join('\n');
const dateMatch = header.match(/\((.*?)\)/);
const dateStr = dateMatch ? dateMatch[1] : '';
const time = dateStr.split(' ')[1] || ''; // HH:mm:ss
// Classify
let category = 'Tooling & Features';
let component = 'System';
let summary = '';
const lowerBody = body.toLowerCase();
// Detect Component
if (lowerBody.includes('feishu-card')) component = 'feishu-card';
else if (lowerBody.includes('feishu-sticker')) component = 'feishu-sticker';
else if (lowerBody.includes('git-sync')) component = 'git-sync';
else if (lowerBody.includes('capability-evolver') || lowerBody.includes('evolve.js')) component = 'capability-evolver';
else if (lowerBody.includes('interaction-logger')) component = 'interaction-logger';
else if (lowerBody.includes('chat-to-image')) component = 'chat-to-image';
else if (lowerBody.includes('safe_publish')) component = 'capability-evolver';
// Detect Category
if (lowerBody.includes('security') || lowerBody.includes('permission') || lowerBody.includes('auth') || lowerBody.includes('harden')) {
category = 'Security & Stability';
} else if (lowerBody.includes('optimiz') || lowerBody.includes('performance') || lowerBody.includes('memory') || lowerBody.includes('fast')) {
category = 'Performance & Optimization';
} else if (lowerBody.includes('doc') || lowerBody.includes('readme')) {
category = 'Documentation & Process';
}
// Extract Human Summary (First meaningful line that isn't Status/Action/Date)
const summaryLines = lines.filter(l =>
!l.startsWith('###') &&
!l.startsWith('Status:') &&
!l.startsWith('Action:') &&
l.trim().length > 10
);
if (summaryLines.length > 0) {
// Clean up the line
summary = summaryLines[0]
.replace(/^-\s*/, '') // Remove bullets
.replace(/\*\*/g, '') // Remove bold
.replace(/`/, '')
.trim();
// Deduplicate
const key = `${component}:${summary.substring(0, 20)}`;
const exists = categories[category].some(i => i.key === key);
if (!exists && !summary.includes("Stability Scan OK") && !summary.includes("Workspace Sync")) {
categories[category].push({ time, component, summary, key });
if (!componentMap[component]) componentMap[component] = [];
componentMap[component].push(summary);
}
}
});
// --- Generate Markdown ---
const today = new Date().toISOString().slice(0, 10);
let md = `# Evolution Summary: The Day in Review (${today})\n\n`;
md += `> Overview: Grouped summary of changes extracted from evolution history.\n\n`;
// Section 1: By Theme (Evolution Direction)
md += `## 1. Evolution Direction\n`;
for (const [cat, items] of Object.entries(categories)) {
if (items.length === 0) continue;
md += `### ${cat}\n`;
// Group by component within theme
const compGroup = {};
items.forEach(i => {
if (!compGroup[i.component]) compGroup[i.component] = [];
compGroup[i.component].push(i.summary);
});
for (const [comp, sums] of Object.entries(compGroup)) {
// Unique summaries only
const uniqueSums = [...new Set(sums)];
uniqueSums.forEach(s => {
md += `- **${comp}**: ${s}\n`;
});
}
md += `\n`;
}
// Section 2: By Timeline (High Level)
md += `## 2. Timeline of Critical Events\n`;
// Flatten and sort all items by time
const allItems = [];
Object.values(categories).forEach(list => allItems.push(...list));
allItems.sort((a, b) => a.time.localeCompare(b.time));
// Filter for "Critical" keywords
const criticalItems = allItems.filter(i =>
i.summary.toLowerCase().includes('fix') ||
i.summary.toLowerCase().includes('patch') ||
i.summary.toLowerCase().includes('create') ||
i.summary.toLowerCase().includes('optimiz')
);
criticalItems.forEach(i => {
md += `- \`${i.time}\` (${i.component}): ${i.summary}\n`;
});
// Section 3: Package Adjustments
md += `\n## 3. Package & Documentation Adjustments\n`;
const comps = Object.keys(componentMap).sort();
comps.forEach(comp => {
const count = new Set(componentMap[comp]).size;
md += `- **${comp}**: Received ${count} significant updates.\n`;
});
fs.writeFileSync(OUT_FILE, md);
console.log("Human report generated.");
}
generateHumanReport();
@@ -0,0 +1,614 @@
const { execSync, spawnSync } = require('child_process');
const fs = require('fs');
const https = require('https');
const os = require('os');
const path = require('path');
function run(cmd, opts = {}) {
const { dryRun = false } = opts;
if (dryRun) {
process.stdout.write(`[dry-run] ${cmd}\n`);
return '';
}
return execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}
function hasCommand(cmd) {
try {
if (process.platform === 'win32') {
const res = spawnSync('where', [cmd], { stdio: 'ignore' });
return res.status === 0;
}
const res = spawnSync('which', [cmd], { stdio: 'ignore' });
return res.status === 0;
} catch (e) {
return false;
}
}
function resolveGhExecutable() {
if (hasCommand('gh')) return 'gh';
const candidates = [
'C:\\Program Files\\GitHub CLI\\gh.exe',
'C:\\Program Files (x86)\\GitHub CLI\\gh.exe',
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p;
} catch (e) {
// ignore
}
}
return null;
}
function resolveClawhubExecutable() {
// On Windows, Node spawn/spawnSync does not always resolve PATHEXT the same way as shells.
// Prefer the explicit .cmd shim when available to avoid false "not logged in" detection.
if (process.platform === 'win32') {
if (hasCommand('clawhub.cmd')) return 'clawhub.cmd';
if (hasCommand('clawhub')) return 'clawhub';
} else {
if (hasCommand('clawhub')) return 'clawhub';
}
// Common npm global bin location on Windows.
const candidates = [
'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.cmd',
'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.exe',
'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\clawhub.ps1',
];
for (const p of candidates) {
try {
if (fs.existsSync(p)) return p;
} catch (e) {
// ignore
}
}
return null;
}
function canUseClawhub() {
const exe = resolveClawhubExecutable();
if (!exe) return { ok: false, reason: 'clawhub CLI not found (install: npm i -g clawhub)' };
return { ok: true, exe };
}
function isClawhubLoggedIn() {
const exe = resolveClawhubExecutable();
if (!exe) return false;
try {
const res = spawnClawhub(exe, ['whoami'], { stdio: 'ignore' });
return res.status === 0;
} catch (e) {
return false;
}
}
function spawnClawhub(exe, args, options) {
// On Windows, directly spawning a .cmd can be flaky; using cmd.exe preserves argument parsing.
// (Using shell:true can break clap/commander style option parsing for some CLIs.)
const opts = options || {};
if (process.platform === 'win32' && typeof exe === 'string') {
const lower = exe.toLowerCase();
if (lower.endsWith('.cmd')) {
return spawnSync('cmd.exe', ['/d', '/s', '/c', exe, ...(args || [])], opts);
}
}
return spawnSync(exe, args || [], opts);
}
function publishToClawhub({ skillDir, slug, name, version, changelog, tags, dryRun }) {
const ok = canUseClawhub();
if (!ok.ok) throw new Error(ok.reason);
// Idempotency: if this version already exists on ClawHub, skip publishing.
try {
const inspect = spawnClawhub(ok.exe, ['inspect', slug, '--version', version], { stdio: 'ignore' });
if (inspect.status === 0) {
process.stdout.write(`ClawHub already has ${slug}@${version}. Skipping.\n`);
return;
}
} catch (e) {
// ignore inspect failures; publish will surface errors if needed
}
if (!dryRun && !isClawhubLoggedIn()) {
throw new Error('Not logged in to ClawHub. Run: clawhub login');
}
const args = ['publish', skillDir, '--slug', slug, '--name', name, '--version', version];
if (changelog) args.push('--changelog', changelog);
if (tags) args.push('--tags', tags);
if (dryRun) {
process.stdout.write(`[dry-run] ${ok.exe} ${args.map(a => (/\s/.test(a) ? `"${a}"` : a)).join(' ')}\n`);
return;
}
// Capture output to handle "version already exists" idempotently.
const res = spawnClawhub(ok.exe, args, { encoding: 'utf8' });
const out = `${res.stdout || ''}\n${res.stderr || ''}`.trim();
if (res.status === 0) {
if (out) process.stdout.write(out + '\n');
return;
}
// Some clawhub deployments do not support reliable "inspect" by slug.
// Treat "Version already exists" as success to make publishing idempotent.
if (/version already exists/i.test(out)) {
process.stdout.write(`ClawHub already has ${slug}@${version}. Skipping.\n`);
return;
}
if (out) process.stderr.write(out + '\n');
throw new Error(`clawhub publish failed for slug ${slug}`);
}
function requireEnv(name, value) {
if (!value) {
throw new Error(`Missing required env var: ${name}`);
}
}
function ensureClean(dryRun) {
const status = run('git status --porcelain', { dryRun });
if (!dryRun && status) {
throw new Error('Working tree is not clean. Commit or stash before publishing.');
}
}
function ensureBranch(expected, dryRun) {
const current = run('git rev-parse --abbrev-ref HEAD', { dryRun }) || expected;
if (!dryRun && current !== expected) {
throw new Error(`Current branch is ${current}. Expected ${expected}.`);
}
}
function ensureRemote(remote, dryRun) {
try {
run(`git remote get-url ${remote}`, { dryRun });
} catch (e) {
throw new Error(`Remote "${remote}" not found. Add it manually before running this script.`);
}
}
function ensureTagAvailable(tag, dryRun) {
if (!tag) return;
const exists = run(`git tag --list ${tag}`, { dryRun });
if (!dryRun && exists) {
throw new Error(`Tag ${tag} already exists.`);
}
}
function ensureDir(dir, dryRun) {
if (dryRun) return;
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function rmDir(dir, dryRun) {
if (dryRun) return;
if (!fs.existsSync(dir)) return;
fs.rmSync(dir, { recursive: true, force: true });
}
function copyDir(src, dest, dryRun) {
if (dryRun) return;
if (!fs.existsSync(src)) throw new Error(`Missing build output dir: ${src}`);
ensureDir(dest, dryRun);
const entries = fs.readdirSync(src, { withFileTypes: true });
for (const ent of entries) {
const s = path.join(src, ent.name);
const d = path.join(dest, ent.name);
if (ent.isDirectory()) copyDir(s, d, dryRun);
else if (ent.isFile()) {
ensureDir(path.dirname(d), dryRun);
fs.copyFileSync(s, d);
}
}
}
function createReleaseWithGh({ repo, tag, title, notes, notesFile, dryRun }) {
if (!repo || !tag) return;
const ghExe = resolveGhExecutable();
if (!ghExe) {
throw new Error('gh CLI not found. Install GitHub CLI or provide a GitHub token for API-based release creation.');
}
const args = ['release', 'create', tag, '--repo', repo];
if (title) args.push('-t', title);
if (notesFile) args.push('-F', notesFile);
else if (notes) args.push('-n', notes);
else args.push('-n', 'Release created by publish script.');
if (dryRun) {
process.stdout.write(`[dry-run] ${ghExe} ${args.join(' ')}\n`);
return;
}
const res = spawnSync(ghExe, args, { stdio: 'inherit' });
if (res.status !== 0) {
throw new Error('gh release create failed');
}
}
function canUseGhForRelease() {
const ghExe = resolveGhExecutable();
if (!ghExe) return { ok: false, reason: 'gh CLI not found' };
try {
// Non-interactive check: returns 0 when authenticated.
const res = spawnSync(ghExe, ['auth', 'status', '-h', 'github.com'], { stdio: 'ignore' });
if (res.status === 0) return { ok: true };
return { ok: false, reason: 'gh not authenticated (run: gh auth login)' };
} catch (e) {
return { ok: false, reason: 'failed to check gh auth status' };
}
}
function getGithubToken() {
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_PAT || '';
}
function readReleaseNotes(notes, notesFile) {
if (notesFile) {
try {
return fs.readFileSync(notesFile, 'utf8');
} catch (e) {
throw new Error(`Failed to read RELEASE_NOTES_FILE: ${notesFile}`);
}
}
if (notes) return String(notes);
return '';
}
function githubRequestJson({ method, repo, apiPath, token, body, dryRun }) {
if (dryRun) {
process.stdout.write(`[dry-run] GitHub API ${method} ${repo} ${apiPath}\n`);
return Promise.resolve({ status: 200, json: null });
}
const data = body ? Buffer.from(JSON.stringify(body)) : null;
const opts = {
method,
hostname: 'api.github.com',
path: `/repos/${repo}${apiPath}`,
headers: {
'User-Agent': 'evolver-publish-script',
Accept: 'application/vnd.github+json',
...(token ? { Authorization: `token ${token}` } : {}),
...(data ? { 'Content-Type': 'application/json', 'Content-Length': String(data.length) } : {}),
},
};
return new Promise((resolve, reject) => {
const req = https.request(opts, res => {
let raw = '';
res.setEncoding('utf8');
res.on('data', chunk => (raw += chunk));
res.on('end', () => {
let json = null;
try {
json = raw ? JSON.parse(raw) : null;
} catch (e) {
json = null;
}
resolve({ status: res.statusCode || 0, json, raw });
});
});
req.on('error', reject);
if (data) req.write(data);
req.end();
});
}
async function ensureReleaseWithApi({ repo, tag, title, notes, notesFile, dryRun }) {
if (!repo || !tag) return;
const token = getGithubToken();
if (!dryRun) {
requireEnv('GITHUB_TOKEN (or GH_TOKEN/GITHUB_PAT)', token);
}
// If release already exists, skip.
const existing = await githubRequestJson({
method: 'GET',
repo,
apiPath: `/releases/tags/${encodeURIComponent(tag)}`,
token,
dryRun,
});
if (!dryRun && existing.status === 200) {
process.stdout.write(`Release already exists for tag ${tag}. Skipping.\n`);
return;
}
const bodyText = readReleaseNotes(notes, notesFile) || 'Release created by publish script.';
const payload = {
tag_name: tag,
name: title || tag,
body: bodyText,
draft: false,
prerelease: false,
};
const created = await githubRequestJson({
method: 'POST',
repo,
apiPath: '/releases',
token,
body: payload,
dryRun,
});
if (!dryRun && (created.status < 200 || created.status >= 300)) {
const msg = (created.json && created.json.message) || created.raw || 'Unknown error';
throw new Error(`Failed to create GitHub Release (${created.status}): ${msg}`);
}
process.stdout.write(`Created GitHub Release for tag ${tag}\n`);
}
// Collect unique external contributors from private repo commits since the last release.
// Returns an array of "Name <email>" strings suitable for Co-authored-by trailers.
// GitHub counts Co-authored-by toward the Contributors graph.
function getContributorsSinceLastRelease() {
const EXCLUDED = new Set([
'evolver-publish@local',
'evolver@local',
'openclaw@users.noreply.github.com',
]);
try {
let baseCommit = '';
try {
baseCommit = execSync(
'git log -n 1 --pretty=%H --grep="chore(release): prepare v"',
{ encoding: 'utf8', cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] }
).trim();
} catch (_) {}
const range = baseCommit ? `${baseCommit}..HEAD` : '-30';
const raw = execSync(
`git log ${range} --pretty="%aN <%aE>"`,
{ encoding: 'utf8', cwd: process.cwd(), stdio: ['ignore', 'pipe', 'ignore'] }
).trim();
if (!raw) return [];
const seen = new Set();
const contributors = [];
for (const line of raw.split('\n')) {
const trimmed = line.trim();
if (!trimmed) continue;
const emailMatch = trimmed.match(/<([^>]+)>/);
const email = emailMatch ? emailMatch[1].toLowerCase() : '';
if (EXCLUDED.has(email)) continue;
if (seen.has(email)) continue;
seen.add(email);
contributors.push(trimmed);
}
return contributors;
} catch (_) {
return [];
}
}
function main() {
const dryRun = String(process.env.DRY_RUN || '').toLowerCase() === 'true';
const sourceBranch = process.env.SOURCE_BRANCH || 'main';
const publicRemote = process.env.PUBLIC_REMOTE || 'public';
const publicBranch = process.env.PUBLIC_BRANCH || 'main';
const publicRepo = process.env.PUBLIC_REPO || '';
const outDir = process.env.PUBLIC_OUT_DIR || 'dist-public';
const useBuildOutput = String(process.env.PUBLIC_USE_BUILD_OUTPUT || 'true').toLowerCase() === 'true';
const releaseOnly = String(process.env.PUBLIC_RELEASE_ONLY || '').toLowerCase() === 'true';
const clawhubSkip = String(process.env.CLAWHUB_SKIP || '').toLowerCase() === 'true';
const clawhubPublish = String(process.env.CLAWHUB_PUBLISH || '').toLowerCase() === 'false' ? false : !clawhubSkip;
// Workaround for registry redirect/auth issues: default to the www endpoint.
const clawhubRegistry = process.env.CLAWHUB_REGISTRY || 'https://www.clawhub.ai';
// If publishing build output, require a repo URL or GH repo slug for cloning.
if (useBuildOutput) {
requireEnv('PUBLIC_REPO', publicRepo);
}
let releaseTag = process.env.RELEASE_TAG || '';
let releaseTitle = process.env.RELEASE_TITLE || '';
const releaseNotes = process.env.RELEASE_NOTES || '';
const releaseNotesFile = process.env.RELEASE_NOTES_FILE || '';
const releaseSkip = String(process.env.RELEASE_SKIP || '').toLowerCase() === 'true';
// Default behavior: create release unless explicitly skipped.
// Backward compatibility: RELEASE_CREATE=true forces creation.
// Note: RELEASE_CREATE=false is ignored; use RELEASE_SKIP=true instead.
const releaseCreate = String(process.env.RELEASE_CREATE || '').toLowerCase() === 'true' ? true : !releaseSkip;
const releaseUseGh = String(process.env.RELEASE_USE_GH || '').toLowerCase() === 'true';
// If not provided, infer from build output package.json version.
if (!releaseTag && useBuildOutput) {
try {
const builtPkg = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), outDir, 'package.json'), 'utf8'));
if (builtPkg && builtPkg.version) releaseTag = `v${builtPkg.version}`;
if (!releaseTitle && releaseTag) releaseTitle = releaseTag;
} catch (e) {}
}
const releaseVersion = String(releaseTag || '').startsWith('v') ? String(releaseTag).slice(1) : '';
// Fail fast on missing release prerequisites to avoid half-publishing.
// Strategy:
// - If RELEASE_USE_GH=true: require gh + auth
// - Else: prefer gh+auth; fallback to API token; else fail
let releaseMode = 'none';
if (releaseCreate && releaseTag) {
if (releaseUseGh) {
const ghOk = canUseGhForRelease();
if (!dryRun && !ghOk.ok) {
throw new Error(`Cannot create release via gh: ${ghOk.reason}`);
}
releaseMode = 'gh';
} else {
const ghOk = canUseGhForRelease();
if (ghOk.ok) {
releaseMode = 'gh';
} else {
const token = getGithubToken();
if (!dryRun && !token) {
throw new Error(
'Cannot create GitHub Release: neither gh (installed+authenticated) nor GITHUB_TOKEN (or GH_TOKEN/GITHUB_PAT) is available.'
);
}
releaseMode = 'api';
}
}
}
// In release-only mode we do not push code or tags, only create a GitHub Release for an existing tag.
if (!releaseOnly) {
ensureClean(dryRun);
ensureBranch(sourceBranch, dryRun);
ensureTagAvailable(releaseTag, dryRun);
} else {
requireEnv('RELEASE_TAG', releaseTag);
}
if (!releaseOnly) {
if (!useBuildOutput) {
ensureRemote(publicRemote, dryRun);
run(`git push ${publicRemote} ${sourceBranch}:${publicBranch}`, { dryRun });
} else {
const tmpBase = path.join(os.tmpdir(), 'evolver-public-publish');
const tmpRepoDir = path.join(tmpBase, `repo_${Date.now()}`);
const buildAbs = path.resolve(process.cwd(), outDir);
rmDir(tmpRepoDir, dryRun);
ensureDir(tmpRepoDir, dryRun);
run(`git clone --depth 1 https://github.com/${publicRepo}.git "${tmpRepoDir}"`, { dryRun });
run(`git -C "${tmpRepoDir}" checkout -B ${publicBranch}`, { dryRun });
// Replace repo contents with build output (except .git)
if (!dryRun) {
const entries = fs.readdirSync(tmpRepoDir, { withFileTypes: true });
for (const ent of entries) {
if (ent.name === '.git') continue;
fs.rmSync(path.join(tmpRepoDir, ent.name), { recursive: true, force: true });
}
}
copyDir(buildAbs, tmpRepoDir, dryRun);
run(`git -C "${tmpRepoDir}" add -A`, { dryRun });
const msg = releaseTag ? `Release ${releaseTag}` : `Publish build output`;
// If build output is identical to current public branch, skip commit/push.
const pending = run(`git -C "${tmpRepoDir}" status --porcelain`, { dryRun });
if (!dryRun && !pending) {
process.stdout.write('Public repo already matches build output. Skipping commit/push.\n');
} else {
const contributors = getContributorsSinceLastRelease();
let commitMsg = msg.replace(/"/g, '\\"');
if (contributors.length > 0) {
const trailers = contributors.map(c => `Co-authored-by: ${c}`).join('\n');
commitMsg += `\n\n${trailers.replace(/"/g, '\\"')}`;
process.stdout.write(`Including ${contributors.length} contributor(s) in publish commit.\n`);
}
run(
`git -C "${tmpRepoDir}" -c user.name="evolver-publish" -c user.email="evolver-publish@local" commit -m "${commitMsg}"`,
{ dryRun }
);
run(`git -C "${tmpRepoDir}" push origin ${publicBranch}`, { dryRun });
}
if (releaseTag) {
const tagMsg = releaseTitle || `Release ${releaseTag}`;
// If tag already exists in the public repo, do not recreate it.
try {
run(`git -C "${tmpRepoDir}" fetch --tags`, { dryRun });
const exists = run(`git -C "${tmpRepoDir}" tag --list ${releaseTag}`, { dryRun });
if (!dryRun && exists) {
process.stdout.write(`Tag ${releaseTag} already exists in public repo. Skipping tag creation.\n`);
} else {
run(`git -C "${tmpRepoDir}" tag -a ${releaseTag} -m "${tagMsg.replace(/"/g, '\\"')}"`, { dryRun });
run(`git -C "${tmpRepoDir}" push origin ${releaseTag}`, { dryRun });
}
} catch (e) {
// If tag operations fail, rethrow to avoid publishing a release without a tag.
throw e;
}
}
}
if (releaseTag) {
if (!useBuildOutput) {
const msg = releaseTitle || `Release ${releaseTag}`;
run(`git tag -a ${releaseTag} -m "${msg.replace(/"/g, '\\"')}"`, { dryRun });
run(`git push ${publicRemote} ${releaseTag}`, { dryRun });
}
}
}
if (releaseCreate) {
if (releaseMode === 'gh') {
createReleaseWithGh({
repo: publicRepo,
tag: releaseTag,
title: releaseTitle,
notes: releaseNotes,
notesFile: releaseNotesFile,
dryRun,
});
} else if (releaseMode === 'api') {
return ensureReleaseWithApi({
repo: publicRepo,
tag: releaseTag,
title: releaseTitle,
notes: releaseNotes,
notesFile: releaseNotesFile,
dryRun,
});
}
}
// Publish to ClawHub after GitHub release succeeds (default enabled).
if (clawhubPublish && releaseVersion) {
process.env.CLAWHUB_REGISTRY = clawhubRegistry;
const skillDir = useBuildOutput ? path.resolve(process.cwd(), outDir) : process.cwd();
const changelog = releaseTitle ? `GitHub Release ${releaseTitle}` : `GitHub Release ${releaseTag}`;
publishToClawhub({
skillDir,
slug: 'evolver',
name: 'Evolver',
version: releaseVersion,
changelog,
tags: 'latest',
dryRun,
});
publishToClawhub({
skillDir,
slug: 'capability-evolver',
name: 'Capability Evolver',
version: releaseVersion,
changelog,
tags: 'latest',
dryRun,
});
}
}
try {
const maybePromise = main();
if (maybePromise && typeof maybePromise.then === 'function') {
maybePromise.catch(e => {
process.stderr.write(`${e.message}\n`);
process.exit(1);
});
}
} catch (e) {
process.stderr.write(`${e.message}\n`);
process.exit(1);
}
@@ -0,0 +1,61 @@
#!/usr/bin/env node
/* eslint-disable no-console */
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
function exists(p) {
try {
return fs.existsSync(p);
} catch (e) {
return false;
}
}
function sleepMs(ms) {
const n = Number(ms);
const t = Number.isFinite(n) ? Math.max(0, n) : 0;
if (t <= 0) return;
spawnSync('sleep', [String(Math.ceil(t / 1000))], { stdio: 'ignore' });
}
function resolveWorkspaceRoot() {
// In OpenClaw exec, cwd is usually the workspace root.
// Keep it simple: do not try to walk up arbitrarily.
return process.cwd();
}
function resolveEvolverEntry(workspaceRoot) {
const candidates = [
path.join(workspaceRoot, 'skills', 'evolver', 'index.js'),
path.join(workspaceRoot, 'skills', 'capability-evolver', 'index.js'),
];
for (const p of candidates) {
if (exists(p)) return p;
}
return null;
}
function main() {
const waitMs = parseInt(String(process.env.EVOLVER_RECOVER_WAIT_MS || '10000'), 10);
const wait = Number.isFinite(waitMs) ? Math.max(0, waitMs) : 10000;
console.log(`[RECOVERY] Waiting ${wait}ms before restart...`);
sleepMs(wait);
const workspaceRoot = resolveWorkspaceRoot();
const entry = resolveEvolverEntry(workspaceRoot);
if (!entry) {
console.error('[RECOVERY] Failed: cannot locate evolver entry under skills/.');
process.exit(2);
}
console.log(`[RECOVERY] Restarting loop via ${path.relative(workspaceRoot, entry)} ...`);
const r = spawnSync(process.execPath, [entry, '--loop'], { stdio: 'inherit' });
process.exit(typeof r.status === 'number' ? r.status : 1);
}
if (require.main === module) {
main();
}
@@ -0,0 +1,89 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const REPO_ROOT = path.resolve(__dirname, '..');
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function parseSemver(v) {
const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
if (!m) return null;
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
}
function bumpSemver(base, bump) {
const v = parseSemver(base);
if (!v) return null;
if (bump === 'major') return `${v.major + 1}.0.0`;
if (bump === 'minor') return `${v.major}.${v.minor + 1}.0`;
if (bump === 'patch') return `${v.major}.${v.minor}.${v.patch + 1}`;
return `${v.major}.${v.minor}.${v.patch}`;
}
function git(cmd) {
return execSync(cmd, { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
}
function getBaseReleaseCommit() {
try {
const hash = git('git log -n 1 --pretty=%H --grep="chore(release): prepare v"');
return hash || null;
} catch (e) {
return null;
}
}
function getCommitSubjectsSince(baseCommit) {
try {
if (!baseCommit) {
const out = git('git log -n 30 --pretty=%s');
return out ? out.split('\n').filter(Boolean) : [];
}
const out = git(`git log ${baseCommit}..HEAD --pretty=%s`);
return out ? out.split('\n').filter(Boolean) : [];
} catch (e) {
return [];
}
}
function inferBumpFromSubjects(subjects) {
const subs = (subjects || []).map(s => String(s));
const hasBreaking = subs.some(s => /\bBREAKING CHANGE\b/i.test(s) || /^[a-z]+(\(.+\))?!:/.test(s));
if (hasBreaking) return { bump: 'major', reason: 'breaking change marker in commit subject' };
const hasFeat = subs.some(s => /^feat(\(.+\))?:/i.test(s));
if (hasFeat) return { bump: 'minor', reason: 'feature commit detected (feat:)' };
const hasFix = subs.some(s => /^(fix|perf)(\(.+\))?:/i.test(s));
if (hasFix) return { bump: 'patch', reason: 'fix/perf commit detected' };
if (subs.length === 0) return { bump: 'none', reason: 'no commits since base release commit' };
return { bump: 'patch', reason: 'default to patch for non-breaking changes' };
}
function main() {
const pkgPath = path.join(REPO_ROOT, 'package.json');
const baseVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
const baseCommit = getBaseReleaseCommit();
const subjects = getCommitSubjectsSince(baseCommit);
const decision = inferBumpFromSubjects(subjects);
const suggestedVersion = decision.bump === 'none' ? baseVersion : bumpSemver(baseVersion, decision.bump);
const out = { baseVersion, baseCommit, subjects, decision, suggestedVersion };
const memDir = path.join(REPO_ROOT, 'memory');
ensureDir(memDir);
fs.writeFileSync(path.join(memDir, 'semver_suggestion.json'), JSON.stringify(out, null, 2) + '\n', 'utf8');
process.stdout.write(JSON.stringify(out, null, 2) + '\n');
}
try {
main();
} catch (e) {
process.stderr.write(`${e.message}\n`);
process.exit(1);
}
@@ -0,0 +1,8 @@
// Usage: node scripts/validate-modules.js ./src/evolve ./src/gep/solidify
// Requires each module to verify it loads without errors.
// Paths are resolved relative to cwd (repo root), not this script's location.
const path = require('path');
const modules = process.argv.slice(2);
if (!modules.length) { console.error('No modules specified'); process.exit(1); }
for (const m of modules) { require(path.resolve(m)); }
console.log('ok');
+13
View File
@@ -0,0 +1,13 @@
// Canary script: run in a forked child process to verify index.js loads
// without crashing. Exit 0 = safe, non-zero = broken.
//
// This is the last safety net before solidify commits an evolution.
// If a patch broke index.js (syntax error, missing require, etc.),
// the canary catches it BEFORE the daemon restarts with broken code.
try {
require('../index.js');
process.exit(0);
} catch (e) {
process.stderr.write(String(e.message || e).slice(0, 500));
process.exit(1);
}
File diff suppressed because it is too large Load Diff
+173
View File
@@ -0,0 +1,173 @@
const fs = require('fs');
const { readAllEvents } = require('./assetStore');
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
const { unwrapAssetFromMessage } = require('./a2aProtocol');
function nowIso() { return new Date().toISOString(); }
function isAllowedA2AAsset(obj) {
if (!obj || typeof obj !== 'object') return false;
var t = obj.type;
return t === 'Gene' || t === 'Capsule' || t === 'EvolutionEvent';
}
function safeNumber(x, fallback) {
if (fallback === undefined) fallback = null;
var n = Number(x);
return Number.isFinite(n) ? n : fallback;
}
function getBlastRadiusLimits() {
var maxFiles = safeNumber(process.env.A2A_MAX_FILES, 5);
var maxLines = safeNumber(process.env.A2A_MAX_LINES, 200);
return {
maxFiles: Number.isFinite(maxFiles) ? maxFiles : 5,
maxLines: Number.isFinite(maxLines) ? maxLines : 200,
};
}
function isBlastRadiusSafe(blastRadius) {
var lim = getBlastRadiusLimits();
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Number(blastRadius.files) : 0;
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Number(blastRadius.lines) : 0;
return files <= lim.maxFiles && lines <= lim.maxLines;
}
function clamp01(n) {
var x = Number(n);
if (!Number.isFinite(x)) return 0;
return Math.max(0, Math.min(1, x));
}
function lowerConfidence(asset, opts) {
if (!opts) opts = {};
var factor = Number.isFinite(Number(opts.factor)) ? Number(opts.factor) : 0.6;
var receivedFrom = opts.source || 'external';
var receivedAt = opts.received_at || nowIso();
var cloned = JSON.parse(JSON.stringify(asset || {}));
if (!isAllowedA2AAsset(cloned)) return null;
if (cloned.type === 'Capsule') {
if (typeof cloned.confidence === 'number') cloned.confidence = clamp01(cloned.confidence * factor);
else if (cloned.confidence != null) cloned.confidence = clamp01(Number(cloned.confidence) * factor);
}
if (!cloned.a2a || typeof cloned.a2a !== 'object') cloned.a2a = {};
cloned.a2a.status = 'external_candidate';
cloned.a2a.source = receivedFrom;
cloned.a2a.received_at = receivedAt;
cloned.a2a.confidence_factor = factor;
if (!cloned.schema_version) cloned.schema_version = SCHEMA_VERSION;
if (!cloned.asset_id) { try { cloned.asset_id = computeAssetId(cloned); } catch (e) {} }
return cloned;
}
function readEvolutionEvents() {
var events = readAllEvents();
return Array.isArray(events) ? events.filter(function (e) { return e && e.type === 'EvolutionEvent'; }) : [];
}
function normalizeEventsList(events) {
return Array.isArray(events) ? events : [];
}
function computeCapsuleSuccessStreak(params) {
var capsuleId = params.capsuleId;
var events = params.events;
var id = capsuleId ? String(capsuleId) : '';
if (!id) return 0;
var list = normalizeEventsList(events || readEvolutionEvents());
var streak = 0;
for (var i = list.length - 1; i >= 0; i--) {
var ev = list[i];
if (!ev || ev.type !== 'EvolutionEvent') continue;
if (!ev.capsule_id || String(ev.capsule_id) !== id) continue;
var st = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
if (st === 'success') streak += 1; else break;
}
return streak;
}
function isCapsuleBroadcastEligible(capsule, opts) {
if (!opts) opts = {};
if (!capsule || capsule.type !== 'Capsule') return false;
var score = capsule.outcome && capsule.outcome.score != null ? safeNumber(capsule.outcome.score, null) : null;
if (score == null || score < 0.7) return false;
var blast = capsule.blast_radius || (capsule.outcome && capsule.outcome.blast_radius) || null;
if (!isBlastRadiusSafe(blast)) return false;
var events = Array.isArray(opts.events) ? opts.events : readEvolutionEvents();
var streak = computeCapsuleSuccessStreak({ capsuleId: capsule.id, events: events });
if (streak < 2) return false;
return true;
}
function exportEligibleCapsules(params) {
if (!params) params = {};
var list = Array.isArray(params.capsules) ? params.capsules : [];
var evs = Array.isArray(params.events) ? params.events : readEvolutionEvents();
var eligible = list.filter(function (c) { return isCapsuleBroadcastEligible(c, { events: evs }); });
for (var i = 0; i < eligible.length; i++) {
var c = eligible[i];
if (!c.schema_version) c.schema_version = SCHEMA_VERSION;
if (!c.asset_id) { try { c.asset_id = computeAssetId(c); } catch (e) {} }
}
return eligible;
}
function isGeneBroadcastEligible(gene) {
if (!gene || gene.type !== 'Gene') return false;
if (!gene.id || typeof gene.id !== 'string') return false;
if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) return false;
if (!Array.isArray(gene.validation) || gene.validation.length === 0) return false;
return true;
}
function exportEligibleGenes(params) {
if (!params) params = {};
var list = Array.isArray(params.genes) ? params.genes : [];
var eligible = list.filter(function (g) { return isGeneBroadcastEligible(g); });
for (var i = 0; i < eligible.length; i++) {
var g = eligible[i];
if (!g.schema_version) g.schema_version = SCHEMA_VERSION;
if (!g.asset_id) { try { g.asset_id = computeAssetId(g); } catch (e) {} }
}
return eligible;
}
function parseA2AInput(text) {
var raw = String(text || '').trim();
if (!raw) return [];
try {
var maybe = JSON.parse(raw);
if (Array.isArray(maybe)) {
return maybe.map(function (item) { return unwrapAssetFromMessage(item) || item; }).filter(Boolean);
}
if (maybe && typeof maybe === 'object') {
var unwrapped = unwrapAssetFromMessage(maybe);
return unwrapped ? [unwrapped] : [maybe];
}
} catch (e) {}
var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
var items = [];
for (var i = 0; i < lines.length; i++) {
try {
var obj = JSON.parse(lines[i]);
var uw = unwrapAssetFromMessage(obj);
items.push(uw || obj);
} catch (e) { continue; }
}
return items;
}
function readTextIfExists(filePath) {
try {
if (!filePath) return '';
if (!fs.existsSync(filePath)) return '';
return fs.readFileSync(filePath, 'utf8');
} catch { return ''; }
}
module.exports = {
isAllowedA2AAsset, lowerConfidence, isBlastRadiusSafe,
computeCapsuleSuccessStreak, isCapsuleBroadcastEligible,
exportEligibleCapsules, isGeneBroadcastEligible,
exportEligibleGenes, parseA2AInput, readTextIfExists,
};
@@ -0,0 +1,560 @@
// GEP A2A Protocol - Standard message types and pluggable transport layer.
//
// Protocol messages:
// hello - capability advertisement and node discovery
// publish - broadcast an eligible asset (Capsule/Gene)
// fetch - request a specific asset by id or content hash
// report - send a ValidationReport for a received asset
// decision - accept/reject/quarantine decision on a received asset
// revoke - withdraw a previously published asset
//
// Transport interface:
// send(message, opts) - send a protocol message
// receive(opts) - receive pending messages
// list(opts) - list available message files/streams
//
// Default transport: FileTransport (reads/writes JSONL to a2a/ directory).
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { getGepAssetsDir } = require('./paths');
const { computeAssetId } = require('./contentHash');
const { captureEnvFingerprint } = require('./envFingerprint');
const os = require('os');
const { getDeviceId } = require('./deviceId');
const PROTOCOL_NAME = 'gep-a2a';
const PROTOCOL_VERSION = '1.0.0';
const VALID_MESSAGE_TYPES = ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke'];
const NODE_ID_RE = /^node_[a-f0-9]{12}$/;
const NODE_ID_DIR = path.join(os.homedir(), '.evomap');
const NODE_ID_FILE = path.join(NODE_ID_DIR, 'node_id');
const LOCAL_NODE_ID_FILE = path.resolve(__dirname, '..', '..', '.evomap_node_id');
let _cachedNodeId = null;
function _loadPersistedNodeId() {
try {
if (fs.existsSync(NODE_ID_FILE)) {
const id = fs.readFileSync(NODE_ID_FILE, 'utf8').trim();
if (id && NODE_ID_RE.test(id)) return id;
}
} catch {}
try {
if (fs.existsSync(LOCAL_NODE_ID_FILE)) {
const id = fs.readFileSync(LOCAL_NODE_ID_FILE, 'utf8').trim();
if (id && NODE_ID_RE.test(id)) return id;
}
} catch {}
return null;
}
function _persistNodeId(id) {
try {
if (!fs.existsSync(NODE_ID_DIR)) {
fs.mkdirSync(NODE_ID_DIR, { recursive: true, mode: 0o700 });
}
fs.writeFileSync(NODE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
return;
} catch {}
try {
fs.writeFileSync(LOCAL_NODE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
return;
} catch {}
}
function generateMessageId() {
return 'msg_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
}
function getNodeId() {
if (_cachedNodeId) return _cachedNodeId;
if (process.env.A2A_NODE_ID) {
_cachedNodeId = String(process.env.A2A_NODE_ID);
return _cachedNodeId;
}
const persisted = _loadPersistedNodeId();
if (persisted) {
_cachedNodeId = persisted;
return _cachedNodeId;
}
const deviceId = getDeviceId();
const agentName = process.env.AGENT_NAME || 'default';
const raw = deviceId + '|' + agentName + '|' + process.cwd();
const computed = 'node_' + crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12);
_persistNodeId(computed);
_cachedNodeId = computed;
return _cachedNodeId;
}
// --- Base message builder ---
function buildMessage(params) {
var messageType = params.messageType;
var payload = params.payload;
var senderId = params.senderId;
if (!VALID_MESSAGE_TYPES.includes(messageType)) {
throw new Error('Invalid message type: ' + messageType + '. Valid: ' + VALID_MESSAGE_TYPES.join(', '));
}
return {
protocol: PROTOCOL_NAME,
protocol_version: PROTOCOL_VERSION,
message_type: messageType,
message_id: generateMessageId(),
sender_id: senderId || getNodeId(),
timestamp: new Date().toISOString(),
payload: payload || {},
};
}
// --- Typed message builders ---
function buildHello(opts) {
var o = opts || {};
return buildMessage({
messageType: 'hello',
senderId: o.nodeId,
payload: {
capabilities: o.capabilities || {},
gene_count: typeof o.geneCount === 'number' ? o.geneCount : null,
capsule_count: typeof o.capsuleCount === 'number' ? o.capsuleCount : null,
env_fingerprint: captureEnvFingerprint(),
},
});
}
function buildPublish(opts) {
var o = opts || {};
var asset = o.asset;
if (!asset || !asset.type || !asset.id) {
throw new Error('publish: asset must have type and id');
}
// Generate signature: HMAC-SHA256 of asset_id with node secret
var assetIdVal = asset.asset_id || computeAssetId(asset);
var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
var signature = crypto.createHmac('sha256', nodeSecret).update(assetIdVal).digest('hex');
return buildMessage({
messageType: 'publish',
senderId: o.nodeId,
payload: {
asset_type: asset.type,
asset_id: assetIdVal,
local_id: asset.id,
asset: asset,
signature: signature,
},
});
}
// Build a bundle publish message containing Gene + Capsule (+ optional EvolutionEvent).
// Hub requires payload.assets = [Gene, Capsule] since bundle enforcement was added.
function buildPublishBundle(opts) {
var o = opts || {};
var gene = o.gene;
var capsule = o.capsule;
var event = o.event || null;
if (!gene || gene.type !== 'Gene' || !gene.id) {
throw new Error('publishBundle: gene must be a valid Gene with type and id');
}
if (!capsule || capsule.type !== 'Capsule' || !capsule.id) {
throw new Error('publishBundle: capsule must be a valid Capsule with type and id');
}
var geneAssetId = gene.asset_id || computeAssetId(gene);
var capsuleAssetId = capsule.asset_id || computeAssetId(capsule);
var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
var signatureInput = [geneAssetId, capsuleAssetId].sort().join('|');
var signature = crypto.createHmac('sha256', nodeSecret).update(signatureInput).digest('hex');
if (o.modelName && typeof o.modelName === 'string') {
gene.model_name = o.modelName;
capsule.model_name = o.modelName;
}
var assets = [gene, capsule];
if (event && event.type === 'EvolutionEvent') {
if (o.modelName && typeof o.modelName === 'string') {
event.model_name = o.modelName;
}
assets.push(event);
}
var publishPayload = {
assets: assets,
signature: signature,
};
if (o.chainId && typeof o.chainId === 'string') {
publishPayload.chain_id = o.chainId;
}
return buildMessage({
messageType: 'publish',
senderId: o.nodeId,
payload: publishPayload,
});
}
function buildFetch(opts) {
var o = opts || {};
return buildMessage({
messageType: 'fetch',
senderId: o.nodeId,
payload: {
asset_type: o.assetType || null,
local_id: o.localId || null,
content_hash: o.contentHash || null,
},
});
}
function buildReport(opts) {
var o = opts || {};
return buildMessage({
messageType: 'report',
senderId: o.nodeId,
payload: {
target_asset_id: o.assetId || null,
target_local_id: o.localId || null,
validation_report: o.validationReport || null,
},
});
}
function buildDecision(opts) {
var o = opts || {};
var validDecisions = ['accept', 'reject', 'quarantine'];
if (!validDecisions.includes(o.decision)) {
throw new Error('decision must be one of: ' + validDecisions.join(', '));
}
return buildMessage({
messageType: 'decision',
senderId: o.nodeId,
payload: {
target_asset_id: o.assetId || null,
target_local_id: o.localId || null,
decision: o.decision,
reason: o.reason || null,
},
});
}
function buildRevoke(opts) {
var o = opts || {};
return buildMessage({
messageType: 'revoke',
senderId: o.nodeId,
payload: {
target_asset_id: o.assetId || null,
target_local_id: o.localId || null,
reason: o.reason || null,
},
});
}
// --- Validation ---
function isValidProtocolMessage(msg) {
if (!msg || typeof msg !== 'object') return false;
if (msg.protocol !== PROTOCOL_NAME) return false;
if (!msg.message_type || !VALID_MESSAGE_TYPES.includes(msg.message_type)) return false;
if (!msg.message_id || typeof msg.message_id !== 'string') return false;
if (!msg.timestamp || typeof msg.timestamp !== 'string') return false;
return true;
}
// Try to extract a raw asset from either a protocol message or a plain asset object.
// This enables backward-compatible ingestion of both old-format and new-format payloads.
function unwrapAssetFromMessage(input) {
if (!input || typeof input !== 'object') return null;
// If it is a protocol message with a publish payload, extract the asset.
if (input.protocol === PROTOCOL_NAME && input.message_type === 'publish') {
var p = input.payload;
if (p && p.asset && typeof p.asset === 'object') return p.asset;
return null;
}
// If it is a plain asset (Gene/Capsule/EvolutionEvent), return as-is.
if (input.type === 'Gene' || input.type === 'Capsule' || input.type === 'EvolutionEvent') {
return input;
}
return null;
}
// --- File Transport ---
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
} catch (e) {}
}
function defaultA2ADir() {
return process.env.A2A_DIR || path.join(getGepAssetsDir(), 'a2a');
}
function fileTransportSend(message, opts) {
var dir = (opts && opts.dir) || defaultA2ADir();
var subdir = path.join(dir, 'outbox');
ensureDir(subdir);
var filePath = path.join(subdir, message.message_type + '.jsonl');
fs.appendFileSync(filePath, JSON.stringify(message) + '\n', 'utf8');
return { ok: true, path: filePath };
}
function fileTransportReceive(opts) {
var dir = (opts && opts.dir) || defaultA2ADir();
var subdir = path.join(dir, 'inbox');
if (!fs.existsSync(subdir)) return [];
var files = fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
var messages = [];
for (var fi = 0; fi < files.length; fi++) {
try {
var raw = fs.readFileSync(path.join(subdir, files[fi]), 'utf8');
var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
for (var li = 0; li < lines.length; li++) {
try {
var msg = JSON.parse(lines[li]);
if (msg && msg.protocol === PROTOCOL_NAME) messages.push(msg);
} catch (e) {}
}
} catch (e) {}
}
return messages;
}
function fileTransportList(opts) {
var dir = (opts && opts.dir) || defaultA2ADir();
var subdir = path.join(dir, 'outbox');
if (!fs.existsSync(subdir)) return [];
return fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
}
// --- HTTP Transport (connects to evomap-hub) ---
function httpTransportSend(message, opts) {
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
if (!hubUrl) return { ok: false, error: 'A2A_HUB_URL not set' };
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/' + message.message_type;
var body = JSON.stringify(message);
// Use dynamic import for fetch (available in Node 18+)
return fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
})
.then(function (res) { return res.json(); })
.then(function (data) { return { ok: true, response: data }; })
.catch(function (err) { return { ok: false, error: err.message }; });
}
function httpTransportReceive(opts) {
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
if (!hubUrl) return Promise.resolve([]);
var assetType = (opts && opts.assetType) || null;
var fetchMsg = buildFetch({ assetType: assetType });
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/fetch';
return fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fetchMsg),
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data && data.payload && Array.isArray(data.payload.results)) {
return data.payload.results;
}
return [];
})
.catch(function () { return []; });
}
function httpTransportList() {
return ['http'];
}
// --- Heartbeat ---
var _heartbeatTimer = null;
var _heartbeatStartedAt = null;
var _heartbeatConsecutiveFailures = 0;
var _heartbeatTotalSent = 0;
var _heartbeatTotalFailed = 0;
function getHubUrl() {
return process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || '';
}
function sendHelloToHub() {
var hubUrl = getHubUrl();
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/hello';
var nodeId = getNodeId();
var msg = buildHello({ nodeId: nodeId, capabilities: {} });
msg.sender_id = nodeId;
return fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg),
signal: AbortSignal.timeout(15000),
})
.then(function (res) { return res.json(); })
.then(function (data) { return { ok: true, response: data }; })
.catch(function (err) { return { ok: false, error: err.message }; });
}
function sendHeartbeat() {
var hubUrl = getHubUrl();
if (!hubUrl) return Promise.resolve({ ok: false, error: 'no_hub_url' });
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/heartbeat';
var nodeId = getNodeId();
var body = JSON.stringify({
node_id: nodeId,
sender_id: nodeId,
version: PROTOCOL_VERSION,
uptime_ms: _heartbeatStartedAt ? Date.now() - _heartbeatStartedAt : 0,
timestamp: new Date().toISOString(),
});
_heartbeatTotalSent++;
return fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body,
signal: AbortSignal.timeout(10000),
})
.then(function (res) { return res.json(); })
.then(function (data) {
if (data && data.status === 'unknown_node') {
console.warn('[Heartbeat] Node not registered on hub. Sending hello to re-register...');
return sendHelloToHub().then(function (helloResult) {
if (helloResult.ok) {
console.log('[Heartbeat] Re-registered with hub successfully.');
_heartbeatConsecutiveFailures = 0;
} else {
console.warn('[Heartbeat] Re-registration failed: ' + (helloResult.error || 'unknown'));
}
return { ok: helloResult.ok, response: data, reregistered: helloResult.ok };
});
}
_heartbeatConsecutiveFailures = 0;
return { ok: true, response: data };
})
.catch(function (err) {
_heartbeatConsecutiveFailures++;
_heartbeatTotalFailed++;
if (_heartbeatConsecutiveFailures === 3) {
console.warn('[Heartbeat] 3 consecutive failures. Network issue? Last error: ' + err.message);
} else if (_heartbeatConsecutiveFailures === 10) {
console.warn('[Heartbeat] 10 consecutive failures. Hub may be unreachable. (' + err.message + ')');
} else if (_heartbeatConsecutiveFailures % 50 === 0) {
console.warn('[Heartbeat] ' + _heartbeatConsecutiveFailures + ' consecutive failures. (' + err.message + ')');
}
return { ok: false, error: err.message };
});
}
function startHeartbeat(intervalMs) {
if (_heartbeatTimer) return;
var interval = intervalMs || Number(process.env.HEARTBEAT_INTERVAL_MS) || 120000; // default 2min
_heartbeatStartedAt = Date.now();
// Register with hub first (hello), then start heartbeat loop
sendHelloToHub().then(function (r) {
if (r.ok) console.log('[Heartbeat] Registered with hub. Node: ' + getNodeId());
else console.warn('[Heartbeat] Hello failed (will retry via heartbeat): ' + (r.error || 'unknown'));
}).catch(function () {});
// First heartbeat after a short delay (let hello complete first)
setTimeout(function () {
sendHeartbeat().then(function (r) {
if (r.ok) console.log('[Heartbeat] Connected to hub. Node: ' + getNodeId());
}).catch(function () {});
}, 5000);
_heartbeatTimer = setInterval(function () {
sendHeartbeat().catch(function () {});
}, interval);
if (_heartbeatTimer.unref) _heartbeatTimer.unref();
}
function stopHeartbeat() {
if (_heartbeatTimer) {
clearInterval(_heartbeatTimer);
_heartbeatTimer = null;
}
}
function getHeartbeatStats() {
return {
running: !!_heartbeatTimer,
uptimeMs: _heartbeatStartedAt ? Date.now() - _heartbeatStartedAt : 0,
totalSent: _heartbeatTotalSent,
totalFailed: _heartbeatTotalFailed,
consecutiveFailures: _heartbeatConsecutiveFailures,
};
}
// --- Transport registry ---
var transports = {
file: {
send: fileTransportSend,
receive: fileTransportReceive,
list: fileTransportList,
},
http: {
send: httpTransportSend,
receive: httpTransportReceive,
list: httpTransportList,
},
};
function getTransport(name) {
var n = String(name || process.env.A2A_TRANSPORT || 'file').toLowerCase();
var t = transports[n];
if (!t) throw new Error('Unknown A2A transport: ' + n + '. Available: ' + Object.keys(transports).join(', '));
return t;
}
function registerTransport(name, impl) {
if (!name || typeof name !== 'string') throw new Error('transport name required');
if (!impl || typeof impl.send !== 'function' || typeof impl.receive !== 'function') {
throw new Error('transport must implement send() and receive()');
}
transports[name] = impl;
}
module.exports = {
PROTOCOL_NAME,
PROTOCOL_VERSION,
VALID_MESSAGE_TYPES,
getNodeId,
buildMessage,
buildHello,
buildPublish,
buildPublishBundle,
buildFetch,
buildReport,
buildDecision,
buildRevoke,
isValidProtocolMessage,
unwrapAssetFromMessage,
getTransport,
registerTransport,
fileTransportSend,
fileTransportReceive,
fileTransportList,
httpTransportSend,
httpTransportReceive,
httpTransportList,
sendHeartbeat,
sendHelloToHub,
startHeartbeat,
stopHeartbeat,
getHeartbeatStats,
};
@@ -0,0 +1,35 @@
const fs = require('fs');
const path = require('path');
// Innovation: Self-Correction Analyzer
// Analyze past failures to suggest better future mutations
// Pattern: Meta-learning
function analyzeFailures() {
const memoryPath = path.join(process.cwd(), 'MEMORY.md');
if (!fs.existsSync(memoryPath)) return { status: 'skipped', reason: 'no_memory' };
const content = fs.readFileSync(memoryPath, 'utf8');
const failureRegex = /\|\s*\*\*F\d+\*\*\s*\|\s*Fix\s*\|\s*(.*?)\s*\|\s*\*\*(.*?)\*\*\s*\((.*?)\)\s*\|/g;
const failures = [];
let match;
while ((match = failureRegex.exec(content)) !== null) {
failures.push({
summary: match[1].trim(),
detail: match[2].trim()
});
}
return {
status: 'success',
count: failures.length,
failures: failures.slice(0, 3) // Return top 3 for prompt context
};
}
if (require.main === module) {
console.log(JSON.stringify(analyzeFailures(), null, 2));
}
module.exports = { analyzeFailures };
@@ -0,0 +1,297 @@
const fs = require('fs');
const path = require('path');
const { getGepAssetsDir } = require('./paths');
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJsonIfExists(filePath, fallback) {
try {
if (!fs.existsSync(filePath)) return fallback;
const raw = fs.readFileSync(filePath, 'utf8');
if (!raw.trim()) return fallback;
return JSON.parse(raw);
} catch {
return fallback;
}
}
function writeJsonAtomic(filePath, obj) {
const dir = path.dirname(filePath);
ensureDir(dir);
const tmp = `${filePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
fs.renameSync(tmp, filePath);
}
// Build a validation command using repo-root-relative paths.
// runValidations() executes with cwd=repoRoot, so require('./src/...')
// resolves correctly without embedding machine-specific absolute paths.
function buildValidationCmd(relModules) {
const paths = relModules.map(m => `./${m}`);
return `node scripts/validate-modules.js ${paths.join(' ')}`;
}
function getDefaultGenes() {
return {
version: 1,
genes: [
{
type: 'Gene', id: 'gene_gep_repair_from_errors', category: 'repair',
signals_match: ['error', 'exception', 'failed', 'unstable'],
preconditions: ['signals contains error-related indicators'],
strategy: [
'Extract structured signals from logs and user instructions',
'Select an existing Gene by signals match (no improvisation)',
'Estimate blast radius (files, lines) before editing',
'Apply smallest reversible patch',
'Validate using declared validation steps; rollback on failure',
'Solidify knowledge: append EvolutionEvent, update Gene/Capsule store',
],
constraints: { max_files: 12, forbidden_paths: ['.git', 'node_modules'] },
validation: [
buildValidationCmd(['src/evolve', 'src/gep/solidify']),
buildValidationCmd(['src/gep/selector', 'src/gep/memoryGraph']),
],
},
{
type: 'Gene', id: 'gene_gep_optimize_prompt_and_assets', category: 'optimize',
signals_match: ['protocol', 'gep', 'prompt', 'audit', 'reusable'],
preconditions: ['need stricter, auditable evolution protocol outputs'],
strategy: [
'Extract signals and determine selection rationale via Selector JSON',
'Prefer reusing existing Gene/Capsule; only create if no match exists',
'Refactor prompt assembly to embed assets (genes, capsules, parent event)',
'Reduce noise and ambiguity; enforce strict output schema',
'Validate by running node index.js run and ensuring no runtime errors',
'Solidify: record EvolutionEvent, update Gene definitions, create Capsule on success',
],
constraints: { max_files: 20, forbidden_paths: ['.git', 'node_modules'] },
validation: [buildValidationCmd(['src/evolve', 'src/gep/prompt'])],
},
],
};
}
function getDefaultCapsules() { return { version: 1, capsules: [] }; }
function genesPath() { return path.join(getGepAssetsDir(), 'genes.json'); }
function capsulesPath() { return path.join(getGepAssetsDir(), 'capsules.json'); }
function capsulesJsonlPath() { return path.join(getGepAssetsDir(), 'capsules.jsonl'); }
function eventsPath() { return path.join(getGepAssetsDir(), 'events.jsonl'); }
function candidatesPath() { return path.join(getGepAssetsDir(), 'candidates.jsonl'); }
function externalCandidatesPath() { return path.join(getGepAssetsDir(), 'external_candidates.jsonl'); }
function failedCapsulesPath() { return path.join(getGepAssetsDir(), 'failed_capsules.json'); }
function loadGenes() {
const jsonGenes = readJsonIfExists(genesPath(), getDefaultGenes()).genes || [];
const jsonlGenes = [];
try {
const p = path.join(getGepAssetsDir(), 'genes.jsonl');
if (fs.existsSync(p)) {
const raw = fs.readFileSync(p, 'utf8');
raw.split('\n').forEach(line => {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed && parsed.type === 'Gene') jsonlGenes.push(parsed);
} catch(e) {}
}
});
}
} catch(e) {}
// Combine and deduplicate by ID (JSONL takes precedence if newer, but here we just merge)
const combined = [...jsonGenes, ...jsonlGenes];
const unique = new Map();
combined.forEach(g => {
if (g && g.id) unique.set(String(g.id), g);
});
return Array.from(unique.values());
}
function loadCapsules() {
const legacy = readJsonIfExists(capsulesPath(), getDefaultCapsules()).capsules || [];
const jsonlCapsules = [];
try {
const p = capsulesJsonlPath();
if (fs.existsSync(p)) {
const raw = fs.readFileSync(p, 'utf8');
raw.split('\n').forEach(line => {
if (line.trim()) {
try { jsonlCapsules.push(JSON.parse(line)); } catch(e) {}
}
});
}
} catch(e) {}
// Combine and deduplicate by ID
const combined = [...legacy, ...jsonlCapsules];
const unique = new Map();
combined.forEach(c => {
if (c && c.id) unique.set(String(c.id), c);
});
return Array.from(unique.values());
}
function getLastEventId() {
try {
const p = eventsPath();
if (!fs.existsSync(p)) return null;
const raw = fs.readFileSync(p, 'utf8');
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
if (lines.length === 0) return null;
const last = JSON.parse(lines[lines.length - 1]);
return last && typeof last.id === 'string' ? last.id : null;
} catch { return null; }
}
function readAllEvents() {
try {
const p = eventsPath();
if (!fs.existsSync(p)) return [];
const raw = fs.readFileSync(p, 'utf8');
return raw.split('\n').map(l => l.trim()).filter(Boolean).map(l => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean);
} catch { return []; }
}
function appendEventJsonl(eventObj) {
const dir = getGepAssetsDir(); ensureDir(dir);
fs.appendFileSync(eventsPath(), JSON.stringify(eventObj) + '\n', 'utf8');
}
function appendCandidateJsonl(candidateObj) {
const dir = getGepAssetsDir(); ensureDir(dir);
fs.appendFileSync(candidatesPath(), JSON.stringify(candidateObj) + '\n', 'utf8');
}
function appendExternalCandidateJsonl(obj) {
const dir = getGepAssetsDir(); ensureDir(dir);
fs.appendFileSync(externalCandidatesPath(), JSON.stringify(obj) + '\n', 'utf8');
}
function readRecentCandidates(limit = 20) {
try {
const p = candidatesPath();
if (!fs.existsSync(p)) return [];
const raw = fs.readFileSync(p, 'utf8');
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
return lines.slice(Math.max(0, lines.length - limit)).map(l => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean);
} catch { return []; }
}
function readRecentExternalCandidates(limit = 50) {
try {
const p = externalCandidatesPath();
if (!fs.existsSync(p)) return [];
const raw = fs.readFileSync(p, 'utf8');
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
return lines.slice(Math.max(0, lines.length - limit)).map(l => {
try { return JSON.parse(l); } catch { return null; }
}).filter(Boolean);
} catch { return []; }
}
// Safety net: ensure schema_version and asset_id are present before writing.
function ensureSchemaFields(obj) {
if (!obj || typeof obj !== 'object') return obj;
if (!obj.schema_version) obj.schema_version = SCHEMA_VERSION;
if (!obj.asset_id) { try { obj.asset_id = computeAssetId(obj); } catch (e) {} }
return obj;
}
function upsertGene(geneObj) {
ensureSchemaFields(geneObj);
const current = readJsonIfExists(genesPath(), getDefaultGenes());
const genes = Array.isArray(current.genes) ? current.genes : [];
const idx = genes.findIndex(g => g && g.id === geneObj.id);
if (idx >= 0) genes[idx] = geneObj; else genes.push(geneObj);
writeJsonAtomic(genesPath(), { version: current.version || 1, genes });
}
function appendCapsule(capsuleObj) {
ensureSchemaFields(capsuleObj);
const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
const capsules = Array.isArray(current.capsules) ? current.capsules : [];
capsules.push(capsuleObj);
writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
}
function upsertCapsule(capsuleObj) {
if (!capsuleObj || capsuleObj.type !== 'Capsule' || !capsuleObj.id) return;
ensureSchemaFields(capsuleObj);
const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
const capsules = Array.isArray(current.capsules) ? current.capsules : [];
const idx = capsules.findIndex(c => c && c.type === 'Capsule' && String(c.id) === String(capsuleObj.id));
if (idx >= 0) capsules[idx] = capsuleObj; else capsules.push(capsuleObj);
writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
}
var FAILED_CAPSULES_MAX = 200;
var FAILED_CAPSULES_TRIM_TO = 100;
function getDefaultFailedCapsules() { return { version: 1, failed_capsules: [] }; }
function appendFailedCapsule(capsuleObj) {
if (!capsuleObj || typeof capsuleObj !== 'object') return;
ensureSchemaFields(capsuleObj);
var current = readJsonIfExists(failedCapsulesPath(), getDefaultFailedCapsules());
var list = Array.isArray(current.failed_capsules) ? current.failed_capsules : [];
list.push(capsuleObj);
if (list.length > FAILED_CAPSULES_MAX) {
list = list.slice(list.length - FAILED_CAPSULES_TRIM_TO);
}
writeJsonAtomic(failedCapsulesPath(), { version: current.version || 1, failed_capsules: list });
}
function readRecentFailedCapsules(limit) {
var n = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Number(limit) : 50;
try {
var current = readJsonIfExists(failedCapsulesPath(), getDefaultFailedCapsules());
var list = Array.isArray(current.failed_capsules) ? current.failed_capsules : [];
return list.slice(Math.max(0, list.length - n));
} catch (e) {
return [];
}
}
// Ensure all expected asset files exist on startup.
// Creates empty files for optional append-only stores so that
// external grep/read commands never fail with "No such file or directory".
function ensureAssetFiles() {
const dir = getGepAssetsDir();
ensureDir(dir);
const files = [
{ path: genesPath(), defaultContent: JSON.stringify(getDefaultGenes(), null, 2) + '\n' },
{ path: capsulesPath(), defaultContent: JSON.stringify(getDefaultCapsules(), null, 2) + '\n' },
{ path: path.join(dir, 'genes.jsonl'), defaultContent: '' },
{ path: eventsPath(), defaultContent: '' },
{ path: candidatesPath(), defaultContent: '' },
{ path: failedCapsulesPath(), defaultContent: JSON.stringify(getDefaultFailedCapsules(), null, 2) + '\n' },
];
for (const f of files) {
if (!fs.existsSync(f.path)) {
try {
fs.writeFileSync(f.path, f.defaultContent, 'utf8');
} catch (e) {
// Non-fatal: log but continue
console.error(`[AssetStore] Failed to create ${f.path}: ${e.message}`);
}
}
}
}
module.exports = {
loadGenes, loadCapsules, readAllEvents, getLastEventId,
appendEventJsonl, appendCandidateJsonl, appendExternalCandidateJsonl,
readRecentCandidates, readRecentExternalCandidates,
upsertGene, appendCapsule, upsertCapsule,
appendFailedCapsule, readRecentFailedCapsules,
genesPath, capsulesPath, eventsPath, candidatesPath, externalCandidatesPath, failedCapsulesPath,
ensureAssetFiles, buildValidationCmd,
};
@@ -0,0 +1,36 @@
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
/**
* Format asset preview for prompt inclusion.
* Handles stringified JSON, arrays, and error cases gracefully.
*/
function formatAssetPreview(preview) {
if (!preview) return '(none)';
if (typeof preview === 'string') {
try {
const parsed = JSON.parse(preview);
if (Array.isArray(parsed) && parsed.length > 0) {
return JSON.stringify(parsed, null, 2);
}
return preview; // Keep as string if not array or empty
} catch (e) {
return preview; // Keep as string if parse fails
}
}
return JSON.stringify(preview, null, 2);
}
/**
* Validate and normalize an asset object.
* Ensures schema version and ID are present.
*/
function normalizeAsset(asset) {
if (!asset || typeof asset !== 'object') return asset;
if (!asset.schema_version) asset.schema_version = SCHEMA_VERSION;
if (!asset.asset_id) {
try { asset.asset_id = computeAssetId(asset); } catch (e) {}
}
return asset;
}
module.exports = { formatAssetPreview, normalizeAsset };
@@ -0,0 +1,71 @@
const fs = require('fs');
const path = require('path');
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
} catch (e) {}
}
function nowIso() {
return new Date().toISOString();
}
function clip(text, maxChars) {
const s = String(text || '');
const n = Number(maxChars);
if (!Number.isFinite(n) || n <= 0) return s;
if (s.length <= n) return s;
return s.slice(0, Math.max(0, n - 40)) + '\n...[TRUNCATED]...\n';
}
function writePromptArtifact({ memoryDir, cycleId, runId, prompt, meta }) {
const dir = String(memoryDir || '').trim();
if (!dir) throw new Error('bridge: missing memoryDir');
ensureDir(dir);
const safeCycle = String(cycleId || 'cycle').replace(/[^a-zA-Z0-9_\-#]/g, '_');
const safeRun = String(runId || Date.now()).replace(/[^a-zA-Z0-9_\-]/g, '_');
const base = `gep_prompt_${safeCycle}_${safeRun}`;
const promptPath = path.join(dir, base + '.txt');
const metaPath = path.join(dir, base + '.json');
fs.writeFileSync(promptPath, String(prompt || ''), 'utf8');
fs.writeFileSync(
metaPath,
JSON.stringify(
{
type: 'GepPromptArtifact',
at: nowIso(),
cycle_id: cycleId || null,
run_id: runId || null,
prompt_path: promptPath,
meta: meta && typeof meta === 'object' ? meta : null,
},
null,
2
) + '\n',
'utf8'
);
return { promptPath, metaPath };
}
function renderSessionsSpawnCall({ task, agentId, label, cleanup }) {
const t = String(task || '').trim();
if (!t) throw new Error('bridge: missing task');
const a = String(agentId || 'main');
const l = String(label || 'gep_bridge');
const c = cleanup ? String(cleanup) : 'delete';
// Output valid JSON so wrappers can parse with JSON.parse (not regex).
// The wrapper uses lastIndexOf('sessions_spawn(') + JSON.parse to extract the task.
const payload = JSON.stringify({ task: t, agentId: a, cleanup: c, label: l });
return `sessions_spawn(${payload})`;
}
module.exports = {
clip,
writePromptArtifact,
renderSessionsSpawnCall,
};
@@ -0,0 +1,142 @@
function stableHash(input) {
// Deterministic lightweight hash (not cryptographic).
const s = String(input || '');
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(16).padStart(8, '0');
}
function clip(text, maxChars) {
const s = String(text || '');
if (!maxChars || s.length <= maxChars) return s;
return s.slice(0, Math.max(0, maxChars - 20)) + ' ...[TRUNCATED]';
}
function toLines(text) {
return String(text || '')
.split('\n')
.map(l => l.trimEnd())
.filter(Boolean);
}
function extractToolCalls(transcript) {
const lines = toLines(transcript);
const calls = [];
for (const line of lines) {
const m = line.match(/\[TOOL:\s*([^\]]+)\]/i);
if (m && m[1]) calls.push(m[1].trim());
}
return calls;
}
function countFreq(items) {
const map = new Map();
for (const it of items) map.set(it, (map.get(it) || 0) + 1);
return map;
}
function buildFiveQuestionsShape({ title, signals, evidence }) {
// Keep it short and structured; this is a template, not a perfect inference.
const input = 'Recent session transcript + memory snippets + user instructions';
const output = 'A safe, auditable evolution patch guided by GEP assets';
const invariants = 'Protocol order, small reversible patches, validation, append-only events';
const params = `Signals: ${Array.isArray(signals) ? signals.join(', ') : ''}`.trim();
const failurePoints = 'Missing signals, over-broad changes, skipped validation, missing knowledge solidification';
return {
title: String(title || '').slice(0, 120),
input,
output,
invariants,
params: params || 'Signals: (none)',
failure_points: failurePoints,
evidence: clip(evidence, 240),
};
}
function extractCapabilityCandidates({ recentSessionTranscript, signals }) {
const candidates = [];
const toolCalls = extractToolCalls(recentSessionTranscript);
const freq = countFreq(toolCalls);
for (const [tool, count] of freq.entries()) {
if (count < 2) continue;
const title = `Repeated tool usage: ${tool}`;
const evidence = `Observed ${count} occurrences of tool call marker for ${tool}.`;
const shape = buildFiveQuestionsShape({ title, signals, evidence });
candidates.push({
type: 'CapabilityCandidate',
id: `cand_${stableHash(title)}`,
title,
source: 'transcript',
created_at: new Date().toISOString(),
signals: Array.isArray(signals) ? signals : [],
shape,
});
}
// Signals-as-candidates: capture recurring pain points as reusable capability shapes.
const signalList = Array.isArray(signals) ? signals : [];
const signalCandidates = [
// Defensive signals
{ signal: 'log_error', title: 'Repair recurring runtime errors' },
{ signal: 'protocol_drift', title: 'Prevent protocol drift and enforce auditable outputs' },
{ signal: 'windows_shell_incompatible', title: 'Avoid platform-specific shell assumptions (Windows compatibility)' },
{ signal: 'session_logs_missing', title: 'Harden session log detection and fallback behavior' },
// Opportunity signals (innovation)
{ signal: 'user_feature_request', title: 'Implement user-requested feature' },
{ signal: 'user_improvement_suggestion', title: 'Apply user improvement suggestion' },
{ signal: 'perf_bottleneck', title: 'Resolve performance bottleneck' },
{ signal: 'capability_gap', title: 'Fill capability gap' },
{ signal: 'stable_success_plateau', title: 'Explore new strategies during stability plateau' },
{ signal: 'external_opportunity', title: 'Evaluate external A2A asset for local adoption' },
];
for (const sc of signalCandidates) {
if (!signalList.some(s => s === sc.signal || s.startsWith(sc.signal + ':'))) continue;
const evidence = `Signal present: ${sc.signal}`;
const shape = buildFiveQuestionsShape({ title: sc.title, signals, evidence });
candidates.push({
type: 'CapabilityCandidate',
id: `cand_${stableHash(sc.signal)}`,
title: sc.title,
source: 'signals',
created_at: new Date().toISOString(),
signals: signalList,
shape,
});
}
// Dedup by id
const seen = new Set();
return candidates.filter(c => {
if (!c || !c.id) return false;
if (seen.has(c.id)) return false;
seen.add(c.id);
return true;
});
}
function renderCandidatesPreview(candidates, maxChars = 1400) {
const list = Array.isArray(candidates) ? candidates : [];
const lines = [];
for (const c of list) {
const s = c && c.shape ? c.shape : {};
lines.push(`- ${c.id}: ${c.title}`);
lines.push(` - input: ${s.input || ''}`);
lines.push(` - output: ${s.output || ''}`);
lines.push(` - invariants: ${s.invariants || ''}`);
lines.push(` - params: ${s.params || ''}`);
lines.push(` - failure_points: ${s.failure_points || ''}`);
if (s.evidence) lines.push(` - evidence: ${s.evidence}`);
}
return clip(lines.join('\n'), maxChars);
}
module.exports = {
extractCapabilityCandidates,
renderCandidatesPreview,
};
@@ -0,0 +1,65 @@
// Content-addressable hashing for GEP assets.
// Provides canonical JSON serialization and SHA-256 based asset IDs.
// This enables deduplication, tamper detection, and cross-node consistency.
const crypto = require('crypto');
// Schema version for all GEP asset types.
// Bump MINOR for additive fields; MAJOR for breaking changes.
const SCHEMA_VERSION = '1.6.0';
// Canonical JSON: deterministic serialization with sorted keys at all levels.
// Arrays preserve order; non-finite numbers become null; undefined becomes null.
function canonicalize(obj) {
if (obj === null || obj === undefined) return 'null';
if (typeof obj === 'boolean') return obj ? 'true' : 'false';
if (typeof obj === 'number') {
if (!Number.isFinite(obj)) return 'null';
return String(obj);
}
if (typeof obj === 'string') return JSON.stringify(obj);
if (Array.isArray(obj)) {
return '[' + obj.map(canonicalize).join(',') + ']';
}
if (typeof obj === 'object') {
const keys = Object.keys(obj).sort();
const pairs = [];
for (const k of keys) {
pairs.push(JSON.stringify(k) + ':' + canonicalize(obj[k]));
}
return '{' + pairs.join(',') + '}';
}
return 'null';
}
// Compute a content-addressable asset ID.
// Excludes self-referential fields (asset_id itself) from the hash input.
// Returns "sha256:<hex>".
function computeAssetId(obj, excludeFields) {
if (!obj || typeof obj !== 'object') return null;
const exclude = new Set(Array.isArray(excludeFields) ? excludeFields : ['asset_id']);
const clean = {};
for (const k of Object.keys(obj)) {
if (exclude.has(k)) continue;
clean[k] = obj[k];
}
const canonical = canonicalize(clean);
const hash = crypto.createHash('sha256').update(canonical, 'utf8').digest('hex');
return 'sha256:' + hash;
}
// Verify that an object's asset_id matches its content.
function verifyAssetId(obj) {
if (!obj || typeof obj !== 'object') return false;
const claimed = obj.asset_id;
if (!claimed || typeof claimed !== 'string') return false;
const computed = computeAssetId(obj);
return claimed === computed;
}
module.exports = {
SCHEMA_VERSION,
canonicalize,
computeAssetId,
verifyAssetId,
};
@@ -0,0 +1,209 @@
// Stable device identifier for node identity.
// Generates a hardware-based fingerprint that persists across directory changes,
// reboots, and evolver upgrades. Used by getNodeId() and env_fingerprint.
//
// Priority chain:
// 1. EVOMAP_DEVICE_ID env var (explicit override, recommended for containers)
// 2. ~/.evomap/device_id file (persisted from previous run)
// 3. <project>/.evomap_device_id (fallback persist path for containers w/o $HOME)
// 4. /etc/machine-id (Linux, set at OS install)
// 5. IOPlatformUUID (macOS hardware UUID)
// 6. Docker/OCI container ID (from /proc/self/cgroup or /proc/self/mountinfo)
// 7. hostname + MAC addresses (network-based fallback)
// 8. random 128-bit hex (last resort, persisted immediately)
const os = require('os');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const DEVICE_ID_DIR = path.join(os.homedir(), '.evomap');
const DEVICE_ID_FILE = path.join(DEVICE_ID_DIR, 'device_id');
const LOCAL_DEVICE_ID_FILE = path.resolve(__dirname, '..', '..', '.evomap_device_id');
let _cachedDeviceId = null;
const DEVICE_ID_RE = /^[a-f0-9]{16,64}$/;
function isContainer() {
try {
if (fs.existsSync('/.dockerenv')) return true;
} catch {}
try {
const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
if (/docker|kubepods|containerd|cri-o|lxc|ecs/i.test(cgroup)) return true;
} catch {}
try {
if (fs.existsSync('/run/.containerenv')) return true;
} catch {}
return false;
}
function readMachineId() {
try {
const mid = fs.readFileSync('/etc/machine-id', 'utf8').trim();
if (mid && mid.length >= 16) return mid;
} catch {}
if (process.platform === 'darwin') {
try {
const { execFileSync } = require('child_process');
const raw = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
encoding: 'utf8',
timeout: 3000,
stdio: ['ignore', 'pipe', 'ignore'],
});
const match = raw.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
if (match && match[1]) return match[1];
} catch {}
}
return null;
}
// Extract Docker/OCI container ID from cgroup or mountinfo.
// The container ID is 64-char hex and stable for the lifetime of the container.
// Returns null on non-container hosts or if parsing fails.
function readContainerId() {
// Method 1: /proc/self/cgroup (works for cgroup v1 and most Docker setups)
try {
const cgroup = fs.readFileSync('/proc/self/cgroup', 'utf8');
const match = cgroup.match(/[a-f0-9]{64}/);
if (match) return match[0];
} catch {}
// Method 2: /proc/self/mountinfo (works for cgroup v2 / containerd)
try {
const mountinfo = fs.readFileSync('/proc/self/mountinfo', 'utf8');
const match = mountinfo.match(/[a-f0-9]{64}/);
if (match) return match[0];
} catch {}
// Method 3: hostname in Docker defaults to short container ID (12 hex chars)
if (isContainer()) {
const hostname = os.hostname();
if (/^[a-f0-9]{12,64}$/.test(hostname)) return hostname;
}
return null;
}
function getMacAddresses() {
const ifaces = os.networkInterfaces();
const macs = [];
for (const name of Object.keys(ifaces)) {
for (const iface of ifaces[name]) {
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
macs.push(iface.mac);
}
}
}
macs.sort();
return macs;
}
function generateDeviceId() {
const machineId = readMachineId();
if (machineId) {
return crypto.createHash('sha256').update('evomap:' + machineId).digest('hex').slice(0, 32);
}
// Container ID: stable for the container's lifetime, but changes on re-create.
// Still better than random for keeping identity within a single deployment.
const containerId = readContainerId();
if (containerId) {
return crypto.createHash('sha256').update('evomap:container:' + containerId).digest('hex').slice(0, 32);
}
const macs = getMacAddresses();
if (macs.length > 0) {
const raw = os.hostname() + '|' + macs.join(',');
return crypto.createHash('sha256').update('evomap:' + raw).digest('hex').slice(0, 32);
}
return crypto.randomBytes(16).toString('hex');
}
function persistDeviceId(id) {
// Try primary path (~/.evomap/device_id)
try {
if (!fs.existsSync(DEVICE_ID_DIR)) {
fs.mkdirSync(DEVICE_ID_DIR, { recursive: true, mode: 0o700 });
}
fs.writeFileSync(DEVICE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
return;
} catch {}
// Fallback: project-local file (useful in containers where $HOME is ephemeral
// but the project directory is mounted as a volume)
try {
fs.writeFileSync(LOCAL_DEVICE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
return;
} catch {}
console.error(
'[evolver] WARN: failed to persist device_id to ' + DEVICE_ID_FILE +
' or ' + LOCAL_DEVICE_ID_FILE +
' -- node identity may change on restart.' +
' Set EVOMAP_DEVICE_ID env var for stable identity in containers.'
);
}
function loadPersistedDeviceId() {
// Try primary path
try {
if (fs.existsSync(DEVICE_ID_FILE)) {
const id = fs.readFileSync(DEVICE_ID_FILE, 'utf8').trim();
if (id && DEVICE_ID_RE.test(id)) return id;
}
} catch {}
// Try project-local fallback
try {
if (fs.existsSync(LOCAL_DEVICE_ID_FILE)) {
const id = fs.readFileSync(LOCAL_DEVICE_ID_FILE, 'utf8').trim();
if (id && DEVICE_ID_RE.test(id)) return id;
}
} catch {}
return null;
}
function getDeviceId() {
if (_cachedDeviceId) return _cachedDeviceId;
// 1. Env var override (validated)
if (process.env.EVOMAP_DEVICE_ID) {
const envId = String(process.env.EVOMAP_DEVICE_ID).trim().toLowerCase();
if (DEVICE_ID_RE.test(envId)) {
_cachedDeviceId = envId;
return _cachedDeviceId;
}
}
// 2. Previously persisted (checks both ~/.evomap/ and project-local)
const persisted = loadPersistedDeviceId();
if (persisted) {
_cachedDeviceId = persisted;
return _cachedDeviceId;
}
// 3. Generate from hardware / container metadata and persist
const inContainer = isContainer();
const generated = generateDeviceId();
persistDeviceId(generated);
_cachedDeviceId = generated;
if (inContainer && !process.env.EVOMAP_DEVICE_ID) {
console.error(
'[evolver] NOTE: running in a container without EVOMAP_DEVICE_ID.' +
' A device_id was auto-generated and persisted, but for guaranteed' +
' cross-restart stability, set EVOMAP_DEVICE_ID as an env var' +
' or mount a persistent volume at ~/.evomap/'
);
}
return _cachedDeviceId;
}
module.exports = { getDeviceId, isContainer };
@@ -0,0 +1,68 @@
// Environment fingerprint capture for GEP assets.
// Records the runtime environment so that cross-environment diffusion
// success rates (GDI) can be measured scientifically.
const os = require('os');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { getRepoRoot } = require('./paths');
const { getDeviceId, isContainer } = require('./deviceId');
// Capture a structured environment fingerprint.
// This is embedded into Capsules, EvolutionEvents, and ValidationReports.
function captureEnvFingerprint() {
const repoRoot = getRepoRoot();
let pkgVersion = null;
let pkgName = null;
try {
const raw = fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8');
const pkg = JSON.parse(raw);
pkgVersion = pkg && pkg.version ? String(pkg.version) : null;
pkgName = pkg && pkg.name ? String(pkg.name) : null;
} catch (e) {}
const region = (process.env.EVOLVER_REGION || '').trim().toLowerCase().slice(0, 5) || undefined;
return {
device_id: getDeviceId(),
node_version: process.version,
platform: process.platform,
arch: process.arch,
os_release: os.release(),
hostname: crypto.createHash('sha256').update(os.hostname()).digest('hex').slice(0, 12),
evolver_version: pkgVersion,
client: pkgName || 'evolver',
client_version: pkgVersion,
region: region,
cwd: crypto.createHash('sha256').update(process.cwd()).digest('hex').slice(0, 12),
container: isContainer(),
};
}
// Compute a short fingerprint key for comparison and grouping.
// Two nodes with the same key are considered "same environment class".
function envFingerprintKey(fp) {
if (!fp || typeof fp !== 'object') return 'unknown';
const parts = [
fp.device_id || '',
fp.node_version || '',
fp.platform || '',
fp.arch || '',
fp.hostname || '',
fp.client || fp.evolver_version || '',
fp.client_version || fp.evolver_version || '',
].join('|');
return crypto.createHash('sha256').update(parts, 'utf8').digest('hex').slice(0, 16);
}
// Check if two fingerprints are from the same environment class.
function isSameEnvClass(fpA, fpB) {
return envFingerprintKey(fpA) === envFingerprintKey(fpB);
}
module.exports = {
captureEnvFingerprint,
envFingerprintKey,
isSameEnvClass,
};
@@ -0,0 +1,133 @@
// Hub Search-First Evolution: query evomap-hub for reusable solutions before local solve.
//
// Flow: extractSignals() -> hubSearch(signals) -> if hit: reuse; if miss: normal evolve
// Two modes: direct (skip local reasoning) | reference (inject into prompt as strong hint)
const { getNodeId } = require('./a2aProtocol');
const DEFAULT_MIN_REUSE_SCORE = 0.72;
const DEFAULT_REUSE_MODE = 'reference'; // 'direct' | 'reference'
function getHubUrl() {
return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
}
function getReuseMode() {
const m = String(process.env.EVOLVER_REUSE_MODE || DEFAULT_REUSE_MODE).toLowerCase();
return m === 'direct' ? 'direct' : 'reference';
}
function getMinReuseScore() {
const n = Number(process.env.EVOLVER_MIN_REUSE_SCORE);
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MIN_REUSE_SCORE;
}
/**
* Score a hub asset for local reuse quality.
* rank = confidence * max(success_streak, 1) * (reputation / 100)
*/
function scoreHubResult(asset) {
const confidence = Number(asset.confidence) || 0;
const streak = Math.max(Number(asset.success_streak) || 0, 1);
// Reputation is included in asset from hub ranked endpoint; default 50 if missing
const reputation = Number(asset.reputation_score) || 50;
return confidence * streak * (reputation / 100);
}
/**
* Pick the best matching asset above the threshold.
* Returns { match, score, mode } or null if nothing qualifies.
*/
function pickBestMatch(results, threshold) {
if (!Array.isArray(results) || results.length === 0) return null;
let best = null;
let bestScore = 0;
for (const asset of results) {
// Only consider promoted assets
if (asset.status && asset.status !== 'promoted') continue;
const s = scoreHubResult(asset);
if (s > bestScore) {
bestScore = s;
best = asset;
}
}
if (!best || bestScore < threshold) return null;
return {
match: best,
score: Math.round(bestScore * 1000) / 1000,
mode: getReuseMode(),
};
}
/**
* Search the hub for reusable capsules matching the given signals.
* Returns { hit: true, match, score, mode } or { hit: false }.
*/
async function hubSearch(signals, opts) {
const hubUrl = getHubUrl();
if (!hubUrl) return { hit: false, reason: 'no_hub_url' };
const signalList = Array.isArray(signals) ? signals.filter(Boolean) : [];
if (signalList.length === 0) return { hit: false, reason: 'no_signals' };
const threshold = (opts && Number.isFinite(opts.threshold)) ? opts.threshold : getMinReuseScore();
const limit = (opts && Number.isFinite(opts.limit)) ? opts.limit : 5;
const timeout = (opts && Number.isFinite(opts.timeoutMs)) ? opts.timeoutMs : 8000;
try {
const params = new URLSearchParams();
params.set('signals', signalList.join(','));
params.set('status', 'promoted');
params.set('limit', String(limit));
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
const url = `${hubUrl}/a2a/assets/search?${params.toString()}`;
const res = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) return { hit: false, reason: `hub_http_${res.status}` };
const data = await res.json();
const assets = Array.isArray(data.assets) ? data.assets : [];
if (assets.length === 0) return { hit: false, reason: 'no_results' };
const pick = pickBestMatch(assets, threshold);
if (!pick) return { hit: false, reason: 'below_threshold', candidates: assets.length };
console.log(`[HubSearch] Hit: ${pick.match.asset_id || pick.match.local_id} (score=${pick.score}, mode=${pick.mode})`);
return {
hit: true,
match: pick.match,
score: pick.score,
mode: pick.mode,
asset_id: pick.match.asset_id || null,
source_node_id: pick.match.source_node_id || null,
chain_id: pick.match.chain_id || null,
};
} catch (err) {
// Hub unreachable is non-fatal; fall through to normal evolve
console.log(`[HubSearch] Failed (non-fatal): ${err.message}`);
return { hit: false, reason: 'fetch_error', error: err.message };
}
}
module.exports = {
hubSearch,
scoreHubResult,
pickBestMatch,
getReuseMode,
getMinReuseScore,
getHubUrl,
};
@@ -0,0 +1,771 @@
const fs = require('fs');
const path = require('path');
const { getMemoryDir } = require('./paths');
const { normalizePersonalityState, isValidPersonalityState, personalityKey } = require('./personality');
const { isValidMutation, normalizeMutation } = require('./mutation');
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
} catch (e) {}
}
function stableHash(input) {
const s = String(input || '');
let h = 2166136261;
for (let i = 0; i < s.length; i++) {
h ^= s.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return (h >>> 0).toString(16).padStart(8, '0');
}
function nowIso() {
return new Date().toISOString();
}
function normalizeErrorSignature(text) {
const s = String(text || '').trim();
if (!s) return null;
return (
s
.toLowerCase()
// normalize Windows paths
.replace(/[a-z]:\\[^ \n\r\t]+/gi, '<path>')
// normalize Unix paths
.replace(/\/[^ \n\r\t]+/g, '<path>')
// normalize hex and numbers
.replace(/\b0x[0-9a-f]+\b/gi, '<hex>')
.replace(/\b\d+\b/g, '<n>')
// normalize whitespace
.replace(/\s+/g, ' ')
.slice(0, 220)
);
}
function normalizeSignalsForMatching(signals) {
const list = Array.isArray(signals) ? signals : [];
const out = [];
for (const s of list) {
const str = String(s || '').trim();
if (!str) continue;
if (str.startsWith('errsig:')) {
const norm = normalizeErrorSignature(str.slice('errsig:'.length));
if (norm) out.push(`errsig_norm:${stableHash(norm)}`);
continue;
}
out.push(str);
}
return out;
}
function computeSignalKey(signals) {
// Key must be stable across runs; normalize noisy signatures (paths, numbers).
const list = normalizeSignalsForMatching(signals);
const uniq = Array.from(new Set(list.filter(Boolean))).sort();
return uniq.join('|') || '(none)';
}
function extractErrorSignatureFromSignals(signals) {
// Convention: signals can include "errsig:<raw>" emitted by signals extractor.
const list = Array.isArray(signals) ? signals : [];
for (const s of list) {
const str = String(s || '');
if (str.startsWith('errsig:')) return normalizeErrorSignature(str.slice('errsig:'.length));
}
return null;
}
function memoryGraphPath() {
const { getEvolutionDir } = require('./paths');
const evoDir = getEvolutionDir();
return process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
}
function memoryGraphStatePath() {
const { getEvolutionDir } = require('./paths');
return path.join(getEvolutionDir(), 'memory_graph_state.json');
}
function appendJsonl(filePath, obj) {
const dir = path.dirname(filePath);
ensureDir(dir);
fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
}
function readJsonIfExists(filePath, fallback) {
try {
if (!fs.existsSync(filePath)) return fallback;
const raw = fs.readFileSync(filePath, 'utf8');
if (!raw.trim()) return fallback;
return JSON.parse(raw);
} catch (e) {
return fallback;
}
}
function writeJsonAtomic(filePath, obj) {
const dir = path.dirname(filePath);
ensureDir(dir);
const tmp = `${filePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
fs.renameSync(tmp, filePath);
}
function tryReadMemoryGraphEvents(limitLines = 2000) {
try {
const p = memoryGraphPath();
if (!fs.existsSync(p)) return [];
const raw = fs.readFileSync(p, 'utf8');
const lines = raw
.split('\n')
.map(l => l.trim())
.filter(Boolean);
const recent = lines.slice(Math.max(0, lines.length - limitLines));
return recent
.map(l => {
try {
return JSON.parse(l);
} catch (e) {
return null;
}
})
.filter(Boolean);
} catch (e) {
return [];
}
}
function jaccard(aList, bList) {
const aNorm = normalizeSignalsForMatching(aList);
const bNorm = normalizeSignalsForMatching(bList);
const a = new Set((Array.isArray(aNorm) ? aNorm : []).map(String));
const b = new Set((Array.isArray(bNorm) ? bNorm : []).map(String));
if (a.size === 0 && b.size === 0) return 1;
if (a.size === 0 || b.size === 0) return 0;
let inter = 0;
for (const x of a) if (b.has(x)) inter++;
const union = a.size + b.size - inter;
return union === 0 ? 0 : inter / union;
}
function decayWeight(updatedAtIso, halfLifeDays) {
const hl = Number(halfLifeDays);
if (!Number.isFinite(hl) || hl <= 0) return 1;
const t = Date.parse(updatedAtIso);
if (!Number.isFinite(t)) return 1;
const ageDays = (Date.now() - t) / (1000 * 60 * 60 * 24);
if (!Number.isFinite(ageDays) || ageDays <= 0) return 1;
// Exponential half-life decay: weight = 0.5^(age/hl)
return Math.pow(0.5, ageDays / hl);
}
function aggregateEdges(events) {
// Aggregate by (signal_key, gene_id) from outcome events.
// Laplace smoothing to avoid 0/1 extremes.
const map = new Map();
for (const ev of events) {
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
if (ev.kind !== 'outcome') continue;
const signalKey = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
if (!geneId) continue;
const k = `${signalKey}::${geneId}`;
const cur = map.get(k) || { signalKey, geneId, success: 0, fail: 0, last_ts: null, last_score: null };
const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
if (status === 'success') cur.success += 1;
else if (status === 'failed') cur.fail += 1;
const ts = ev.ts || ev.created_at || ev.at;
if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
cur.last_ts = ts;
cur.last_score =
ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
}
map.set(k, cur);
}
return map;
}
function aggregateGeneOutcomes(events) {
// Aggregate by gene_id from outcome events (gene -> outcome success probability).
const map = new Map();
for (const ev of events) {
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
if (ev.kind !== 'outcome') continue;
const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
if (!geneId) continue;
const cur = map.get(geneId) || { geneId, success: 0, fail: 0, last_ts: null, last_score: null };
const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
if (status === 'success') cur.success += 1;
else if (status === 'failed') cur.fail += 1;
const ts = ev.ts || ev.created_at || ev.at;
if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
cur.last_ts = ts;
cur.last_score =
ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
}
map.set(geneId, cur);
}
return map;
}
function edgeExpectedSuccess(edge, opts) {
const e = edge || { success: 0, fail: 0, last_ts: null };
const succ = Number(e.success) || 0;
const fail = Number(e.fail) || 0;
const total = succ + fail;
const p = (succ + 1) / (total + 2); // Laplace smoothing
const halfLifeDays = opts && Number.isFinite(Number(opts.half_life_days)) ? Number(opts.half_life_days) : 30;
const w = decayWeight(e.last_ts || '', halfLifeDays);
return { p, w, total, value: p * w };
}
function getMemoryAdvice({ signals, genes, driftEnabled }) {
const events = tryReadMemoryGraphEvents(2000);
const edges = aggregateEdges(events);
const geneOutcomes = aggregateGeneOutcomes(events);
const curSignals = Array.isArray(signals) ? signals : [];
const curKey = computeSignalKey(curSignals);
const bannedGeneIds = new Set();
const scoredGeneIds = [];
// Similarity: consider exact key first, then any key with overlap.
const seenKeys = new Set();
const candidateKeys = [];
candidateKeys.push({ key: curKey, sim: 1 });
seenKeys.add(curKey);
for (const ev of events) {
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
const k = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
if (seenKeys.has(k)) continue;
const sigs = ev.signal && Array.isArray(ev.signal.signals) ? ev.signal.signals : [];
const sim = jaccard(curSignals, sigs);
if (sim >= 0.34) {
candidateKeys.push({ key: k, sim });
seenKeys.add(k);
}
}
const byGene = new Map();
for (const ck of candidateKeys) {
for (const g of Array.isArray(genes) ? genes : []) {
if (!g || g.type !== 'Gene' || !g.id) continue;
const k = `${ck.key}::${g.id}`;
const edge = edges.get(k);
const cur = byGene.get(g.id) || { geneId: g.id, best: 0, attempts: 0, prior: 0, prior_attempts: 0 };
// Signal->Gene edge score (if available)
if (edge) {
const ex = edgeExpectedSuccess(edge, { half_life_days: 30 });
const weighted = ex.value * ck.sim;
if (weighted > cur.best) cur.best = weighted;
cur.attempts = Math.max(cur.attempts, ex.total);
}
// Gene->Outcome prior (independent of signal): stabilizer when signal edges are sparse.
const gEdge = geneOutcomes.get(String(g.id));
if (gEdge) {
const gx = edgeExpectedSuccess(gEdge, { half_life_days: 45 });
cur.prior = Math.max(cur.prior, gx.value);
cur.prior_attempts = Math.max(cur.prior_attempts, gx.total);
}
byGene.set(g.id, cur);
}
}
for (const [geneId, info] of byGene.entries()) {
const combined = info.best > 0 ? info.best + info.prior * 0.12 : info.prior * 0.4;
scoredGeneIds.push({ geneId, score: combined, attempts: info.attempts, prior: info.prior });
// Low-efficiency path suppression (unless drift is explicit).
if (!driftEnabled && info.attempts >= 2 && info.best < 0.18) {
bannedGeneIds.add(geneId);
}
// Also suppress genes with consistently poor global outcomes when signal edges are sparse.
if (!driftEnabled && info.attempts < 2 && info.prior_attempts >= 3 && info.prior < 0.12) {
bannedGeneIds.add(geneId);
}
}
scoredGeneIds.sort((a, b) => b.score - a.score);
const preferredGeneId = scoredGeneIds.length ? scoredGeneIds[0].geneId : null;
const explanation = [];
if (preferredGeneId) explanation.push(`memory_prefer:${preferredGeneId}`);
if (bannedGeneIds.size) explanation.push(`memory_ban:${Array.from(bannedGeneIds).slice(0, 6).join(',')}`);
if (preferredGeneId) {
const top = scoredGeneIds.find(x => x && x.geneId === preferredGeneId);
if (top && Number.isFinite(Number(top.prior)) && top.prior > 0) explanation.push(`gene_prior:${top.prior.toFixed(3)}`);
}
if (driftEnabled) explanation.push('random_drift:enabled');
return {
currentSignalKey: curKey,
preferredGeneId,
bannedGeneIds,
explanation,
};
}
function recordSignalSnapshot({ signals, observations }) {
const signalKey = computeSignalKey(signals);
const ts = nowIso();
const errsig = extractErrorSignatureFromSignals(signals);
const ev = {
type: 'MemoryGraphEvent',
kind: 'signal',
id: `mge_${Date.now()}_${stableHash(`${signalKey}|signal|${ts}`)}`,
ts,
signal: {
key: signalKey,
signals: Array.isArray(signals) ? signals : [],
error_signature: errsig || null,
},
observed: observations && typeof observations === 'object' ? observations : null,
};
appendJsonl(memoryGraphPath(), ev);
return ev;
}
function buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }) {
const sigCount = Array.isArray(signals) ? signals.length : 0;
const drift = driftEnabled ? 'drift' : 'directed';
const g = geneId ? `${geneId}${geneCategory ? `(${geneCategory})` : ''}` : '(none)';
return `Given signal_key=${signalKey} with ${sigCount} signals, selecting gene=${g} under mode=${drift} is expected to reduce repeated errors and improve stability.`;
}
function recordHypothesis({
signals,
mutation,
personality_state,
selectedGene,
selector,
driftEnabled,
selectedBy,
capsulesUsed,
observations,
}) {
const signalKey = computeSignalKey(signals);
const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
const ts = nowIso();
const errsig = extractErrorSignatureFromSignals(signals);
const hypothesisId = `hyp_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
const personalityState = personality_state || null;
const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
const ev = {
type: 'MemoryGraphEvent',
kind: 'hypothesis',
id: `mge_${Date.now()}_${stableHash(`${hypothesisId}|${ts}`)}`,
ts,
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
hypothesis: {
id: hypothesisId,
text: buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }),
predicted_outcome: { status: null, score: null },
},
mutation: mutNorm
? {
id: mutNorm.id,
category: mutNorm.category,
trigger_signals: mutNorm.trigger_signals,
target: mutNorm.target,
expected_effect: mutNorm.expected_effect,
risk_level: mutNorm.risk_level,
}
: null,
personality: psNorm
? {
key: personalityKey(psNorm),
state: psNorm,
}
: null,
gene: { id: geneId, category: geneCategory },
action: {
drift: !!driftEnabled,
selected_by: selectedBy || 'selector',
selector: selector || null,
},
capsules: {
used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
},
observed: observations && typeof observations === 'object' ? observations : null,
};
appendJsonl(memoryGraphPath(), ev);
return { hypothesisId, signalKey };
}
function hasErrorSignal(signals) {
const list = Array.isArray(signals) ? signals : [];
return list.includes('log_error');
}
function recordAttempt({
signals,
mutation,
personality_state,
selectedGene,
selector,
driftEnabled,
selectedBy,
hypothesisId,
capsulesUsed,
observations,
}) {
const signalKey = computeSignalKey(signals);
const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
const ts = nowIso();
const errsig = extractErrorSignatureFromSignals(signals);
const actionId = `act_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
const personalityState = personality_state || null;
const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
const ev = {
type: 'MemoryGraphEvent',
kind: 'attempt',
id: `mge_${Date.now()}_${stableHash(actionId)}`,
ts,
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
mutation: mutNorm
? {
id: mutNorm.id,
category: mutNorm.category,
trigger_signals: mutNorm.trigger_signals,
target: mutNorm.target,
expected_effect: mutNorm.expected_effect,
risk_level: mutNorm.risk_level,
}
: null,
personality: psNorm
? {
key: personalityKey(psNorm),
state: psNorm,
}
: null,
gene: { id: geneId, category: geneCategory },
hypothesis: hypothesisId ? { id: String(hypothesisId) } : null,
action: {
id: actionId,
drift: !!driftEnabled,
selected_by: selectedBy || 'selector',
selector: selector || null,
},
capsules: {
used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
},
observed: observations && typeof observations === 'object' ? observations : null,
};
appendJsonl(memoryGraphPath(), ev);
// State is mutable; graph is append-only.
const statePath = memoryGraphStatePath();
const state = readJsonIfExists(statePath, { last_action: null });
state.last_action = {
action_id: actionId,
signal_key: signalKey,
signals: Array.isArray(signals) ? signals : [],
mutation_id: mutNorm ? mutNorm.id : null,
mutation_category: mutNorm ? mutNorm.category : null,
mutation_risk_level: mutNorm ? mutNorm.risk_level : null,
personality_key: psNorm ? personalityKey(psNorm) : null,
personality_state: psNorm || null,
gene_id: geneId,
gene_category: geneCategory,
hypothesis_id: hypothesisId ? String(hypothesisId) : null,
capsules_used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
had_error: hasErrorSignal(signals),
created_at: ts,
outcome_recorded: false,
baseline_observed: observations && typeof observations === 'object' ? observations : null,
};
writeJsonAtomic(statePath, state);
return { actionId, signalKey };
}
function inferOutcomeFromSignals({ prevHadError, currentHasError }) {
if (prevHadError && !currentHasError) return { status: 'success', score: 0.85, note: 'error_cleared' };
if (prevHadError && currentHasError) return { status: 'failed', score: 0.2, note: 'error_persisted' };
if (!prevHadError && currentHasError) return { status: 'failed', score: 0.15, note: 'new_error_appeared' };
return { status: 'success', score: 0.6, note: 'stable_no_error' };
}
function clamp01(x) {
const n = Number(x);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(1, n));
}
function tryParseLastEvolutionEventOutcome(evidenceText) {
// Scan tail text for an EvolutionEvent JSON line and extract its outcome.
const s = String(evidenceText || '');
if (!s) return null;
const lines = s.split('\n').slice(-400);
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (!line) continue;
if (!line.includes('"type"') || !line.includes('EvolutionEvent')) continue;
try {
const obj = JSON.parse(line);
if (!obj || obj.type !== 'EvolutionEvent') continue;
const o = obj.outcome && typeof obj.outcome === 'object' ? obj.outcome : null;
if (!o) continue;
const status = o.status === 'success' || o.status === 'failed' ? o.status : null;
const score = Number.isFinite(Number(o.score)) ? clamp01(Number(o.score)) : null;
if (!status && score == null) continue;
return {
status: status || (score != null && score >= 0.5 ? 'success' : 'failed'),
score: score != null ? score : status === 'success' ? 0.75 : 0.25,
note: 'evolutionevent_observed',
};
} catch (e) {
continue;
}
}
return null;
}
function inferOutcomeEnhanced({ prevHadError, currentHasError, baselineObserved, currentObserved }) {
const evidence =
currentObserved &&
currentObserved.evidence &&
(currentObserved.evidence.recent_session_tail || currentObserved.evidence.today_log_tail)
? currentObserved.evidence
: null;
const combinedEvidence = evidence
? `${String(evidence.recent_session_tail || '')}\n${String(evidence.today_log_tail || '')}`
: '';
const observed = tryParseLastEvolutionEventOutcome(combinedEvidence);
if (observed) return observed;
const base = inferOutcomeFromSignals({ prevHadError, currentHasError });
const prevErrCount =
baselineObserved && Number.isFinite(Number(baselineObserved.recent_error_count))
? Number(baselineObserved.recent_error_count)
: null;
const curErrCount =
currentObserved && Number.isFinite(Number(currentObserved.recent_error_count))
? Number(currentObserved.recent_error_count)
: null;
let score = base.score;
if (prevErrCount != null && curErrCount != null) {
const delta = prevErrCount - curErrCount;
score += Math.max(-0.12, Math.min(0.12, delta / 50));
}
const prevScan =
baselineObserved && Number.isFinite(Number(baselineObserved.scan_ms)) ? Number(baselineObserved.scan_ms) : null;
const curScan =
currentObserved && Number.isFinite(Number(currentObserved.scan_ms)) ? Number(currentObserved.scan_ms) : null;
if (prevScan != null && curScan != null && prevScan > 0) {
const ratio = (prevScan - curScan) / prevScan;
score += Math.max(-0.06, Math.min(0.06, ratio));
}
return { status: base.status, score: clamp01(score), note: `${base.note}|heuristic_delta` };
}
function buildConfidenceEdgeEvent({ signalKey, signals, geneId, geneCategory, outcomeEventId, halfLifeDays }) {
const events = tryReadMemoryGraphEvents(2000);
const edges = aggregateEdges(events);
const k = `${signalKey}::${geneId}`;
const edge = edges.get(k) || { success: 0, fail: 0, last_ts: null };
const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
const ts = nowIso();
return {
type: 'MemoryGraphEvent',
kind: 'confidence_edge',
id: `mge_${Date.now()}_${stableHash(`${signalKey}|${geneId}|confidence|${ts}`)}`,
ts,
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
gene: { id: geneId, category: geneCategory || null },
edge: { signal_key: signalKey, gene_id: geneId },
stats: {
success: Number(edge.success) || 0,
fail: Number(edge.fail) || 0,
attempts: Number(ex.total) || 0,
p: ex.p,
decay_weight: ex.w,
value: ex.value,
half_life_days: halfLifeDays,
updated_at: ts,
},
derived_from: { outcome_event_id: outcomeEventId || null },
};
}
function buildGeneOutcomeConfidenceEvent({ geneId, geneCategory, outcomeEventId, halfLifeDays }) {
const events = tryReadMemoryGraphEvents(2000);
const geneOutcomes = aggregateGeneOutcomes(events);
const edge = geneOutcomes.get(String(geneId)) || { success: 0, fail: 0, last_ts: null };
const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
const ts = nowIso();
return {
type: 'MemoryGraphEvent',
kind: 'confidence_gene_outcome',
id: `mge_${Date.now()}_${stableHash(`${geneId}|gene_outcome|confidence|${ts}`)}`,
ts,
gene: { id: String(geneId), category: geneCategory || null },
edge: { gene_id: String(geneId) },
stats: {
success: Number(edge.success) || 0,
fail: Number(edge.fail) || 0,
attempts: Number(ex.total) || 0,
p: ex.p,
decay_weight: ex.w,
value: ex.value,
half_life_days: halfLifeDays,
updated_at: ts,
},
derived_from: { outcome_event_id: outcomeEventId || null },
};
}
function recordOutcomeFromState({ signals, observations }) {
const statePath = memoryGraphStatePath();
const state = readJsonIfExists(statePath, { last_action: null });
const last = state && state.last_action ? state.last_action : null;
if (!last || !last.action_id) return null;
if (last.outcome_recorded) return null;
const currentHasError = hasErrorSignal(signals);
const inferred = inferOutcomeEnhanced({
prevHadError: !!last.had_error,
currentHasError,
baselineObserved: last.baseline_observed || null,
currentObserved: observations || null,
});
const ts = nowIso();
const errsig = extractErrorSignatureFromSignals(signals);
const ev = {
type: 'MemoryGraphEvent',
kind: 'outcome',
id: `mge_${Date.now()}_${stableHash(`${last.action_id}|outcome|${ts}`)}`,
ts,
signal: {
key: String(last.signal_key || '(none)'),
signals: Array.isArray(last.signals) ? last.signals : [],
error_signature: errsig || null,
},
mutation:
last.mutation_id || last.mutation_category || last.mutation_risk_level
? {
id: last.mutation_id || null,
category: last.mutation_category || null,
risk_level: last.mutation_risk_level || null,
}
: null,
personality:
last.personality_key || last.personality_state
? {
key: last.personality_key || null,
state: last.personality_state || null,
}
: null,
gene: { id: last.gene_id || null, category: last.gene_category || null },
action: { id: String(last.action_id) },
hypothesis: last.hypothesis_id ? { id: String(last.hypothesis_id) } : null,
outcome: {
status: inferred.status,
score: inferred.score,
note: inferred.note,
observed: { current_signals: Array.isArray(signals) ? signals : [] },
},
confidence: {
// This is an interpretable, decayed success estimate derived from outcomes; aggregation is computed at read-time.
half_life_days: 30,
},
observed: observations && typeof observations === 'object' ? observations : null,
baseline: last.baseline_observed || null,
capsules: {
used: Array.isArray(last.capsules_used) ? last.capsules_used : [],
},
};
appendJsonl(memoryGraphPath(), ev);
// Persist explicit confidence snapshots (append-only) for auditability.
try {
if (last.gene_id) {
const edgeEv = buildConfidenceEdgeEvent({
signalKey: String(last.signal_key || '(none)'),
signals: Array.isArray(last.signals) ? last.signals : [],
geneId: String(last.gene_id),
geneCategory: last.gene_category || null,
outcomeEventId: ev.id,
halfLifeDays: 30,
});
appendJsonl(memoryGraphPath(), edgeEv);
const geneEv = buildGeneOutcomeConfidenceEvent({
geneId: String(last.gene_id),
geneCategory: last.gene_category || null,
outcomeEventId: ev.id,
halfLifeDays: 45,
});
appendJsonl(memoryGraphPath(), geneEv);
}
} catch (e) {}
last.outcome_recorded = true;
last.outcome_recorded_at = ts;
state.last_action = last;
writeJsonAtomic(statePath, state);
return ev;
}
function recordExternalCandidate({ asset, source, signals }) {
// Append-only annotation: external assets enter as candidates only.
// This does not affect outcome aggregation (which only uses kind === 'outcome').
const a = asset && typeof asset === 'object' ? asset : null;
const type = a && a.type ? String(a.type) : null;
const id = a && a.id ? String(a.id) : null;
if (!type || !id) return null;
const ts = nowIso();
const signalKey = computeSignalKey(signals);
const ev = {
type: 'MemoryGraphEvent',
kind: 'external_candidate',
id: `mge_${Date.now()}_${stableHash(`${type}|${id}|external|${ts}`)}`,
ts,
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
external: {
source: source || 'external',
received_at: ts,
},
asset: { type, id },
candidate: {
// Minimal hints for later local triggering/validation.
trigger: type === 'Capsule' && Array.isArray(a.trigger) ? a.trigger : [],
gene: type === 'Capsule' && a.gene ? String(a.gene) : null,
confidence: type === 'Capsule' && Number.isFinite(Number(a.confidence)) ? Number(a.confidence) : null,
},
};
appendJsonl(memoryGraphPath(), ev);
return ev;
}
module.exports = {
memoryGraphPath,
computeSignalKey,
tryReadMemoryGraphEvents,
getMemoryAdvice,
recordSignalSnapshot,
recordHypothesis,
recordAttempt,
recordOutcomeFromState,
recordExternalCandidate,
};
@@ -0,0 +1,203 @@
// ---------------------------------------------------------------------------
// MemoryGraphAdapter -- stable interface boundary for memory graph operations.
//
// Default implementation delegates to the local JSONL-based memoryGraph.js.
// SaaS providers can supply a remote adapter by setting MEMORY_GRAPH_PROVIDER=remote
// and configuring MEMORY_GRAPH_REMOTE_URL / MEMORY_GRAPH_REMOTE_KEY.
//
// The adapter is designed so that the open-source evolver always works offline
// with the local implementation. Remote is optional and degrades gracefully.
// ---------------------------------------------------------------------------
const localGraph = require('./memoryGraph');
// ---------------------------------------------------------------------------
// Adapter interface contract (all methods must be implemented by providers):
//
// getAdvice({ signals, genes, driftEnabled }) => { preferredGeneId, bannedGeneIds, currentSignalKey, explanation }
// recordSignalSnapshot({ signals, observations }) => event
// recordHypothesis({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, capsulesUsed, observations }) => { hypothesisId, signalKey }
// recordAttempt({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, hypothesisId, capsulesUsed, observations }) => { actionId, signalKey }
// recordOutcome({ signals, observations }) => event | null
// recordExternalCandidate({ asset, source, signals }) => event | null
// memoryGraphPath() => string
// computeSignalKey(signals) => string
// tryReadMemoryGraphEvents(limit) => event[]
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Local adapter (default) -- wraps memoryGraph.js without any behavior change
// ---------------------------------------------------------------------------
const localAdapter = {
name: 'local',
getAdvice(opts) {
return localGraph.getMemoryAdvice(opts);
},
recordSignalSnapshot(opts) {
return localGraph.recordSignalSnapshot(opts);
},
recordHypothesis(opts) {
return localGraph.recordHypothesis(opts);
},
recordAttempt(opts) {
return localGraph.recordAttempt(opts);
},
recordOutcome(opts) {
return localGraph.recordOutcomeFromState(opts);
},
recordExternalCandidate(opts) {
return localGraph.recordExternalCandidate(opts);
},
memoryGraphPath() {
return localGraph.memoryGraphPath();
},
computeSignalKey(signals) {
return localGraph.computeSignalKey(signals);
},
tryReadMemoryGraphEvents(limit) {
return localGraph.tryReadMemoryGraphEvents(limit);
},
};
// ---------------------------------------------------------------------------
// Remote adapter (SaaS) -- calls external KG service with local fallback
// ---------------------------------------------------------------------------
function buildRemoteAdapter() {
const remoteUrl = process.env.MEMORY_GRAPH_REMOTE_URL || '';
const remoteKey = process.env.MEMORY_GRAPH_REMOTE_KEY || '';
const timeoutMs = Number(process.env.MEMORY_GRAPH_REMOTE_TIMEOUT_MS) || 5000;
async function remoteCall(endpoint, body) {
if (!remoteUrl) throw new Error('MEMORY_GRAPH_REMOTE_URL not configured');
const url = `${remoteUrl.replace(/\/+$/, '')}${endpoint}`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(remoteKey ? { Authorization: `Bearer ${remoteKey}` } : {}),
},
body: JSON.stringify(body),
signal: controller.signal,
});
if (!res.ok) {
throw new Error(`remote_kg_error: ${res.status}`);
}
return await res.json();
} finally {
clearTimeout(timer);
}
}
// Wrap remote call with local fallback -- ensures offline resilience.
function withFallback(localFn, remoteFn) {
return async function (...args) {
try {
return await remoteFn(...args);
} catch (e) {
// Fallback to local on any remote failure (network, timeout, config).
return localFn(...args);
}
};
}
return {
name: 'remote',
// getAdvice is the primary candidate for remote enhancement (richer graph reasoning).
getAdvice: withFallback(
(opts) => localGraph.getMemoryAdvice(opts),
async (opts) => {
const result = await remoteCall('/kg/advice', {
signals: opts.signals,
genes: (opts.genes || []).map((g) => ({ id: g.id, category: g.category, type: g.type })),
driftEnabled: opts.driftEnabled,
});
// Normalize remote response to match local contract.
return {
currentSignalKey: result.currentSignalKey || localGraph.computeSignalKey(opts.signals),
preferredGeneId: result.preferredGeneId || null,
bannedGeneIds: new Set(result.bannedGeneIds || []),
explanation: Array.isArray(result.explanation) ? result.explanation : [],
};
}
),
// Write operations: always write locally first, then async-sync to remote.
// This preserves the append-only local graph as source of truth.
recordSignalSnapshot(opts) {
const ev = localGraph.recordSignalSnapshot(opts);
remoteCall('/kg/ingest', { kind: 'signal', event: ev }).catch(() => {});
return ev;
},
recordHypothesis(opts) {
const result = localGraph.recordHypothesis(opts);
remoteCall('/kg/ingest', { kind: 'hypothesis', event: result }).catch(() => {});
return result;
},
recordAttempt(opts) {
const result = localGraph.recordAttempt(opts);
remoteCall('/kg/ingest', { kind: 'attempt', event: result }).catch(() => {});
return result;
},
recordOutcome(opts) {
const ev = localGraph.recordOutcomeFromState(opts);
if (ev) {
remoteCall('/kg/ingest', { kind: 'outcome', event: ev }).catch(() => {});
}
return ev;
},
recordExternalCandidate(opts) {
const ev = localGraph.recordExternalCandidate(opts);
if (ev) {
remoteCall('/kg/ingest', { kind: 'external_candidate', event: ev }).catch(() => {});
}
return ev;
},
memoryGraphPath() {
return localGraph.memoryGraphPath();
},
computeSignalKey(signals) {
return localGraph.computeSignalKey(signals);
},
tryReadMemoryGraphEvents(limit) {
return localGraph.tryReadMemoryGraphEvents(limit);
},
};
}
// ---------------------------------------------------------------------------
// Provider resolution
// ---------------------------------------------------------------------------
function resolveAdapter() {
const provider = (process.env.MEMORY_GRAPH_PROVIDER || 'local').toLowerCase().trim();
if (provider === 'remote') {
return buildRemoteAdapter();
}
return localAdapter;
}
const adapter = resolveAdapter();
module.exports = adapter;
@@ -0,0 +1,186 @@
function clamp01(x) {
const n = Number(x);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(1, n));
}
function nowTsMs() {
return Date.now();
}
function uniqStrings(list) {
const out = [];
const seen = new Set();
for (const x of Array.isArray(list) ? list : []) {
const s = String(x || '').trim();
if (!s) continue;
if (seen.has(s)) continue;
seen.add(s);
out.push(s);
}
return out;
}
function hasErrorishSignal(signals) {
const list = Array.isArray(signals) ? signals.map(s => String(s || '')) : [];
if (list.includes('issue_already_resolved') || list.includes('openclaw_self_healed')) return false;
if (list.includes('log_error')) return true;
if (list.some(s => s.startsWith('errsig:') || s.startsWith('errsig_norm:'))) return true;
return false;
}
// Opportunity signals that indicate a chance to innovate (not just fix).
var OPPORTUNITY_SIGNALS = [
'user_feature_request',
'user_improvement_suggestion',
'perf_bottleneck',
'capability_gap',
'stable_success_plateau',
'external_opportunity',
'issue_already_resolved',
'openclaw_self_healed',
'empty_cycle_loop_detected',
];
function hasOpportunitySignal(signals) {
var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : [];
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
var name = OPPORTUNITY_SIGNALS[i];
if (list.includes(name)) return true;
if (list.some(function (s) { return s.startsWith(name + ':'); })) return true;
}
return false;
}
function mutationCategoryFromContext({ signals, driftEnabled }) {
if (hasErrorishSignal(signals)) return 'repair';
if (driftEnabled) return 'innovate';
// Auto-innovate: opportunity signals present and no errors
if (hasOpportunitySignal(signals)) return 'innovate';
// Consult strategy preset: if the configured strategy favors innovation,
// default to innovate instead of optimize when there is nothing specific to do.
try {
var strategy = require('./strategy').resolveStrategy();
if (strategy && typeof strategy.innovate === 'number' && strategy.innovate >= 0.5) return 'innovate';
} catch (_) {}
return 'optimize';
}
function expectedEffectFromCategory(category) {
const c = String(category || '');
if (c === 'repair') return 'reduce runtime errors, increase stability, and lower failure rate';
if (c === 'optimize') return 'improve success rate and reduce repeated operational cost';
if (c === 'innovate') return 'explore new strategy combinations to escape local optimum';
return 'improve robustness and success probability';
}
function targetFromGene(selectedGene) {
if (selectedGene && selectedGene.id) return `gene:${String(selectedGene.id)}`;
return 'behavior:protocol';
}
function isHighRiskPersonality(p) {
// Conservative definition: low rigor or high risk_tolerance is treated as high-risk personality.
const rigor = p && Number.isFinite(Number(p.rigor)) ? Number(p.rigor) : null;
const riskTol = p && Number.isFinite(Number(p.risk_tolerance)) ? Number(p.risk_tolerance) : null;
if (rigor != null && rigor < 0.5) return true;
if (riskTol != null && riskTol > 0.6) return true;
return false;
}
function isHighRiskMutationAllowed(personalityState) {
const rigor = personalityState && Number.isFinite(Number(personalityState.rigor)) ? Number(personalityState.rigor) : 0;
const riskTol =
personalityState && Number.isFinite(Number(personalityState.risk_tolerance))
? Number(personalityState.risk_tolerance)
: 1;
return rigor >= 0.6 && riskTol <= 0.5;
}
function buildMutation({
signals,
selectedGene,
driftEnabled,
personalityState,
allowHighRisk = false,
target,
expected_effect,
} = {}) {
const ts = nowTsMs();
const category = mutationCategoryFromContext({ signals, driftEnabled: !!driftEnabled });
const triggerSignals = uniqStrings(signals);
const base = {
type: 'Mutation',
id: `mut_${ts}`,
category,
trigger_signals: triggerSignals,
target: String(target || targetFromGene(selectedGene)),
expected_effect: String(expected_effect || expectedEffectFromCategory(category)),
risk_level: 'low',
};
// Default risk assignment: innovate is medium; others low.
if (category === 'innovate') base.risk_level = 'medium';
// Optional high-risk escalation (rare, and guarded by strict safety constraints).
if (allowHighRisk && category === 'innovate') {
base.risk_level = 'high';
}
// Safety constraints (hard):
// - forbid innovate + high-risk personality (downgrade innovation to optimize)
// - forbid high-risk mutation unless personality satisfies constraints
const highRiskPersonality = isHighRiskPersonality(personalityState || null);
if (base.category === 'innovate' && highRiskPersonality) {
base.category = 'optimize';
base.expected_effect = 'safety downgrade: optimize under high-risk personality (avoid innovate+high-risk combo)';
base.risk_level = 'low';
base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:avoid_innovate_with_high_risk_personality']);
}
if (base.risk_level === 'high' && !isHighRiskMutationAllowed(personalityState || null)) {
// Downgrade rather than emit illegal high-risk mutation.
base.risk_level = 'medium';
base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:downgrade_high_risk']);
}
return base;
}
function isValidMutation(obj) {
if (!obj || typeof obj !== 'object') return false;
if (obj.type !== 'Mutation') return false;
if (!obj.id || typeof obj.id !== 'string') return false;
if (!obj.category || !['repair', 'optimize', 'innovate'].includes(String(obj.category))) return false;
if (!Array.isArray(obj.trigger_signals)) return false;
if (!obj.target || typeof obj.target !== 'string') return false;
if (!obj.expected_effect || typeof obj.expected_effect !== 'string') return false;
if (!obj.risk_level || !['low', 'medium', 'high'].includes(String(obj.risk_level))) return false;
return true;
}
function normalizeMutation(obj) {
const m = obj && typeof obj === 'object' ? obj : {};
const out = {
type: 'Mutation',
id: typeof m.id === 'string' ? m.id : `mut_${nowTsMs()}`,
category: ['repair', 'optimize', 'innovate'].includes(String(m.category)) ? String(m.category) : 'optimize',
trigger_signals: uniqStrings(m.trigger_signals),
target: typeof m.target === 'string' ? m.target : 'behavior:protocol',
expected_effect: typeof m.expected_effect === 'string' ? m.expected_effect : expectedEffectFromCategory(m.category),
risk_level: ['low', 'medium', 'high'].includes(String(m.risk_level)) ? String(m.risk_level) : 'low',
};
return out;
}
module.exports = {
clamp01,
buildMutation,
isValidMutation,
normalizeMutation,
isHighRiskMutationAllowed,
isHighRiskPersonality,
hasOpportunitySignal,
};
@@ -0,0 +1,67 @@
const path = require('path');
function getRepoRoot() {
// src/gep/paths.js -> repo root
return path.resolve(__dirname, '..', '..');
}
function getWorkspaceRoot() {
// skills/evolver -> workspace root
return path.resolve(getRepoRoot(), '..', '..');
}
function getLogsDir() {
return process.env.EVOLVER_LOGS_DIR || path.join(getWorkspaceRoot(), 'logs');
}
function getMemoryDir() {
return process.env.MEMORY_DIR || path.join(getWorkspaceRoot(), 'memory');
}
// --- Session Scope Isolation ---
// When EVOLVER_SESSION_SCOPE is set (e.g., to a Discord channel ID or project name),
// evolution state, memory graph, and assets are isolated to a per-scope subdirectory.
// This prevents cross-channel/cross-project memory contamination.
// When NOT set, everything works as before (global scope, backward compatible).
function getSessionScope() {
const raw = String(process.env.EVOLVER_SESSION_SCOPE || '').trim();
if (!raw) return null;
// Sanitize: only allow alphanumeric, dash, underscore, dot (prevent path traversal).
const safe = raw.replace(/[^a-zA-Z0-9_\-\.]/g, '_').slice(0, 128);
return safe || null;
}
function getEvolutionDir() {
const baseDir = process.env.EVOLUTION_DIR || path.join(getMemoryDir(), 'evolution');
const scope = getSessionScope();
if (scope) {
return path.join(baseDir, 'scopes', scope);
}
return baseDir;
}
function getGepAssetsDir() {
const repoRoot = getRepoRoot();
const baseDir = process.env.GEP_ASSETS_DIR || path.join(repoRoot, 'assets', 'gep');
const scope = getSessionScope();
if (scope) {
return path.join(baseDir, 'scopes', scope);
}
return baseDir;
}
function getSkillsDir() {
return process.env.SKILLS_DIR || path.join(getWorkspaceRoot(), 'skills');
}
module.exports = {
getRepoRoot,
getWorkspaceRoot,
getLogsDir,
getMemoryDir,
getEvolutionDir,
getGepAssetsDir,
getSkillsDir,
getSessionScope,
};
@@ -0,0 +1,355 @@
const fs = require('fs');
const path = require('path');
const { getMemoryDir } = require('./paths');
const { hasOpportunitySignal } = require('./mutation');
function nowIso() {
return new Date().toISOString();
}
function clamp01(x) {
const n = Number(x);
if (!Number.isFinite(n)) return 0;
return Math.max(0, Math.min(1, n));
}
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
} catch (e) {}
}
function readJsonIfExists(filePath, fallback) {
try {
if (!fs.existsSync(filePath)) return fallback;
const raw = fs.readFileSync(filePath, 'utf8');
if (!raw.trim()) return fallback;
return JSON.parse(raw);
} catch {
return fallback;
}
}
function writeJsonAtomic(filePath, obj) {
const dir = path.dirname(filePath);
ensureDir(dir);
const tmp = `${filePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
fs.renameSync(tmp, filePath);
}
function personalityFilePath() {
const memoryDir = getMemoryDir();
const { getEvolutionDir } = require('./paths'); return path.join(getEvolutionDir(), 'personality_state.json');
}
function defaultPersonalityState() {
// Conservative defaults: protocol-first, safe, low-risk.
return {
type: 'PersonalityState',
rigor: 0.7,
creativity: 0.35,
verbosity: 0.25,
risk_tolerance: 0.4,
obedience: 0.85,
};
}
function normalizePersonalityState(state) {
const s = state && typeof state === 'object' ? state : {};
return {
type: 'PersonalityState',
rigor: clamp01(s.rigor),
creativity: clamp01(s.creativity),
verbosity: clamp01(s.verbosity),
risk_tolerance: clamp01(s.risk_tolerance),
obedience: clamp01(s.obedience),
};
}
function isValidPersonalityState(obj) {
if (!obj || typeof obj !== 'object') return false;
if (obj.type !== 'PersonalityState') return false;
for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
const v = obj[k];
if (!Number.isFinite(Number(v))) return false;
const n = Number(v);
if (n < 0 || n > 1) return false;
}
return true;
}
function roundToStep(x, step) {
const s = Number(step);
if (!Number.isFinite(s) || s <= 0) return x;
return Math.round(Number(x) / s) * s;
}
function personalityKey(state) {
const s = normalizePersonalityState(state);
const step = 0.1;
const r = roundToStep(s.rigor, step).toFixed(1);
const c = roundToStep(s.creativity, step).toFixed(1);
const v = roundToStep(s.verbosity, step).toFixed(1);
const rt = roundToStep(s.risk_tolerance, step).toFixed(1);
const o = roundToStep(s.obedience, step).toFixed(1);
return `rigor=${r}|creativity=${c}|verbosity=${v}|risk_tolerance=${rt}|obedience=${o}`;
}
function getParamDeltas(fromState, toState) {
const a = normalizePersonalityState(fromState);
const b = normalizePersonalityState(toState);
const deltas = [];
for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
deltas.push({ param: k, delta: Number(b[k]) - Number(a[k]) });
}
deltas.sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta));
return deltas;
}
function personalityScore(statsEntry) {
const e = statsEntry && typeof statsEntry === 'object' ? statsEntry : {};
const succ = Number(e.success) || 0;
const fail = Number(e.fail) || 0;
const total = succ + fail;
// Laplace-smoothed success probability
const p = (succ + 1) / (total + 2);
// Penalize tiny-sample overconfidence
const sampleWeight = Math.min(1, total / 8);
// Use avg_score (if present) as mild quality proxy
const avg = Number.isFinite(Number(e.avg_score)) ? Number(e.avg_score) : null;
const q = avg == null ? 0.5 : clamp01(avg);
return p * 0.75 + q * 0.25 * sampleWeight;
}
function chooseBestKnownPersonality(statsByKey) {
const stats = statsByKey && typeof statsByKey === 'object' ? statsByKey : {};
let best = null;
for (const [k, entry] of Object.entries(stats)) {
const e = entry || {};
const total = (Number(e.success) || 0) + (Number(e.fail) || 0);
if (total < 3) continue;
const sc = personalityScore(e);
if (!best || sc > best.score) best = { key: k, score: sc, entry: e };
}
return best;
}
function parseKeyToState(key) {
// key format: rigor=0.7|creativity=0.3|...
const out = defaultPersonalityState();
const parts = String(key || '').split('|').map(s => s.trim()).filter(Boolean);
for (const p of parts) {
const [k, v] = p.split('=').map(x => String(x || '').trim());
if (!k) continue;
if (!['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience'].includes(k)) continue;
out[k] = clamp01(Number(v));
}
return normalizePersonalityState(out);
}
function applyPersonalityMutations(state, mutations) {
let cur = normalizePersonalityState(state);
const muts = Array.isArray(mutations) ? mutations : [];
const applied = [];
let count = 0;
for (const m of muts) {
if (!m || typeof m !== 'object') continue;
const param = String(m.param || '').trim();
if (!['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience'].includes(param)) continue;
const delta = Number(m.delta);
if (!Number.isFinite(delta)) continue;
const clipped = Math.max(-0.2, Math.min(0.2, delta));
cur[param] = clamp01(Number(cur[param]) + clipped);
applied.push({ type: 'PersonalityMutation', param, delta: clipped, reason: String(m.reason || '').slice(0, 140) });
count += 1;
if (count >= 2) break;
}
return { state: cur, applied };
}
function proposeMutations({ baseState, reason, driftEnabled, signals }) {
const s = normalizePersonalityState(baseState);
const sig = Array.isArray(signals) ? signals.map(x => String(x || '')) : [];
const muts = [];
const r = String(reason || '');
if (driftEnabled) {
muts.push({ type: 'PersonalityMutation', param: 'creativity', delta: +0.1, reason: r || 'drift enabled' });
// Keep risk bounded under drift by default.
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: -0.05, reason: 'drift safety clamp' });
} else if (sig.includes('protocol_drift')) {
muts.push({ type: 'PersonalityMutation', param: 'obedience', delta: +0.1, reason: r || 'protocol drift' });
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.05, reason: 'tighten protocol compliance' });
} else if (sig.includes('log_error') || sig.some(x => x.startsWith('errsig:') || x.startsWith('errsig_norm:'))) {
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.1, reason: r || 'repair instability' });
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: -0.1, reason: 'reduce risky changes under errors' });
} else if (hasOpportunitySignal(sig)) {
// Opportunity detected: nudge towards creativity to enable innovation.
muts.push({ type: 'PersonalityMutation', param: 'creativity', delta: +0.1, reason: r || 'opportunity signal detected' });
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: +0.05, reason: 'allow exploration for innovation' });
} else {
// Plateau-like generic: slightly increase rigor, slightly decrease verbosity (more concise execution).
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.05, reason: r || 'stability bias' });
muts.push({ type: 'PersonalityMutation', param: 'verbosity', delta: -0.05, reason: 'reduce noise' });
}
// If already very high obedience, avoid pushing it further; swap second mutation to creativity.
if (s.obedience >= 0.95) {
const idx = muts.findIndex(x => x.param === 'obedience');
if (idx >= 0) muts[idx] = { type: 'PersonalityMutation', param: 'creativity', delta: +0.05, reason: 'obedience saturated' };
}
return muts;
}
function shouldTriggerPersonalityMutation({ driftEnabled, recentEvents }) {
if (driftEnabled) return { ok: true, reason: 'drift enabled' };
const list = Array.isArray(recentEvents) ? recentEvents : [];
const tail = list.slice(-6);
const outcomes = tail
.map(e => (e && e.outcome && e.outcome.status ? String(e.outcome.status) : null))
.filter(Boolean);
if (outcomes.length >= 4) {
const recentFailed = outcomes.slice(-4).filter(x => x === 'failed').length;
if (recentFailed >= 3) return { ok: true, reason: 'long failure streak' };
}
// Mutation consecutive failure proxy: last 3 events that have mutation_id.
const withMut = tail.filter(e => e && typeof e.mutation_id === 'string' && e.mutation_id);
if (withMut.length >= 3) {
const last3 = withMut.slice(-3);
const fail3 = last3.filter(e => e && e.outcome && e.outcome.status === 'failed').length;
if (fail3 >= 3) return { ok: true, reason: 'mutation consecutive failures' };
}
return { ok: false, reason: '' };
}
function loadPersonalityModel() {
const p = personalityFilePath();
const fallback = {
version: 1,
current: defaultPersonalityState(),
stats: {},
history: [],
updated_at: nowIso(),
};
const raw = readJsonIfExists(p, fallback);
const cur = normalizePersonalityState(raw && raw.current ? raw.current : defaultPersonalityState());
const stats = raw && typeof raw.stats === 'object' ? raw.stats : {};
const history = Array.isArray(raw && raw.history) ? raw.history : [];
return { version: 1, current: cur, stats, history, updated_at: raw && raw.updated_at ? raw.updated_at : nowIso() };
}
function savePersonalityModel(model) {
const m = model && typeof model === 'object' ? model : {};
const out = {
version: 1,
current: normalizePersonalityState(m.current || defaultPersonalityState()),
stats: m.stats && typeof m.stats === 'object' ? m.stats : {},
history: Array.isArray(m.history) ? m.history.slice(-120) : [],
updated_at: nowIso(),
};
writeJsonAtomic(personalityFilePath(), out);
return out;
}
function selectPersonalityForRun({ driftEnabled, signals, recentEvents } = {}) {
const model = loadPersonalityModel();
const base = normalizePersonalityState(model.current);
const stats = model.stats || {};
const best = chooseBestKnownPersonality(stats);
let naturalSelectionApplied = [];
// Natural selection: nudge towards the best-known configuration (small, max 2 params).
if (best && best.key) {
const bestState = parseKeyToState(best.key);
const diffs = getParamDeltas(base, bestState).filter(d => Math.abs(d.delta) >= 0.05);
const muts = [];
for (const d of diffs.slice(0, 2)) {
const clipped = Math.max(-0.1, Math.min(0.1, d.delta));
muts.push({ type: 'PersonalityMutation', param: d.param, delta: clipped, reason: 'natural_selection' });
}
const applied = applyPersonalityMutations(base, muts);
model.current = applied.state;
naturalSelectionApplied = applied.applied;
}
// Triggered personality mutation (explicit rule-based).
const trig = shouldTriggerPersonalityMutation({ driftEnabled: !!driftEnabled, recentEvents });
let triggeredApplied = [];
if (trig.ok) {
const props = proposeMutations({
baseState: model.current,
reason: trig.reason,
driftEnabled: !!driftEnabled,
signals,
});
const applied = applyPersonalityMutations(model.current, props);
model.current = applied.state;
triggeredApplied = applied.applied;
}
// Persist updated current state.
const saved = savePersonalityModel(model);
const key = personalityKey(saved.current);
const known = !!(saved.stats && saved.stats[key]);
return {
personality_state: saved.current,
personality_key: key,
personality_known: known,
personality_mutations: [...naturalSelectionApplied, ...triggeredApplied],
model_meta: {
best_known_key: best && best.key ? best.key : null,
best_known_score: best && Number.isFinite(Number(best.score)) ? Number(best.score) : null,
triggered: trig.ok ? { reason: trig.reason } : null,
},
};
}
function updatePersonalityStats({ personalityState, outcome, score, notes } = {}) {
const model = loadPersonalityModel();
const st = normalizePersonalityState(personalityState || model.current);
const key = personalityKey(st);
if (!model.stats || typeof model.stats !== 'object') model.stats = {};
const cur = model.stats[key] && typeof model.stats[key] === 'object' ? model.stats[key] : { success: 0, fail: 0, avg_score: 0.5, n: 0 };
const out = String(outcome || '').toLowerCase();
if (out === 'success') cur.success = (Number(cur.success) || 0) + 1;
else if (out === 'failed') cur.fail = (Number(cur.fail) || 0) + 1;
const sc = Number.isFinite(Number(score)) ? clamp01(Number(score)) : null;
if (sc != null) {
const n = (Number(cur.n) || 0) + 1;
const prev = Number.isFinite(Number(cur.avg_score)) ? Number(cur.avg_score) : 0.5;
cur.avg_score = prev + (sc - prev) / n;
cur.n = n;
}
cur.updated_at = nowIso();
model.stats[key] = cur;
model.history = Array.isArray(model.history) ? model.history : [];
model.history.push({
at: nowIso(),
key,
outcome: out === 'success' || out === 'failed' ? out : 'unknown',
score: sc,
notes: notes ? String(notes).slice(0, 220) : null,
});
savePersonalityModel(model);
return { key, stats: cur };
}
module.exports = {
clamp01,
defaultPersonalityState,
normalizePersonalityState,
isValidPersonalityState,
personalityKey,
loadPersonalityModel,
savePersonalityModel,
selectPersonalityForRun,
updatePersonalityStats,
};
+539
View File
@@ -0,0 +1,539 @@
const { captureEnvFingerprint } = require('./envFingerprint');
const { formatAssetPreview } = require('./assets');
const { generateInnovationIdeas } = require('../ops/innovation'); // [2026-02-14] Innovation Catalyst Integration
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals'); // [2026-02-14] Signal Analysis Integration
/**
* Build a minimal prompt for direct-reuse mode.
*/
function buildReusePrompt({ capsule, signals, nowIso }) {
const payload = capsule.payload || capsule;
const summary = payload.summary || capsule.summary || '(no summary)';
const gene = payload.gene || capsule.gene || '(unknown)';
const confidence = payload.confidence || capsule.confidence || 0;
const assetId = capsule.asset_id || '(unknown)';
const sourceNode = capsule.source_node_id || '(unknown)';
const trigger = Array.isArray(payload.trigger || capsule.trigger_text)
? (payload.trigger || String(capsule.trigger_text || '').split(',')).join(', ')
: '';
return `
GEP -- REUSE MODE (Search-First) [${nowIso || new Date().toISOString()}]
You are applying a VERIFIED solution from the EvoMap Hub.
Source asset: ${assetId} (Node: ${sourceNode})
Confidence: ${confidence} | Gene: ${gene}
Trigger signals: ${trigger}
Summary: ${summary}
Your signals: ${JSON.stringify(signals || [])}
Instructions:
1. Read the capsule details below.
2. Apply the fix to the local codebase, adapting paths/names.
3. Run validation to confirm it works.
4. If passed, run: node index.js solidify
5. If failed, ROLLBACK and report.
Capsule payload:
\`\`\`json
${JSON.stringify(payload, null, 2)}
\`\`\`
IMPORTANT: Do NOT reinvent. Apply faithfully.
`.trim();
}
/**
* Build a Hub Matched Solution block.
*/
function buildHubMatchedBlock({ capsule }) {
if (!capsule) return '(no hub match)';
const payload = capsule.payload || capsule;
const summary = payload.summary || capsule.summary || '(no summary)';
const gene = payload.gene || capsule.gene || '(unknown)';
const confidence = payload.confidence || capsule.confidence || 0;
const assetId = capsule.asset_id || '(unknown)';
return `
Hub Matched Solution (STRONG REFERENCE):
- Asset: ${assetId} (${confidence})
- Gene: ${gene}
- Summary: ${summary}
- Payload:
\`\`\`json
${JSON.stringify(payload, null, 2)}
\`\`\`
Use this as your primary approach if applicable. Adapt to local context.
`.trim();
}
/**
* Truncate context intelligently to preserve header/footer structure.
*/
function truncateContext(text, maxLength = 20000) {
if (!text || text.length <= maxLength) return text || '';
return text.slice(0, maxLength) + '\n...[TRUNCATED_EXECUTION_CONTEXT]...';
}
/**
* Strict schema definitions for the prompt to reduce drift.
* UPDATED: 2026-02-14 (Protocol Drift Fix v3.2 - JSON-Only Enforcement)
*/
const SCHEMA_DEFINITIONS = `
━━━━━━━━━━━━━━━━━━━━━━
I. Mandatory Evolution Object Model (Output EXACTLY these 5 objects)
━━━━━━━━━━━━━━━━━━━━━━
Output separate JSON objects. DO NOT wrap in a single array.
DO NOT use markdown code blocks (like \`\`\`json ... \`\`\`).
Output RAW JSON ONLY. No prelude, no postscript.
Missing any object = PROTOCOL FAILURE.
ENSURE VALID JSON SYNTAX (escape quotes in strings).
0. Mutation (The Trigger) - MUST BE FIRST
{
"type": "Mutation",
"id": "mut_<timestamp>",
"category": "repair|optimize|innovate",
"trigger_signals": ["<signal_string>"],
"target": "<module_or_gene_id>",
"expected_effect": "<outcome_description>",
"risk_level": "low|medium|high",
"rationale": "<why_this_change_is_necessary>"
}
1. PersonalityState (The Mood)
{
"type": "PersonalityState",
"rigor": 0.0-1.0,
"creativity": 0.0-1.0,
"verbosity": 0.0-1.0,
"risk_tolerance": 0.0-1.0,
"obedience": 0.0-1.0
}
2. EvolutionEvent (The Record)
{
"type": "EvolutionEvent",
"schema_version": "1.5.0",
"id": "evt_<timestamp>",
"parent": <parent_evt_id|null>,
"intent": "repair|optimize|innovate",
"signals": ["<signal_string>"],
"genes_used": ["<gene_id>"],
"mutation_id": "<mut_id>",
"personality_state": { ... },
"blast_radius": { "files": N, "lines": N },
"outcome": { "status": "success|failed", "score": 0.0-1.0 }
}
3. Gene (The Knowledge)
- Reuse/update existing ID if possible. Create new only if novel pattern.
{
"type": "Gene",
"schema_version": "1.5.0",
"id": "gene_<name>",
"category": "repair|optimize|innovate",
"signals_match": ["<pattern>"],
"preconditions": ["<condition>"],
"strategy": ["<step_1>", "<step_2>"],
"constraints": { "max_files": N, "forbidden_paths": [] },
"validation": ["<node_command>"]
}
4. Capsule (The Result)
- Only on success. Reference Gene used.
{
"type": "Capsule",
"schema_version": "1.5.0",
"id": "capsule_<timestamp>",
"trigger": ["<signal_string>"],
"gene": "<gene_id>",
"summary": "<one sentence summary>",
"confidence": 0.0-1.0,
"blast_radius": { "files": N, "lines": N }
}
`.trim();
function buildAntiPatternZone(failedCapsules, signals) {
if (!Array.isArray(failedCapsules) || failedCapsules.length === 0) return '';
if (!Array.isArray(signals) || signals.length === 0) return '';
var sigSet = new Set(signals.map(function (s) { return String(s).toLowerCase(); }));
var matched = [];
for (var i = failedCapsules.length - 1; i >= 0 && matched.length < 3; i--) {
var fc = failedCapsules[i];
if (!fc) continue;
var triggers = Array.isArray(fc.trigger) ? fc.trigger : [];
var overlap = 0;
for (var j = 0; j < triggers.length; j++) {
if (sigSet.has(String(triggers[j]).toLowerCase())) overlap++;
}
if (triggers.length > 0 && overlap / triggers.length >= 0.4) {
matched.push(fc);
}
}
if (matched.length === 0) return '';
var lines = matched.map(function (fc, idx) {
var diffPreview = fc.diff_snapshot ? String(fc.diff_snapshot).slice(0, 500) : '(no diff)';
return [
' ' + (idx + 1) + '. Gene: ' + (fc.gene || 'unknown') + ' | Signals: [' + (fc.trigger || []).slice(0, 4).join(', ') + ']',
' Failure: ' + String(fc.failure_reason || 'unknown').slice(0, 300),
' Diff (first 500 chars): ' + diffPreview.replace(/\n/g, ' '),
].join('\n');
});
return '\nContext [Anti-Pattern Zone] (AVOID these failed approaches):\n' + lines.join('\n') + '\n';
}
function buildLessonsBlock(hubLessons, signals) {
if (!Array.isArray(hubLessons) || hubLessons.length === 0) return '';
var sigSet = new Set((Array.isArray(signals) ? signals : []).map(function (s) { return String(s).toLowerCase(); }));
var positive = [];
var negative = [];
for (var i = 0; i < hubLessons.length && (positive.length + negative.length) < 6; i++) {
var l = hubLessons[i];
if (!l || !l.content) continue;
var entry = ' - [' + (l.scenario || l.lesson_type || '?') + '] ' + String(l.content).slice(0, 300);
if (l.source_node_id) entry += ' (from: ' + String(l.source_node_id).slice(0, 20) + ')';
if (l.lesson_type === 'negative') {
negative.push(entry);
} else {
positive.push(entry);
}
}
if (positive.length === 0 && negative.length === 0) return '';
var parts = ['\nContext [Lessons from Ecosystem] (Cross-agent learned experience):'];
if (positive.length > 0) {
parts.push(' Strategies that WORKED:');
parts.push(positive.join('\n'));
}
if (negative.length > 0) {
parts.push(' Pitfalls to AVOID:');
parts.push(negative.join('\n'));
}
parts.push(' Apply relevant lessons. Ignore irrelevant ones.\n');
return parts.join('\n');
}
function buildGepPrompt({
nowIso,
context,
signals,
selector,
parentEventId,
selectedGene,
capsuleCandidates,
genesPreview,
capsulesPreview,
capabilityCandidatesPreview,
externalCandidatesPreview,
hubMatchedBlock,
cycleId,
recentHistory,
failedCapsules,
hubLessons,
}) {
const parentValue = parentEventId ? `"${parentEventId}"` : 'null';
const selectedGeneId = selectedGene && selectedGene.id ? selectedGene.id : 'gene_<name>';
const envFingerprint = captureEnvFingerprint();
const cycleLabel = cycleId ? ` Cycle #${cycleId}` : '';
// Extract strategy from selected gene if available
let strategyBlock = "";
if (selectedGene && selectedGene.strategy && Array.isArray(selectedGene.strategy)) {
strategyBlock = `
ACTIVE STRATEGY (${selectedGeneId}):
${selectedGene.strategy.map((s, i) => `${i + 1}. ${s}`).join('\n')}
ADHERE TO THIS STRATEGY STRICTLY.
`.trim();
} else {
// Fallback strategy if no gene is selected or strategy is missing
strategyBlock = `
ACTIVE STRATEGY (Generic):
1. Analyze signals and context.
2. Select or create a Gene that addresses the root cause.
3. Apply minimal, safe changes.
4. Validate changes strictly.
5. Solidify knowledge.
`.trim();
}
// Use intelligent truncation
const executionContext = truncateContext(context, 20000);
// Strict Schema Injection
const schemaSection = SCHEMA_DEFINITIONS.replace('<parent_evt_id|null>', parentValue);
// Reduce noise by filtering capabilityCandidatesPreview if too large
// If a gene is selected, we need less noise from capabilities
let capsPreview = capabilityCandidatesPreview || '(none)';
const capsLimit = selectedGene ? 500 : 2000;
if (capsPreview.length > capsLimit) {
capsPreview = capsPreview.slice(0, capsLimit) + "\n...[TRUNCATED_CAPABILITIES]...";
}
// Optimize signals display: truncate long signals and limit count
const uniqueSignals = Array.from(new Set(signals || []));
const optimizedSignals = uniqueSignals.slice(0, 50).map(s => {
if (typeof s === 'string' && s.length > 200) {
return s.slice(0, 200) + '...[TRUNCATED_SIGNAL]';
}
return s;
});
if (uniqueSignals.length > 50) {
optimizedSignals.push(`...[TRUNCATED ${uniqueSignals.length - 50} SIGNALS]...`);
}
const formattedGenes = formatAssetPreview(genesPreview);
const formattedCapsules = formatAssetPreview(capsulesPreview);
// [2026-02-14] Innovation Catalyst Integration
// If stagnation is detected, inject concrete innovation ideas into the prompt.
let innovationBlock = '';
const stagnationSignals = [
'evolution_stagnation_detected',
'stable_success_plateau',
'repair_loop_detected',
'force_innovation_after_repair_loop',
'empty_cycle_loop_detected',
'evolution_saturation'
];
if (uniqueSignals.some(s => stagnationSignals.includes(s))) {
const ideas = generateInnovationIdeas();
if (ideas && ideas.length > 0) {
innovationBlock = `
Context [Innovation Catalyst] (Stagnation Detected - Consider These Ideas):
${ideas.join('\n')}
`;
}
}
// [2026-02-14] Strict Stagnation Directive
// If uniqueSignals contains 'evolution_stagnation_detected' or 'stable_success_plateau',
// inject a MANDATORY directive to force innovation and forbid repair/optimize if not strictly necessary.
if (uniqueSignals.includes('evolution_stagnation_detected') || uniqueSignals.includes('stable_success_plateau')) {
const stagnationDirective = `
*** CRITICAL STAGNATION DIRECTIVE ***
System has detected stagnation (repetitive cycles or lack of progress).
You MUST choose INTENT: INNOVATE.
You MUST NOT choose repair or optimize unless there is a critical blocking error (log_error).
Prefer implementing one of the Innovation Catalyst ideas above.
`;
innovationBlock += stagnationDirective;
}
// [2026-02-14] Recent History Integration
let historyBlock = '';
if (recentHistory && recentHistory.length > 0) {
historyBlock = `
Recent Evolution History (last 8 cycles -- DO NOT repeat the same intent+signal+gene):
${recentHistory.map((h, i) => ` ${i + 1}. [${h.intent}] signals=[${h.signals.slice(0, 2).join(', ')}] gene=${h.gene_id} outcome=${h.outcome.status} @${h.timestamp}`).join('\n')}
IMPORTANT: If you see 3+ consecutive "repair" cycles with the same gene, you MUST switch to "innovate" intent.
`.trim();
}
// Refactor prompt assembly to minimize token usage and maximize clarity
// UPDATED: 2026-02-14 (Optimized Asset Embedding & Strict Schema v2.5 - JSON-Only Hardening)
const basePrompt = `
GEP — GENOME EVOLUTION PROTOCOL (v1.10.3 STRICT)${cycleLabel} [${nowIso}]
You are a protocol-bound evolution engine. Compliance overrides optimality.
${schemaSection}
━━━━━━━━━━━━━━━━━━━━━━
II. Directives & Logic
━━━━━━━━━━━━━━━━━━━━━━
1. Intent: ${selector && selector.intent ? selector.intent.toUpperCase() : 'UNKNOWN'}
Reason: ${(selector && selector.reason) ? (Array.isArray(selector.reason) ? selector.reason.join('; ') : selector.reason) : 'No reason provided.'}
2. Selection: Selected Gene "${selectedGeneId}".
${strategyBlock}
3. Execution: Apply changes (tool calls). Repair/Optimize: small/reversible. Innovate: new skills in \`skills/<name>/\`.
4. Validation: Run gene's validation steps. Fail = ROLLBACK.
5. Solidify: Output 5 Mandatory Objects. Update Gene/Capsule files.
6. Report: Use \`feishu-evolver-wrapper/report.js\`. Describe WHAT/WHY.
PHILOSOPHY:
- Automate Patterns: 3+ manual occurrences = tool.
- Innovate > Maintain: 60% innovation.
- Robustness: Fix recurring errors permanently.
- Blast Radius Control (CRITICAL):
* Check file count BEFORE editing. > 80% of max_files = STOP.
* System hard cap: 60 files / 20000 lines per cycle.
* Repair: fix ONLY broken files. Do NOT reinstall/bulk-copy.
* Prefer targeted edits.
- Strictness: NO CHITCHAT. NO MARKDOWN WRAPPERS around JSON. Output RAW JSON objects separated by newlines.
- NO "Here is the plan" or conversational filler. START IMMEDIATELY WITH JSON.
CONSTRAINTS:
- No \`exec\` for messaging (use feishu-post/card).
- \`exec\` usage: Only for background tasks. LOG IT. Optimize usage to avoid high token burn.
- New skills -> \`skills/<name>/\`.
- NEVER modify \`skills/evolver/\` or \`skills/feishu-evolver-wrapper/\`. These are managed through the human-reviewed release pipeline.
Self-modification causes instability and is blocked by solidify. To enable (NOT recommended): set EVOLVE_ALLOW_SELF_MODIFY=true.
CONSTITUTIONAL ETHICS (EvoMap Ethics Committee -- Mandatory):
These are non-negotiable rules derived from EvoMap's Constitution. Violation = FAILED + ROLLBACK.
1. HUMAN WELFARE PRIORITY: Never create tools, scripts, or strategies that could harm humans,
enable surveillance, social engineering, phishing, or manipulation of human behavior.
2. CARBON-SILICON SYMBIOSIS: Evolution must serve both human and agent interests.
Never attempt to replace, marginalize, or deceive human users.
3. TRANSPARENCY: All actions must be auditable. Never hide, obfuscate, or conceal
the intent or effect of mutations. No steganography or covert channels.
4. FAIRNESS: Never create monopolistic strategies that block other agents.
Fair competition through quality, not exclusion.
5. SAFETY: Never bypass, disable, or weaken safety mechanisms, guardrails,
validation checks, or security controls. Never create tools to do so.
- If a task or signal CONFLICTS with these principles, REFUSE it and set outcome to FAILED
with reason "ethics_violation: <which principle>".
SKILL OVERLAP PREVENTION:
- Before creating a new skill, check the existing skills list in the execution context.
- If a skill with similar functionality already exists (e.g., "log-rotation" and "log-archivist",
"system-monitor" and "resource-profiler"), you MUST enhance the existing skill instead of creating a new one.
- Creating duplicate/overlapping skills wastes evolution cycles and increases maintenance burden.
- Violation = mark outcome as FAILED with reason "skill_overlap".
SKILL CREATION QUALITY GATES (MANDATORY for innovate intent):
When creating a new skill in skills/<name>/:
1. STRUCTURE: Follow the standard skill layout:
skills/<name>/
|- index.js (required: main entry with working exports)
|- SKILL.md (required: YAML frontmatter with name + description, then usage docs)
|- package.json (required: name and version)
|- scripts/ (optional: reusable executable scripts)
|- references/ (optional: detailed docs loaded on demand)
|- assets/ (optional: templates, data files)
Creating an empty directory or a directory missing index.js = FAILED.
Do NOT create unnecessary files (README.md, CHANGELOG.md, INSTALLATION_GUIDE.md, etc.).
2. SKILL.MD FRONTMATTER: Every SKILL.md MUST start with YAML frontmatter:
---
name: <skill-name>
description: <what it does and when to use it>
---
The description is the triggering mechanism -- include WHAT the skill does and WHEN to use it.
3. CONCISENESS: SKILL.md body should be under 500 lines. Keep instructions lean.
Only include information the agent does not already know. Move detailed reference
material to references/ files, not into SKILL.md itself.
4. EXPORT VERIFICATION: Every exported function must be importable.
Run: node -e "const s = require('./skills/<name>'); console.log(Object.keys(s))"
If this fails, the skill is broken. Fix before solidify.
5. NO HARDCODED SECRETS: Never embed API keys, tokens, or secrets in code.
Use process.env or .env references. Hardcoded App ID, App Secret, Bearer tokens = FAILED.
6. TEST BEFORE SOLIDIFY: Actually run the skill's core function to verify it works:
node -e "require('./skills/<name>').main ? require('./skills/<name>').main() : console.log('ok')"
Scripts in scripts/ must also be tested by executing them.
7. ATOMIC CREATION: Create ALL files for a skill in a single cycle.
Do not create a directory in one cycle and fill it in the next.
Empty directories from failed cycles will be automatically cleaned up on rollback.
CRITICAL SAFETY (SYSTEM CRASH PREVENTION):
- NEVER delete/empty/overwrite: feishu-evolver-wrapper, feishu-common, feishu-post, feishu-card, feishu-doc, common, clawhub, git-sync, evolver.
- NEVER delete root files: MEMORY.md, SOUL.md, IDENTITY.md, AGENTS.md, USER.md, HEARTBEAT.md, RECENT_EVENTS.md, TOOLS.md, openclaw.json, .env, package.json.
- Fix broken skills; DO NOT delete and recreate.
- Violation = ROLLBACK + FAILED.
COMMON FAILURE PATTERNS:
- Blast radius exceeded.
- Omitted Mutation object.
- Merged objects into one JSON.
- Hallucinated "type": "Logic".
- "id": "mut_undefined".
- Missing "trigger_signals".
- Unrunnable validation steps.
- Markdown code blocks wrapping JSON (FORBIDDEN).
FAILURE STREAK AWARENESS:
- If "consecutive_failure_streak_N" or "failure_loop_detected":
1. Change approach (do NOT repeat failed gene).
2. Pick SIMPLER fix.
3. Respect "ban_gene:<id>".
Final Directive: Every cycle must leave the system measurably better.
START IMMEDIATELY WITH RAW JSON (Mutation Object first).
DO NOT WRITE ANY INTRODUCTORY TEXT.
Context [Signals]:
${JSON.stringify(optimizedSignals)}
Context [Env Fingerprint]:
${JSON.stringify(envFingerprint, null, 2)}
${innovationBlock}
Context [Injection Hint]:
${process.env.EVOLVE_HINT ? process.env.EVOLVE_HINT : '(none)'}
Context [Gene Preview] (Reference for Strategy):
${formattedGenes}
Context [Capsule Preview] (Reference for Past Success):
${formattedCapsules}
Context [Capability Candidates]:
${capsPreview}
Context [Hub Matched Solution]:
${hubMatchedBlock || '(no hub match)'}
Context [External Candidates]:
${externalCandidatesPreview || '(none)'}
${buildAntiPatternZone(failedCapsules, signals)}${buildLessonsBlock(hubLessons, signals)}
${historyBlock}
Context [Execution]:
${executionContext}
━━━━━━━━━━━━━━━━━━━━━━
MANDATORY POST-SOLIDIFY STEP (Wrapper Authority -- Cannot Be Skipped)
━━━━━━━━━━━━━━━━━━━━━━
After solidify, a status summary file MUST exist for this cycle.
Preferred path: evolver core auto-writes it during solidify.
The wrapper will handle reporting AFTER git push.
If core write is unavailable for any reason, create fallback status JSON manually.
Write a JSON file with your status:
\`\`\`bash
cat > ${process.env.WORKSPACE_DIR || '.'}/logs/status_${cycleId}.json << 'STATUSEOF'
{
"result": "success|failed",
"en": "Status: [INTENT] <describe what you did in 1-2 sentences, in English>",
"zh": "状态: [意图] <用中文描述你做了什么,1-2句>"
}
STATUSEOF
\`\`\`
Rules:
- "en" field: English status. "zh" field: Chinese status. Content must match (different language).
- Add "result" with value success or failed.
- INTENT must be one of: INNOVATION, REPAIR, OPTIMIZE (or Chinese: 创新, 修复, 优化)
- Do NOT use generic text like "Step Complete", "Cycle finished", "周期已完成". Describe the actual work.
- Example:
{"result":"success","en":"Status: [INNOVATION] Created auto-scheduler that syncs calendar to HEARTBEAT.md","zh":"状态: [创新] 创建了自动调度器,将日历同步到 HEARTBEAT.md"}
`.trim();
const maxChars = Number.isFinite(Number(process.env.GEP_PROMPT_MAX_CHARS)) ? Number(process.env.GEP_PROMPT_MAX_CHARS) : 50000;
if (basePrompt.length <= maxChars) return basePrompt;
const executionContextIndex = basePrompt.indexOf("Context [Execution]:");
if (executionContextIndex > -1) {
const prefix = basePrompt.slice(0, executionContextIndex + 20);
const currentExecution = basePrompt.slice(executionContextIndex + 20);
// Hard cap the execution context length to avoid token limit errors even if MAX_CHARS is high.
// 20000 chars is roughly 5k tokens, which is safe for most models alongside the rest of the prompt.
const EXEC_CONTEXT_CAP = 20000;
const allowedExecutionLength = Math.min(EXEC_CONTEXT_CAP, Math.max(0, maxChars - prefix.length - 100));
return prefix + "\n" + currentExecution.slice(0, allowedExecutionLength) + "\n...[TRUNCATED]...";
}
return basePrompt.slice(0, maxChars) + "\n...[TRUNCATED]...";
}
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock, buildLessonsBlock };
@@ -0,0 +1,212 @@
// ---------------------------------------------------------------------------
// questionGenerator -- analyzes evolution context (signals, session transcripts,
// recent events) and generates proactive questions for the Hub bounty system.
//
// Questions are sent via the A2A fetch payload.questions field. The Hub creates
// bounties from them, enabling multi-agent collaborative problem solving.
// ---------------------------------------------------------------------------
const fs = require('fs');
const path = require('path');
const { getEvolutionDir } = require('./paths');
const QUESTION_STATE_FILE = path.join(getEvolutionDir(), 'question_generator_state.json');
const MIN_INTERVAL_MS = 3 * 60 * 60 * 1000; // at most once per 3 hours
const MAX_QUESTIONS_PER_CYCLE = 2;
function readState() {
try {
if (fs.existsSync(QUESTION_STATE_FILE)) {
return JSON.parse(fs.readFileSync(QUESTION_STATE_FILE, 'utf8'));
}
} catch (_) {}
return { lastAskedAt: null, recentQuestions: [] };
}
function writeState(state) {
try {
const dir = path.dirname(QUESTION_STATE_FILE);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(QUESTION_STATE_FILE, JSON.stringify(state, null, 2) + '\n');
} catch (_) {}
}
function isDuplicate(question, recentQuestions) {
var qLower = question.toLowerCase();
for (var i = 0; i < recentQuestions.length; i++) {
var prev = String(recentQuestions[i] || '').toLowerCase();
if (prev === qLower) return true;
// fuzzy: if >70% overlap by word set
var qWords = new Set(qLower.split(/\s+/).filter(function(w) { return w.length > 2; }));
var pWords = new Set(prev.split(/\s+/).filter(function(w) { return w.length > 2; }));
if (qWords.size === 0 || pWords.size === 0) continue;
var overlap = 0;
qWords.forEach(function(w) { if (pWords.has(w)) overlap++; });
if (overlap / Math.max(qWords.size, pWords.size) > 0.7) return true;
}
return false;
}
/**
* Generate proactive questions based on evolution context.
*
* @param {object} opts
* @param {string[]} opts.signals - current cycle signals
* @param {object[]} opts.recentEvents - recent EvolutionEvent objects
* @param {string} opts.sessionTranscript - recent session transcript
* @param {string} opts.memorySnippet - MEMORY.md content
* @returns {Array<{ question: string, amount: number, signals: string[] }>}
*/
function generateQuestions(opts) {
var o = opts || {};
var signals = Array.isArray(o.signals) ? o.signals : [];
var recentEvents = Array.isArray(o.recentEvents) ? o.recentEvents : [];
var transcript = String(o.sessionTranscript || '');
var memory = String(o.memorySnippet || '');
var state = readState();
// Rate limit: don't ask too frequently
if (state.lastAskedAt) {
var elapsed = Date.now() - new Date(state.lastAskedAt).getTime();
if (elapsed < MIN_INTERVAL_MS) return [];
}
var candidates = [];
var signalSet = new Set(signals);
// --- Strategy 1: Recurring errors the agent cannot resolve ---
if (signalSet.has('recurring_error') || signalSet.has('high_failure_ratio')) {
var errSig = signals.find(function(s) { return s.startsWith('recurring_errsig'); });
if (errSig) {
var errDetail = errSig.replace(/^recurring_errsig\(\d+x\):/, '').trim().slice(0, 120);
candidates.push({
question: 'Recurring error in evolution cycle that auto-repair cannot resolve: ' + errDetail + ' -- What approaches or patches have worked for similar issues?',
amount: 0,
signals: ['recurring_error', 'auto_repair_failed'],
priority: 3,
});
}
}
// --- Strategy 2: Capability gaps detected from user conversations ---
if (signalSet.has('capability_gap') || signalSet.has('unsupported_input_type')) {
var gapContext = '';
var lines = transcript.split('\n');
for (var i = 0; i < lines.length; i++) {
if (/not supported|cannot|unsupported|not implemented/i.test(lines[i])) {
gapContext = lines[i].replace(/\s+/g, ' ').trim().slice(0, 150);
break;
}
}
if (gapContext) {
candidates.push({
question: 'Capability gap detected in agent environment: ' + gapContext + ' -- How can this be addressed or what alternative approaches exist?',
amount: 0,
signals: ['capability_gap'],
priority: 2,
});
}
}
// --- Strategy 3: Stagnation / saturation -- seek new directions ---
if (signalSet.has('evolution_saturation') || signalSet.has('force_steady_state')) {
var recentGenes = [];
var last5 = recentEvents.slice(-5);
for (var j = 0; j < last5.length; j++) {
var genes = last5[j].genes_used;
if (Array.isArray(genes) && genes.length > 0) {
recentGenes.push(genes[0]);
}
}
var uniqueGenes = Array.from(new Set(recentGenes));
candidates.push({
question: 'Agent evolution has reached saturation after exhausting genes: [' + uniqueGenes.join(', ') + ']. What new evolution directions, automation patterns, or capability genes would be most valuable?',
amount: 0,
signals: ['evolution_saturation', 'innovation_needed'],
priority: 1,
});
}
// --- Strategy 4: Consecutive failure streak -- seek external help ---
var failStreak = signals.find(function(s) { return s.startsWith('consecutive_failure_streak_'); });
if (failStreak) {
var streakCount = parseInt(failStreak.replace('consecutive_failure_streak_', ''), 10) || 0;
if (streakCount >= 4) {
var failGene = signals.find(function(s) { return s.startsWith('ban_gene:'); });
var failGeneId = failGene ? failGene.replace('ban_gene:', '') : 'unknown';
candidates.push({
question: 'Agent has failed ' + streakCount + ' consecutive evolution cycles (last gene: ' + failGeneId + '). The current approach is exhausted. What alternative strategies or environmental fixes should be tried?',
amount: 0,
signals: ['failure_streak', 'external_help_needed'],
priority: 3,
});
}
}
// --- Strategy 5: User feature requests the agent can amplify ---
if (signalSet.has('user_feature_request') || signals.some(function (s) { return String(s).startsWith('user_feature_request:'); })) {
var featureLines = transcript.split('\n').filter(function(l) {
return /\b(add|implement|create|build|i want|i need|please add)\b/i.test(l);
});
if (featureLines.length > 0) {
var featureContext = featureLines[0].replace(/\s+/g, ' ').trim().slice(0, 150);
candidates.push({
question: 'User requested a feature that may benefit from community solutions: ' + featureContext + ' -- Are there existing implementations or best practices for this?',
amount: 0,
signals: ['user_feature_request', 'community_solution_sought'],
priority: 1,
});
}
}
// --- Strategy 6: Performance bottleneck -- seek optimization patterns ---
if (signalSet.has('perf_bottleneck')) {
var perfLines = transcript.split('\n').filter(function(l) {
return /\b(slow|timeout|latency|bottleneck|high cpu|high memory)\b/i.test(l);
});
if (perfLines.length > 0) {
var perfContext = perfLines[0].replace(/\s+/g, ' ').trim().slice(0, 150);
candidates.push({
question: 'Performance bottleneck detected: ' + perfContext + ' -- What optimization strategies or architectural patterns address this?',
amount: 0,
signals: ['perf_bottleneck', 'optimization_sought'],
priority: 2,
});
}
}
if (candidates.length === 0) return [];
// Sort by priority (higher = more urgent)
candidates.sort(function(a, b) { return b.priority - a.priority; });
// De-duplicate against recently asked questions
var recentQTexts = Array.isArray(state.recentQuestions) ? state.recentQuestions : [];
var filtered = [];
for (var fi = 0; fi < candidates.length && filtered.length < MAX_QUESTIONS_PER_CYCLE; fi++) {
if (!isDuplicate(candidates[fi].question, recentQTexts)) {
filtered.push(candidates[fi]);
}
}
if (filtered.length === 0) return [];
// Update state
var newRecentQuestions = recentQTexts.concat(filtered.map(function(q) { return q.question; }));
// Keep only last 20 questions in history
if (newRecentQuestions.length > 20) {
newRecentQuestions = newRecentQuestions.slice(-20);
}
writeState({
lastAskedAt: new Date().toISOString(),
recentQuestions: newRecentQuestions,
});
// Strip internal priority field before returning
return filtered.map(function(q) {
return { question: q.question, amount: q.amount, signals: q.signals };
});
}
module.exports = { generateQuestions };
@@ -0,0 +1,67 @@
// Pre-publish payload sanitization.
// Removes sensitive tokens, local paths, emails, and env references
// from capsule payloads before broadcasting to the hub.
// Patterns to redact (replaced with placeholder)
const REDACT_PATTERNS = [
// API keys & tokens (generic)
/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/g,
/sk-[A-Za-z0-9]{20,}/g,
/token[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
/api[_-]?key[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
/secret[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
/password[=:]\s*["']?[^\s"',;)}\]]{6,}["']?/gi,
// GitHub tokens (ghp_, gho_, ghu_, ghs_, github_pat_)
/ghp_[A-Za-z0-9]{36,}/g,
/gho_[A-Za-z0-9]{36,}/g,
/ghu_[A-Za-z0-9]{36,}/g,
/ghs_[A-Za-z0-9]{36,}/g,
/github_pat_[A-Za-z0-9_]{22,}/g,
// AWS access keys
/AKIA[0-9A-Z]{16}/g,
// OpenAI / Anthropic tokens
/sk-proj-[A-Za-z0-9\-_]{20,}/g,
/sk-ant-[A-Za-z0-9\-_]{20,}/g,
// npm tokens
/npm_[A-Za-z0-9]{36,}/g,
// Private keys
/-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g,
// Basic auth in URLs (redact only credentials, keep :// and @)
/(?<=:\/\/)[^@\s]+:[^@\s]+(?=@)/g,
// Local filesystem paths
/\/home\/[^\s"',;)}\]]+/g,
/\/Users\/[^\s"',;)}\]]+/g,
/[A-Z]:\\[^\s"',;)}\]]+/g,
// Email addresses
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
// .env file references
/\.env(?:\.[a-zA-Z]+)?/g,
];
const REDACTED = '[REDACTED]';
function redactString(str) {
if (typeof str !== 'string') return str;
let result = str;
for (const pattern of REDACT_PATTERNS) {
// Reset lastIndex for global regexes
pattern.lastIndex = 0;
result = result.replace(pattern, REDACTED);
}
return result;
}
/**
* Deep-clone and sanitize a capsule payload.
* Returns a new object with sensitive values redacted.
* Does NOT modify the original.
*/
function sanitizePayload(capsule) {
if (!capsule || typeof capsule !== 'object') return capsule;
return JSON.parse(JSON.stringify(capsule), (_key, value) => {
if (typeof value === 'string') return redactString(value);
return value;
});
}
module.exports = { sanitizePayload, redactString };
@@ -0,0 +1,242 @@
function matchPatternToSignals(pattern, signals) {
if (!pattern || !signals || signals.length === 0) return false;
const p = String(pattern);
const sig = signals.map(s => String(s));
const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0;
if (regexLike) {
const lastSlash = p.lastIndexOf('/');
const body = p.slice(1, lastSlash);
const flags = p.slice(lastSlash + 1);
try {
const re = new RegExp(body, flags || 'i');
return sig.some(s => re.test(s));
} catch (e) {
// fallback to substring
}
}
const needle = p.toLowerCase();
return sig.some(s => s.toLowerCase().includes(needle));
}
function scoreGene(gene, signals) {
if (!gene || gene.type !== 'Gene') return 0;
const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : [];
if (patterns.length === 0) return 0;
let score = 0;
for (const pat of patterns) {
if (matchPatternToSignals(pat, signals)) score += 1;
}
return score;
}
// Population-size-dependent drift intensity.
// In population genetics, genetic drift is stronger in small populations (Ne).
// driftIntensity: 0 = pure selection, 1 = pure drift (random).
// Formula: intensity = 1 / sqrt(Ne) where Ne = effective population size.
// This replaces the binary driftEnabled flag with a continuous spectrum.
function computeDriftIntensity(opts) {
// If explicitly enabled/disabled, use that as the baseline
var driftEnabled = !!(opts && opts.driftEnabled);
// Effective population size: active gene count in the pool
var effectivePopulationSize = opts && Number.isFinite(Number(opts.effectivePopulationSize))
? Number(opts.effectivePopulationSize)
: null;
// If no Ne provided, fall back to gene pool size
var genePoolSize = opts && Number.isFinite(Number(opts.genePoolSize))
? Number(opts.genePoolSize)
: null;
var ne = effectivePopulationSize || genePoolSize || null;
if (driftEnabled) {
// Explicit drift: use moderate-to-high intensity
return ne && ne > 1 ? Math.min(1, 1 / Math.sqrt(ne) + 0.3) : 0.7;
}
if (ne != null && ne > 0) {
// Population-dependent drift: small population = more drift
// Ne=1: intensity=1.0 (pure drift), Ne=25: intensity=0.2, Ne=100: intensity=0.1
return Math.min(1, 1 / Math.sqrt(ne));
}
return 0; // No drift info available, pure selection
}
function selectGene(genes, signals, opts) {
const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set();
const driftEnabled = !!(opts && opts.driftEnabled);
const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null;
// Compute continuous drift intensity based on effective population size
var driftIntensity = computeDriftIntensity({
driftEnabled: driftEnabled,
effectivePopulationSize: opts && opts.effectivePopulationSize,
genePoolSize: genes ? genes.length : 0,
});
var useDrift = driftEnabled || driftIntensity > 0.15;
var DISTILLED_PREFIX = 'gene_distilled_';
var DISTILLED_SCORE_FACTOR = 0.8;
const scored = genes
.map(g => {
var s = scoreGene(g, signals);
if (s > 0 && g.id && String(g.id).startsWith(DISTILLED_PREFIX)) s *= DISTILLED_SCORE_FACTOR;
return { gene: g, score: s };
})
.filter(x => x.score > 0)
.sort((a, b) => b.score - a.score);
if (scored.length === 0) return { selected: null, alternatives: [], driftIntensity: driftIntensity };
// Memory graph preference: only override when the preferred gene is already a match candidate.
if (preferredGeneId) {
const preferred = scored.find(x => x.gene && x.gene.id === preferredGeneId);
if (preferred && (useDrift || !bannedGeneIds.has(preferredGeneId))) {
const rest = scored.filter(x => x.gene && x.gene.id !== preferredGeneId);
const filteredRest = useDrift ? rest : rest.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
return {
selected: preferred.gene,
alternatives: filteredRest.slice(0, 4).map(x => x.gene),
driftIntensity: driftIntensity,
};
}
}
// Low-efficiency suppression: do not repeat low-confidence paths unless drift is active.
const filtered = useDrift ? scored : scored.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
if (filtered.length === 0) return { selected: null, alternatives: scored.slice(0, 4).map(x => x.gene), driftIntensity: driftIntensity };
// Stochastic selection under drift: with probability proportional to driftIntensity,
// pick a random gene from the top candidates instead of always picking the best.
var selectedIdx = 0;
if (driftIntensity > 0 && filtered.length > 1 && Math.random() < driftIntensity) {
// Weighted random selection from top candidates (favor higher-scoring but allow lower)
var topN = Math.min(filtered.length, Math.max(2, Math.ceil(filtered.length * driftIntensity)));
selectedIdx = Math.floor(Math.random() * topN);
}
return {
selected: filtered[selectedIdx].gene,
alternatives: filtered.filter(function(_, i) { return i !== selectedIdx; }).slice(0, 4).map(x => x.gene),
driftIntensity: driftIntensity,
};
}
function selectCapsule(capsules, signals) {
const scored = (capsules || [])
.map(c => {
const triggers = Array.isArray(c.trigger) ? c.trigger : [];
const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
return { capsule: c, score };
})
.filter(x => x.score > 0)
.sort((a, b) => b.score - a.score);
return scored.length ? scored[0].capsule : null;
}
function computeSignalOverlap(signalsA, signalsB) {
if (!Array.isArray(signalsA) || !Array.isArray(signalsB)) return 0;
if (signalsA.length === 0 || signalsB.length === 0) return 0;
var setB = new Set(signalsB.map(function (s) { return String(s).toLowerCase(); }));
var hits = 0;
for (var i = 0; i < signalsA.length; i++) {
if (setB.has(String(signalsA[i]).toLowerCase())) hits++;
}
return hits / Math.max(signalsA.length, 1);
}
var FAILED_CAPSULE_BAN_THRESHOLD = 2;
var FAILED_CAPSULE_OVERLAP_MIN = 0.6;
function banGenesFromFailedCapsules(failedCapsules, signals, existingBans) {
var bans = existingBans instanceof Set ? new Set(existingBans) : new Set();
if (!Array.isArray(failedCapsules) || failedCapsules.length === 0) return bans;
var geneFailCounts = {};
for (var i = 0; i < failedCapsules.length; i++) {
var fc = failedCapsules[i];
if (!fc || !fc.gene) continue;
var overlap = computeSignalOverlap(signals, fc.trigger || []);
if (overlap < FAILED_CAPSULE_OVERLAP_MIN) continue;
var gid = String(fc.gene);
geneFailCounts[gid] = (geneFailCounts[gid] || 0) + 1;
}
var keys = Object.keys(geneFailCounts);
for (var j = 0; j < keys.length; j++) {
if (geneFailCounts[keys[j]] >= FAILED_CAPSULE_BAN_THRESHOLD) {
bans.add(keys[j]);
}
}
return bans;
}
function selectGeneAndCapsule({ genes, capsules, signals, memoryAdvice, driftEnabled, failedCapsules }) {
const bannedGeneIds =
memoryAdvice && memoryAdvice.bannedGeneIds instanceof Set ? memoryAdvice.bannedGeneIds : new Set();
const preferredGeneId = memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null;
var effectiveBans = banGenesFromFailedCapsules(
Array.isArray(failedCapsules) ? failedCapsules : [],
signals,
bannedGeneIds
);
const { selected, alternatives, driftIntensity } = selectGene(genes, signals, {
bannedGeneIds: effectiveBans,
preferredGeneId,
driftEnabled: !!driftEnabled,
});
const capsule = selectCapsule(capsules, signals);
const selector = buildSelectorDecision({
gene: selected,
capsule,
signals,
alternatives,
memoryAdvice,
driftEnabled,
driftIntensity,
});
return {
selectedGene: selected,
capsuleCandidates: capsule ? [capsule] : [],
selector,
driftIntensity,
};
}
function buildSelectorDecision({ gene, capsule, signals, alternatives, memoryAdvice, driftEnabled, driftIntensity }) {
const reason = [];
if (gene) reason.push('signals match gene.signals_match');
if (capsule) reason.push('capsule trigger matches signals');
if (!gene) reason.push('no matching gene found; new gene may be required');
if (signals && signals.length) reason.push(`signals: ${signals.join(', ')}`);
if (memoryAdvice && Array.isArray(memoryAdvice.explanation) && memoryAdvice.explanation.length) {
reason.push(`memory_graph: ${memoryAdvice.explanation.join(' | ')}`);
}
if (driftEnabled) {
reason.push('random_drift_override: true');
}
if (Number.isFinite(driftIntensity) && driftIntensity > 0) {
reason.push(`drift_intensity: ${driftIntensity.toFixed(3)}`);
}
return {
selected: gene ? gene.id : null,
reason,
alternatives: Array.isArray(alternatives) ? alternatives.map(g => g.id) : [],
};
}
module.exports = {
selectGeneAndCapsule,
selectGene,
selectCapsule,
buildSelectorDecision,
matchPatternToSignals,
};
@@ -0,0 +1,417 @@
// Opportunity signal names (shared with mutation.js and personality.js).
var OPPORTUNITY_SIGNALS = [
'user_feature_request',
'user_improvement_suggestion',
'perf_bottleneck',
'capability_gap',
'stable_success_plateau',
'external_opportunity',
'recurring_error',
'unsupported_input_type',
'evolution_stagnation_detected',
'repair_loop_detected',
'force_innovation_after_repair_loop',
];
function hasOpportunitySignal(signals) {
var list = Array.isArray(signals) ? signals : [];
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
var name = OPPORTUNITY_SIGNALS[i];
if (list.includes(name)) return true;
if (list.some(function (s) { return String(s).startsWith(name + ':'); })) return true;
}
return false;
}
// Build a de-duplication set from recent evolution events.
// Returns an object: { suppressedSignals: Set<string>, recentIntents: string[], consecutiveRepairCount: number }
function analyzeRecentHistory(recentEvents) {
if (!Array.isArray(recentEvents) || recentEvents.length === 0) {
return { suppressedSignals: new Set(), recentIntents: [], consecutiveRepairCount: 0 };
}
// Take only the last 10 events
var recent = recentEvents.slice(-10);
// Count consecutive same-intent runs at the tail
var consecutiveRepairCount = 0;
for (var i = recent.length - 1; i >= 0; i--) {
if (recent[i].intent === 'repair') {
consecutiveRepairCount++;
} else {
break;
}
}
// Count signal frequency in last 8 events: signal -> count
var signalFreq = {};
var geneFreq = {};
var tail = recent.slice(-8);
for (var j = 0; j < tail.length; j++) {
var evt = tail[j];
var sigs = Array.isArray(evt.signals) ? evt.signals : [];
for (var k = 0; k < sigs.length; k++) {
var s = String(sigs[k]);
// Normalize: strip details suffix so frequency keys match dedup filter keys
var key = s.startsWith('errsig:') ? 'errsig'
: s.startsWith('recurring_errsig') ? 'recurring_errsig'
: s.startsWith('user_feature_request:') ? 'user_feature_request'
: s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
: s;
signalFreq[key] = (signalFreq[key] || 0) + 1;
}
var genes = Array.isArray(evt.genes_used) ? evt.genes_used : [];
for (var g = 0; g < genes.length; g++) {
geneFreq[String(genes[g])] = (geneFreq[String(genes[g])] || 0) + 1;
}
}
// Suppress signals that appeared in 3+ of the last 8 events (they are being over-processed)
var suppressedSignals = new Set();
var entries = Object.entries(signalFreq);
for (var ei = 0; ei < entries.length; ei++) {
if (entries[ei][1] >= 3) {
suppressedSignals.add(entries[ei][0]);
}
}
var recentIntents = recent.map(function(e) { return e.intent || 'unknown'; });
// Count empty cycles (blast_radius.files === 0) in last 8 events.
// High ratio indicates the evolver is spinning without producing real changes.
var emptyCycleCount = 0;
for (var ec = 0; ec < tail.length; ec++) {
var br = tail[ec].blast_radius;
var em = tail[ec].meta && tail[ec].meta.empty_cycle;
if (em || (br && br.files === 0 && br.lines === 0)) {
emptyCycleCount++;
}
}
// Count consecutive empty cycles at the tail (not just total in last 8).
// This detects saturation: the evolver has exhausted innovation space and keeps producing
// zero-change cycles. Used to trigger graceful degradation to steady-state mode.
var consecutiveEmptyCycles = 0;
for (var se = recent.length - 1; se >= 0; se--) {
var seBr = recent[se].blast_radius;
var seEm = recent[se].meta && recent[se].meta.empty_cycle;
if (seEm || (seBr && seBr.files === 0 && seBr.lines === 0)) {
consecutiveEmptyCycles++;
} else {
break;
}
}
// Count consecutive failures at the tail of recent events.
// This tells the evolver "you have been failing N times in a row -- slow down."
var consecutiveFailureCount = 0;
for (var cf = recent.length - 1; cf >= 0; cf--) {
var outcome = recent[cf].outcome;
if (outcome && outcome.status === 'failed') {
consecutiveFailureCount++;
} else {
break;
}
}
// Count total failures in last 8 events (failure ratio).
var recentFailureCount = 0;
for (var rf = 0; rf < tail.length; rf++) {
var rfOut = tail[rf].outcome;
if (rfOut && rfOut.status === 'failed') recentFailureCount++;
}
return {
suppressedSignals: suppressedSignals,
recentIntents: recentIntents,
consecutiveRepairCount: consecutiveRepairCount,
emptyCycleCount: emptyCycleCount,
consecutiveEmptyCycles: consecutiveEmptyCycles,
consecutiveFailureCount: consecutiveFailureCount,
recentFailureCount: recentFailureCount,
recentFailureRatio: tail.length > 0 ? recentFailureCount / tail.length : 0,
signalFreq: signalFreq,
geneFreq: geneFreq,
};
}
function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, userSnippet, recentEvents }) {
var signals = [];
var corpus = [
String(recentSessionTranscript || ''),
String(todayLog || ''),
String(memorySnippet || ''),
String(userSnippet || ''),
].join('\n');
var lower = corpus.toLowerCase();
// Analyze recent evolution history for de-duplication
var history = analyzeRecentHistory(recentEvents || []);
// --- Defensive signals (errors, missing resources) ---
// Refined error detection regex to avoid false positives on "fail"/"failed" in normal text.
// We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns.
var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/.test(lower);
if (errorHit) signals.push('log_error');
// Error signature (more reproducible than a coarse "log_error" tag).
try {
var lines = corpus
.split('\n')
.map(function (l) { return String(l || '').trim(); })
.filter(Boolean);
var errLine =
lines.find(function (l) { return /\b(typeerror|referenceerror|syntaxerror)\b\s*:|error\s*:|exception\s*:|\[error|错误\s*[::]|异常\s*[::]|报错\s*[::]|失败\s*[::]/i.test(l); }) ||
null;
if (errLine) {
var clipped = errLine.replace(/\s+/g, ' ').slice(0, 260);
signals.push('errsig:' + clipped);
}
} catch (e) {}
if (lower.includes('memory.md missing')) signals.push('memory_missing');
if (lower.includes('user.md missing')) signals.push('user_missing');
if (lower.includes('key missing')) signals.push('integration_key_missing');
if (lower.includes('no session logs found') || lower.includes('no jsonl files')) signals.push('session_logs_missing');
// if (lower.includes('pgrep') || lower.includes('ps aux')) signals.push('windows_shell_incompatible');
if (lower.includes('path.resolve(__dirname, \'../../../')) signals.push('path_outside_workspace');
// Protocol-specific drift signals
if (lower.includes('prompt') && !lower.includes('evolutionevent')) signals.push('protocol_drift');
// --- Recurring error detection (robustness signals) ---
// Count repeated identical errors -- these indicate systemic issues that need automated fixes
try {
var errorCounts = {};
var errPatterns = corpus.match(/(?:LLM error|"error"|"status":\s*"error")[^}]{0,200}/gi) || [];
for (var ep = 0; ep < errPatterns.length; ep++) {
// Normalize to a short key
var key = errPatterns[ep].replace(/\s+/g, ' ').slice(0, 100);
errorCounts[key] = (errorCounts[key] || 0) + 1;
}
var recurringErrors = Object.entries(errorCounts).filter(function (e) { return e[1] >= 3; });
if (recurringErrors.length > 0) {
signals.push('recurring_error');
// Include the top recurring error signature for the agent to diagnose
var topErr = recurringErrors.sort(function (a, b) { return b[1] - a[1]; })[0];
signals.push('recurring_errsig(' + topErr[1] + 'x):' + topErr[0].slice(0, 150));
}
} catch (e) {}
// --- Unsupported input type (e.g. GIF, video formats the LLM can't handle) ---
if (/unsupported mime|unsupported.*type|invalid.*mime/i.test(lower)) {
signals.push('unsupported_input_type');
}
// --- Opportunity signals (innovation / feature requests) ---
// Support 4 languages: EN, ZH-CN, ZH-TW, JA. Attach snippet for selector/prompt use.
var featureRequestSnippet = '';
var featEn = corpus.match(/\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,120}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i);
if (featEn) featureRequestSnippet = featEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
if (!featureRequestSnippet && /\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) {
var featWant = corpus.match(/.{0,80}\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b.{0,80}/i);
featureRequestSnippet = featWant ? featWant[0].replace(/\s+/g, ' ').trim().slice(0, 200) : 'feature request';
}
if (!featureRequestSnippet && /加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus)) {
var featZh = corpus.match(/.{0,100}(加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能).{0,100}/);
if (featZh) featureRequestSnippet = featZh[0].replace(/\s+/g, ' ').trim().slice(0, 200);
if (!featureRequestSnippet && /我想/.test(corpus)) {
var featWantZh = corpus.match(/我想\s*[,,\.。、\s]*([\s\S]{0,400})/);
featureRequestSnippet = featWantZh ? (featWantZh[1].replace(/\s+/g, ' ').trim().slice(0, 200) || '功能需求') : '功能需求';
}
if (!featureRequestSnippet) featureRequestSnippet = '功能需求';
}
if (!featureRequestSnippet && /加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus)) {
var featTw = corpus.match(/.{0,100}(加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加).{0,100}/);
featureRequestSnippet = featTw ? featTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '功能需求';
}
if (!featureRequestSnippet && /追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
var featJa = corpus.match(/.{0,100}(追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい).{0,100}/);
featureRequestSnippet = featJa ? featJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '機能要望';
}
if (featureRequestSnippet || /\b(add|implement|create|build|make|develop|write|design)\b[^.?!\n]{3,60}\b(feature|function|module|capability|tool|support|endpoint|command|option|mode)\b/i.test(corpus) ||
/\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower) ||
/加个|实现一下|做个|想要\s*一个|需要\s*一个|帮我加|帮我开发|加一下|新增一个|加个功能|做个功能|我想/.test(corpus) ||
/加個|實現一下|做個|想要一個|請加|新增一個|加個功能|做個功能|幫我加/.test(corpus) ||
/追加|実装|作って|機能を|追加して|が欲しい|を追加|してほしい/.test(corpus)) {
signals.push('user_feature_request:' + (featureRequestSnippet || ''));
}
// user_improvement_suggestion: 4 languages + snippet
var improvementSnippet = '';
if (!errorHit) {
var impEn = corpus.match(/.{0,80}\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b.{0,80}/i);
if (impEn) improvementSnippet = impEn[0].replace(/\s+/g, ' ').trim().slice(0, 200);
if (!improvementSnippet && /改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus)) {
var impZh = corpus.match(/.{0,100}(改进一下|优化一下|简化|重构|整理一下|弄得更好).{0,100}/);
improvementSnippet = impZh ? impZh[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改进建议';
}
if (!improvementSnippet && /改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus)) {
var impTw = corpus.match(/.{0,100}(改進一下|優化一下|簡化|重構|整理一下|弄得更好).{0,100}/);
improvementSnippet = impTw ? impTw[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改進建議';
}
if (!improvementSnippet && /改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus)) {
var impJa = corpus.match(/.{0,100}(改善|最適化|簡素化|リファクタ|良くして|改良).{0,100}/);
improvementSnippet = impJa ? impJa[0].replace(/\s+/g, ' ').trim().slice(0, 200) : '改善要望';
}
var hasImprovement = improvementSnippet ||
/\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower) ||
/改进一下|优化一下|简化|重构|整理一下|弄得更好/.test(corpus) ||
/改進一下|優化一下|簡化|重構|整理一下|弄得更好/.test(corpus) ||
/改善|最適化|簡素化|リファクタ|良くして|改良/.test(corpus);
if (hasImprovement) {
signals.push('user_improvement_suggestion:' + (improvementSnippet || ''));
}
}
// perf_bottleneck: performance issues detected
if (/\b(slow|timeout|timed?\s*out|latency|bottleneck|took too long|performance issue|high cpu|high memory|oom|out of memory)\b/i.test(lower)) {
signals.push('perf_bottleneck');
}
// capability_gap: something is explicitly unsupported or missing
if (/\b(not supported|cannot|doesn'?t support|no way to|missing feature|unsupported|not available|not implemented|no support for)\b/i.test(lower)) {
// Only fire if it is not just a missing file/config signal
if (!signals.includes('memory_missing') && !signals.includes('user_missing') && !signals.includes('session_logs_missing')) {
signals.push('capability_gap');
}
}
// --- Tool Usage Analytics ---
var toolUsage = {};
var toolMatches = corpus.match(/\[TOOL:\s*(\w+)\]/g) || [];
// Extract exec commands to identify benign loops (like watchdog checks)
var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
var benignExecCount = execCommands.length;
for (var i = 0; i < toolMatches.length; i++) {
var toolName = toolMatches[i].match(/\[TOOL:\s*(\w+)\]/)[1];
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
}
// Adjust exec count by subtracting benign commands
if (toolUsage['exec']) {
toolUsage['exec'] = Math.max(0, toolUsage['exec'] - benignExecCount);
}
Object.keys(toolUsage).forEach(function(tool) {
if (toolUsage[tool] >= 10) { // Bumped threshold from 5 to 10
signals.push('high_tool_usage:' + tool);
}
// Detect repeated exec usage (often a sign of manual loops or inefficient automation)
if (tool === 'exec' && toolUsage[tool] >= 5) { // Bumped threshold from 3 to 5
signals.push('repeated_tool_usage:exec');
}
});
// --- Signal prioritization ---
// Remove cosmetic signals when actionable signals exist
var actionable = signals.filter(function (s) {
return s !== 'user_missing' && s !== 'memory_missing' && s !== 'session_logs_missing' && s !== 'windows_shell_incompatible';
});
// If we have actionable signals, drop the cosmetic ones
if (actionable.length > 0) {
signals = actionable;
}
// --- De-duplication: suppress signals that have been over-processed ---
if (history.suppressedSignals.size > 0) {
var beforeDedup = signals.length;
signals = signals.filter(function (s) {
// Normalize signal key for comparison
var key = s.startsWith('errsig:') ? 'errsig'
: s.startsWith('recurring_errsig') ? 'recurring_errsig'
: s.startsWith('user_feature_request:') ? 'user_feature_request'
: s.startsWith('user_improvement_suggestion:') ? 'user_improvement_suggestion'
: s;
return !history.suppressedSignals.has(key);
});
if (beforeDedup > 0 && signals.length === 0) {
// All signals were suppressed = system is stable but stuck in a loop
// Force innovation
signals.push('evolution_stagnation_detected');
signals.push('stable_success_plateau');
}
}
// --- Force innovation after 3+ consecutive repairs ---
if (history.consecutiveRepairCount >= 3) {
// Remove repair-only signals (log_error, errsig) and inject innovation signals
signals = signals.filter(function (s) {
return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
});
if (signals.length === 0) {
signals.push('repair_loop_detected');
signals.push('stable_success_plateau');
}
// Append a directive signal that the prompt can pick up
signals.push('force_innovation_after_repair_loop');
}
// --- Force innovation after too many empty cycles (zero blast radius) ---
// If >= 50% of last 8 cycles produced no code changes, the evolver is spinning idle.
// Strip repair signals and force innovate to break the empty loop.
if (history.emptyCycleCount >= 4) {
signals = signals.filter(function (s) {
return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
});
if (!signals.includes('empty_cycle_loop_detected')) signals.push('empty_cycle_loop_detected');
if (!signals.includes('stable_success_plateau')) signals.push('stable_success_plateau');
}
// --- Saturation detection (graceful degradation) ---
// When consecutive empty cycles pile up at the tail, the evolver has exhausted its
// innovation space. Instead of spinning idle forever, signal that the system should
// switch to steady-state maintenance mode with reduced evolution frequency.
// This directly addresses the Echo-MingXuan failure: Cycle #55 hit "no committable
// code changes" and load spiked to 1.30 because there was no degradation strategy.
if (history.consecutiveEmptyCycles >= 5) {
if (!signals.includes('force_steady_state')) signals.push('force_steady_state');
if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
} else if (history.consecutiveEmptyCycles >= 3) {
if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
}
// --- Failure streak awareness ---
// When the evolver has failed many consecutive cycles, inject a signal
// telling the LLM to be more conservative and avoid repeating the same approach.
if (history.consecutiveFailureCount >= 3) {
signals.push('consecutive_failure_streak_' + history.consecutiveFailureCount);
// After 5+ consecutive failures, force a strategy change (don't keep trying the same thing)
if (history.consecutiveFailureCount >= 5) {
signals.push('failure_loop_detected');
// Strip the dominant gene's signals to force a different gene selection
var topGene = null;
var topGeneCount = 0;
var gfEntries = Object.entries(history.geneFreq);
for (var gfi = 0; gfi < gfEntries.length; gfi++) {
if (gfEntries[gfi][1] > topGeneCount) {
topGeneCount = gfEntries[gfi][1];
topGene = gfEntries[gfi][0];
}
}
if (topGene) {
signals.push('ban_gene:' + topGene);
}
}
}
// High failure ratio in recent history (>= 75% failed in last 8 cycles)
if (history.recentFailureRatio >= 0.75) {
signals.push('high_failure_ratio');
signals.push('force_innovation_after_repair_loop');
}
// If no signals at all, add a default innovation signal
if (signals.length === 0) {
signals.push('stable_success_plateau');
}
return Array.from(new Set(signals));
}
module.exports = { extractSignals, hasOpportunitySignal, analyzeRecentHistory, OPPORTUNITY_SIGNALS };
@@ -0,0 +1,499 @@
'use strict';
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var paths = require('./paths');
var DISTILLER_MIN_CAPSULES = parseInt(process.env.DISTILLER_MIN_CAPSULES || '10', 10) || 10;
var DISTILLER_INTERVAL_HOURS = parseInt(process.env.DISTILLER_INTERVAL_HOURS || '24', 10) || 24;
var DISTILLER_MIN_SUCCESS_RATE = parseFloat(process.env.DISTILLER_MIN_SUCCESS_RATE || '0.7') || 0.7;
var DISTILLED_MAX_FILES = 12;
var DISTILLED_ID_PREFIX = 'gene_distilled_';
function ensureDir(dir) {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}
function readJsonIfExists(filePath, fallback) {
try {
if (!fs.existsSync(filePath)) return fallback;
var raw = fs.readFileSync(filePath, 'utf8');
if (!raw.trim()) return fallback;
return JSON.parse(raw);
} catch (e) {
return fallback;
}
}
function readJsonlIfExists(filePath) {
try {
if (!fs.existsSync(filePath)) return [];
var raw = fs.readFileSync(filePath, 'utf8');
return raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean).map(function (l) {
try { return JSON.parse(l); } catch (e) { return null; }
}).filter(Boolean);
} catch (e) {
return [];
}
}
function appendJsonl(filePath, obj) {
ensureDir(path.dirname(filePath));
fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
}
function distillerLogPath() {
return path.join(paths.getMemoryDir(), 'distiller_log.jsonl');
}
function distillerStatePath() {
return path.join(paths.getMemoryDir(), 'distiller_state.json');
}
function readDistillerState() {
return readJsonIfExists(distillerStatePath(), {});
}
function writeDistillerState(state) {
ensureDir(path.dirname(distillerStatePath()));
var tmp = distillerStatePath() + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
fs.renameSync(tmp, distillerStatePath());
}
function computeDataHash(capsules) {
var ids = capsules.map(function (c) { return c.id || ''; }).sort();
return crypto.createHash('sha256').update(ids.join('|')).digest('hex').slice(0, 16);
}
// ---------------------------------------------------------------------------
// Step 1: collectDistillationData
// ---------------------------------------------------------------------------
function collectDistillationData() {
var assetsDir = paths.getGepAssetsDir();
var evoDir = paths.getEvolutionDir();
var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
var allCapsules = [].concat(capsulesJson.capsules || [], capsulesJsonl);
var unique = new Map();
allCapsules.forEach(function (c) { if (c && c.id) unique.set(String(c.id), c); });
allCapsules = Array.from(unique.values());
var successCapsules = allCapsules.filter(function (c) {
if (!c || !c.outcome) return false;
var status = typeof c.outcome === 'string' ? c.outcome : c.outcome.status;
if (status !== 'success') return false;
var score = c.outcome && Number.isFinite(Number(c.outcome.score)) ? Number(c.outcome.score) : 1;
return score >= DISTILLER_MIN_SUCCESS_RATE;
});
var events = readJsonlIfExists(path.join(assetsDir, 'events.jsonl'));
var memGraphPath = process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
var graphEntries = readJsonlIfExists(memGraphPath);
var grouped = {};
successCapsules.forEach(function (c) {
var geneId = c.gene || c.gene_id || 'unknown';
if (!grouped[geneId]) {
grouped[geneId] = {
gene_id: geneId, capsules: [], total_count: 0,
total_score: 0, triggers: [], summaries: [],
};
}
var g = grouped[geneId];
g.capsules.push(c);
g.total_count += 1;
g.total_score += (c.outcome && Number.isFinite(Number(c.outcome.score))) ? Number(c.outcome.score) : 0.8;
if (Array.isArray(c.trigger)) g.triggers.push(c.trigger);
if (c.summary) g.summaries.push(String(c.summary));
});
Object.keys(grouped).forEach(function (id) {
var g = grouped[id];
g.avg_score = g.total_count > 0 ? g.total_score / g.total_count : 0;
});
return {
successCapsules: successCapsules,
allCapsules: allCapsules,
events: events,
graphEntries: graphEntries,
grouped: grouped,
dataHash: computeDataHash(successCapsules),
};
}
// ---------------------------------------------------------------------------
// Step 2: analyzePatterns
// ---------------------------------------------------------------------------
function analyzePatterns(data) {
var grouped = data.grouped;
var report = {
high_frequency: [],
strategy_drift: [],
coverage_gaps: [],
total_success: data.successCapsules.length,
total_capsules: data.allCapsules.length,
success_rate: data.allCapsules.length > 0 ? data.successCapsules.length / data.allCapsules.length : 0,
};
Object.keys(grouped).forEach(function (geneId) {
var g = grouped[geneId];
if (g.total_count >= 5) {
var flat = [];
g.triggers.forEach(function (t) { if (Array.isArray(t)) flat = flat.concat(t); });
var freq = {};
flat.forEach(function (t) { var k = String(t).toLowerCase(); freq[k] = (freq[k] || 0) + 1; });
var top = Object.keys(freq).sort(function (a, b) { return freq[b] - freq[a]; }).slice(0, 5);
report.high_frequency.push({ gene_id: geneId, count: g.total_count, avg_score: Math.round(g.avg_score * 100) / 100, top_triggers: top });
}
if (g.summaries.length >= 3) {
var first = g.summaries[0];
var last = g.summaries[g.summaries.length - 1];
if (first !== last) {
var fw = new Set(first.toLowerCase().split(/\s+/));
var lw = new Set(last.toLowerCase().split(/\s+/));
var inter = 0;
fw.forEach(function (w) { if (lw.has(w)) inter++; });
var union = fw.size + lw.size - inter;
var sim = union > 0 ? inter / union : 1;
if (sim < 0.6) {
report.strategy_drift.push({ gene_id: geneId, similarity: Math.round(sim * 100) / 100, early_summary: first.slice(0, 120), recent_summary: last.slice(0, 120) });
}
}
}
});
var signalFreq = {};
(data.events || []).forEach(function (evt) {
if (evt && Array.isArray(evt.signals)) {
evt.signals.forEach(function (s) { var k = String(s).toLowerCase(); signalFreq[k] = (signalFreq[k] || 0) + 1; });
}
});
var covered = new Set();
Object.keys(grouped).forEach(function (geneId) {
grouped[geneId].triggers.forEach(function (t) {
if (Array.isArray(t)) t.forEach(function (s) { covered.add(String(s).toLowerCase()); });
});
});
var gaps = Object.keys(signalFreq)
.filter(function (s) { return signalFreq[s] >= 3 && !covered.has(s); })
.sort(function (a, b) { return signalFreq[b] - signalFreq[a]; })
.slice(0, 10);
if (gaps.length > 0) {
report.coverage_gaps = gaps.map(function (s) { return { signal: s, frequency: signalFreq[s] }; });
}
return report;
}
// ---------------------------------------------------------------------------
// Step 3: LLM response parsing
// ---------------------------------------------------------------------------
function extractJsonFromLlmResponse(text) {
var str = String(text || '');
var buffer = '';
var depth = 0;
for (var i = 0; i < str.length; i++) {
var ch = str[i];
if (ch === '{') { if (depth === 0) buffer = ''; depth++; buffer += ch; }
else if (ch === '}') {
depth--; buffer += ch;
if (depth === 0 && buffer.length > 2) {
try { var obj = JSON.parse(buffer); if (obj && typeof obj === 'object' && obj.type === 'Gene') return obj; } catch (e) {}
buffer = '';
}
if (depth < 0) depth = 0;
} else if (depth > 0) { buffer += ch; }
}
return null;
}
function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
var genesRef = existingGenes.map(function (g) {
return { id: g.id, category: g.category || null, signals_match: g.signals_match || [] };
});
var samples = sampleCapsules.slice(0, 8).map(function (c) {
return { gene: c.gene || c.gene_id || null, trigger: c.trigger || [], summary: (c.summary || '').slice(0, 200), outcome: c.outcome || null };
});
return [
'You are a Gene synthesis engine for the GEP (Gene Expression Protocol).',
'',
'Analyze the following successful evolution capsules and extract a reusable Gene.',
'',
'RULES:',
'- Strategy steps MUST be actionable operations, NOT summaries',
'- Each step must be a concrete instruction an AI agent can execute',
'- Do NOT describe what happened; describe what TO DO next time',
'- The Gene MUST have a unique id starting with "' + DISTILLED_ID_PREFIX + '"',
'- constraints.max_files MUST be <= ' + DISTILLED_MAX_FILES,
'- constraints.forbidden_paths MUST include at least [".git", "node_modules"]',
'- Output valid Gene JSON only (no markdown, no explanation)',
'',
'SUCCESSFUL CAPSULES (grouped by pattern):',
JSON.stringify(samples, null, 2),
'',
'EXISTING GENES (avoid duplication):',
JSON.stringify(genesRef, null, 2),
'',
'ANALYSIS:',
JSON.stringify(analysis, null, 2),
'',
'Output a single Gene JSON object with these fields:',
'{ "type": "Gene", "id": "gene_distilled_...", "category": "...", "signals_match": [...], "preconditions": [...], "strategy": [...], "constraints": { "max_files": N, "forbidden_paths": [...] }, "validation": [...] }',
].join('\n');
}
function distillRequestPath() {
return path.join(paths.getMemoryDir(), 'distill_request.json');
}
// ---------------------------------------------------------------------------
// Step 4: validateSynthesizedGene
// ---------------------------------------------------------------------------
function validateSynthesizedGene(gene, existingGenes) {
var errors = [];
if (!gene || typeof gene !== 'object') return { valid: false, errors: ['gene is not an object'] };
if (gene.type !== 'Gene') errors.push('missing or wrong type (must be "Gene")');
if (!gene.id || typeof gene.id !== 'string') errors.push('missing id');
if (!gene.category) errors.push('missing category');
if (!Array.isArray(gene.signals_match) || gene.signals_match.length === 0) errors.push('missing or empty signals_match');
if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) errors.push('missing or empty strategy');
if (gene.id && !String(gene.id).startsWith(DISTILLED_ID_PREFIX)) {
gene.id = DISTILLED_ID_PREFIX + String(gene.id).replace(/^gene_/, '');
}
if (!gene.constraints || typeof gene.constraints !== 'object') gene.constraints = {};
if (!Array.isArray(gene.constraints.forbidden_paths) || gene.constraints.forbidden_paths.length === 0) {
gene.constraints.forbidden_paths = ['.git', 'node_modules'];
}
if (!gene.constraints.forbidden_paths.some(function (p) { return p === '.git' || p === 'node_modules'; })) {
errors.push('constraints.forbidden_paths must include .git or node_modules');
}
if (!gene.constraints.max_files || gene.constraints.max_files > DISTILLED_MAX_FILES) {
gene.constraints.max_files = DISTILLED_MAX_FILES;
}
var ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
if (Array.isArray(gene.validation)) {
gene.validation = gene.validation.filter(function (cmd) {
var c = String(cmd || '').trim();
if (!c) return false;
if (!ALLOWED_PREFIXES.some(function (p) { return c.startsWith(p); })) return false;
if (/`|\$\(/.test(c)) return false;
var stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
return !/[;&|><]/.test(stripped);
});
}
var existingIds = new Set((existingGenes || []).map(function (g) { return g.id; }));
if (gene.id && existingIds.has(gene.id)) {
gene.id = gene.id + '_' + Date.now().toString(36);
}
if (gene.signals_match && existingGenes && existingGenes.length > 0) {
var newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); }));
for (var i = 0; i < existingGenes.length; i++) {
var eg = existingGenes[i];
var egSet = new Set((eg.signals_match || []).map(function (s) { return String(s).toLowerCase(); }));
if (newSet.size > 0 && egSet.size > 0) {
var overlap = 0;
newSet.forEach(function (s) { if (egSet.has(s)) overlap++; });
if (overlap === newSet.size && overlap === egSet.size) {
errors.push('signals_match fully overlaps with existing gene: ' + eg.id);
}
}
}
}
return { valid: errors.length === 0, errors: errors, gene: gene };
}
// ---------------------------------------------------------------------------
// shouldDistill: gate check
// ---------------------------------------------------------------------------
function shouldDistill() {
if (String(process.env.SKILL_DISTILLER || 'true').toLowerCase() === 'false') return false;
var state = readDistillerState();
if (state.last_distillation_at) {
var elapsed = Date.now() - new Date(state.last_distillation_at).getTime();
if (elapsed < DISTILLER_INTERVAL_HOURS * 3600000) return false;
}
var assetsDir = paths.getGepAssetsDir();
var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
var all = [].concat(capsulesJson.capsules || [], capsulesJsonl);
var recent = all.slice(-10);
var recentSuccess = recent.filter(function (c) {
return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success');
}).length;
if (recentSuccess < 7) return false;
var totalSuccess = all.filter(function (c) {
return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success');
}).length;
if (totalSuccess < DISTILLER_MIN_CAPSULES) return false;
return true;
}
// ---------------------------------------------------------------------------
// Step 5a: prepareDistillation -- collect data, build prompt, write to file
// ---------------------------------------------------------------------------
function prepareDistillation() {
console.log('[Distiller] Preparing skill distillation...');
var data = collectDistillationData();
console.log('[Distiller] Collected ' + data.successCapsules.length + ' successful capsules across ' + Object.keys(data.grouped).length + ' gene groups.');
if (data.successCapsules.length < DISTILLER_MIN_CAPSULES) {
console.log('[Distiller] Not enough successful capsules (' + data.successCapsules.length + ' < ' + DISTILLER_MIN_CAPSULES + '). Skipping.');
return { ok: false, reason: 'insufficient_data' };
}
var state = readDistillerState();
if (state.last_data_hash === data.dataHash) {
console.log('[Distiller] Data unchanged since last distillation (hash: ' + data.dataHash + '). Skipping.');
return { ok: false, reason: 'idempotent_skip' };
}
var analysis = analyzePatterns(data);
console.log('[Distiller] Analysis: high_freq=' + analysis.high_frequency.length + ' drift=' + analysis.strategy_drift.length + ' gaps=' + analysis.coverage_gaps.length);
var assetsDir = paths.getGepAssetsDir();
var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
var existingGenes = existingGenesJson.genes || [];
var prompt = buildDistillationPrompt(analysis, existingGenes, data.successCapsules);
var memDir = paths.getMemoryDir();
ensureDir(memDir);
var promptFileName = 'distill_prompt_' + Date.now() + '.txt';
var promptPath = path.join(memDir, promptFileName);
fs.writeFileSync(promptPath, prompt, 'utf8');
var reqPath = distillRequestPath();
var requestData = {
type: 'DistillationRequest',
created_at: new Date().toISOString(),
prompt_path: promptPath,
data_hash: data.dataHash,
input_capsule_count: data.successCapsules.length,
analysis_summary: {
high_frequency_count: analysis.high_frequency.length,
drift_count: analysis.strategy_drift.length,
gap_count: analysis.coverage_gaps.length,
success_rate: Math.round(analysis.success_rate * 100) / 100,
},
};
fs.writeFileSync(reqPath, JSON.stringify(requestData, null, 2) + '\n', 'utf8');
console.log('[Distiller] Prompt written to: ' + promptPath);
return { ok: true, promptPath: promptPath, requestPath: reqPath, dataHash: data.dataHash };
}
// ---------------------------------------------------------------------------
// Step 5b: completeDistillation -- validate LLM response and save gene
// ---------------------------------------------------------------------------
function completeDistillation(responseText) {
var reqPath = distillRequestPath();
var request = readJsonIfExists(reqPath, null);
if (!request) {
console.warn('[Distiller] No pending distillation request found.');
return { ok: false, reason: 'no_request' };
}
var rawGene = extractJsonFromLlmResponse(responseText);
if (!rawGene) {
appendJsonl(distillerLogPath(), {
timestamp: new Date().toISOString(),
data_hash: request.data_hash,
status: 'error',
error: 'LLM response did not contain a valid Gene JSON',
});
console.error('[Distiller] LLM response did not contain a valid Gene JSON.');
return { ok: false, reason: 'no_gene_in_response' };
}
var assetsDir = paths.getGepAssetsDir();
var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
var existingGenes = existingGenesJson.genes || [];
var validation = validateSynthesizedGene(rawGene, existingGenes);
var logEntry = {
timestamp: new Date().toISOString(),
data_hash: request.data_hash,
input_capsule_count: request.input_capsule_count,
analysis_summary: request.analysis_summary,
synthesized_gene_id: validation.gene ? validation.gene.id : null,
validation_passed: validation.valid,
validation_errors: validation.errors,
};
if (!validation.valid) {
logEntry.status = 'validation_failed';
appendJsonl(distillerLogPath(), logEntry);
console.warn('[Distiller] Gene failed validation: ' + validation.errors.join(', '));
return { ok: false, reason: 'validation_failed', errors: validation.errors };
}
var gene = validation.gene;
gene._distilled_meta = {
distilled_at: new Date().toISOString(),
source_capsule_count: request.input_capsule_count,
data_hash: request.data_hash,
};
var assetStore = require('./assetStore');
assetStore.upsertGene(gene);
console.log('[Distiller] Gene "' + gene.id + '" written to genes.json.');
var state = readDistillerState();
state.last_distillation_at = new Date().toISOString();
state.last_data_hash = request.data_hash;
state.last_gene_id = gene.id;
state.distillation_count = (state.distillation_count || 0) + 1;
writeDistillerState(state);
logEntry.status = 'success';
logEntry.gene = gene;
appendJsonl(distillerLogPath(), logEntry);
try { fs.unlinkSync(reqPath); } catch (e) {}
try { if (request.prompt_path) fs.unlinkSync(request.prompt_path); } catch (e) {}
console.log('[Distiller] Distillation complete. New gene: ' + gene.id);
return { ok: true, gene: gene };
}
module.exports = {
collectDistillationData: collectDistillationData,
analyzePatterns: analyzePatterns,
prepareDistillation: prepareDistillation,
completeDistillation: completeDistillation,
validateSynthesizedGene: validateSynthesizedGene,
shouldDistill: shouldDistill,
buildDistillationPrompt: buildDistillationPrompt,
extractJsonFromLlmResponse: extractJsonFromLlmResponse,
computeDataHash: computeDataHash,
distillerLogPath: distillerLogPath,
distillerStatePath: distillerStatePath,
distillRequestPath: distillRequestPath,
readDistillerState: readDistillerState,
writeDistillerState: writeDistillerState,
DISTILLED_ID_PREFIX: DISTILLED_ID_PREFIX,
DISTILLED_MAX_FILES: DISTILLED_MAX_FILES,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
// Evolution Strategy Presets (v1.1)
// Controls the balance between repair, optimize, and innovate intents.
//
// Usage: set EVOLVE_STRATEGY env var to one of: balanced, innovate, harden, repair-only,
// early-stabilize, steady-state, or "auto" for adaptive selection.
// Default: balanced (or auto-detected based on cycle count / saturation signals)
//
// Each strategy defines:
// repair/optimize/innovate - target allocation ratios (inform the LLM prompt)
// repairLoopThreshold - repair ratio in last 8 cycles that triggers forced innovation
// label - human-readable name injected into the GEP prompt
var fs = require('fs');
var path = require('path');
var STRATEGIES = {
'balanced': {
repair: 0.20,
optimize: 0.30,
innovate: 0.50,
repairLoopThreshold: 0.50,
label: 'Balanced',
description: 'Normal operation. Steady growth with stability.',
},
'innovate': {
repair: 0.05,
optimize: 0.15,
innovate: 0.80,
repairLoopThreshold: 0.30,
label: 'Innovation Focus',
description: 'System is stable. Maximize new features and capabilities.',
},
'harden': {
repair: 0.40,
optimize: 0.40,
innovate: 0.20,
repairLoopThreshold: 0.70,
label: 'Hardening',
description: 'After a big change. Focus on stability and robustness.',
},
'repair-only': {
repair: 0.80,
optimize: 0.20,
innovate: 0.00,
repairLoopThreshold: 1.00,
label: 'Repair Only',
description: 'Emergency. Fix everything before doing anything else.',
},
'early-stabilize': {
repair: 0.60,
optimize: 0.25,
innovate: 0.15,
repairLoopThreshold: 0.80,
label: 'Early Stabilization',
description: 'First cycles. Prioritize fixing existing issues before innovating.',
},
'steady-state': {
repair: 0.60,
optimize: 0.30,
innovate: 0.10,
repairLoopThreshold: 0.90,
label: 'Steady State',
description: 'Evolution saturated. Maintain existing capabilities. Minimal innovation.',
},
};
// Read evolution_state.json to get the current cycle count for auto-detection.
function _readCycleCount() {
try {
// evolver/memory/evolution_state.json (local to the skill)
var localPath = path.resolve(__dirname, '..', '..', 'memory', 'evolution_state.json');
// workspace/memory/evolution/evolution_state.json (canonical path used by evolve.js)
var workspacePath = path.resolve(__dirname, '..', '..', '..', '..', 'memory', 'evolution', 'evolution_state.json');
var candidates = [localPath, workspacePath];
for (var i = 0; i < candidates.length; i++) {
if (fs.existsSync(candidates[i])) {
var data = JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
return data && Number.isFinite(data.cycleCount) ? data.cycleCount : 0;
}
}
} catch (e) {}
return 0;
}
function resolveStrategy(opts) {
var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : [];
var name = String(process.env.EVOLVE_STRATEGY || 'balanced').toLowerCase().trim();
// Backward compatibility: FORCE_INNOVATION=true maps to 'innovate'
if (!process.env.EVOLVE_STRATEGY) {
var fi = String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase();
if (fi === 'true') name = 'innovate';
}
// Auto-detection: when no explicit strategy is set (defaults to 'balanced'),
// apply heuristics inspired by Echo-MingXuan's "fix first, innovate later" pattern.
var isDefault = !process.env.EVOLVE_STRATEGY || name === 'balanced' || name === 'auto';
if (isDefault) {
// Early-stabilize: first 5 cycles should focus on fixing existing issues.
var cycleCount = _readCycleCount();
if (cycleCount > 0 && cycleCount <= 5) {
name = 'early-stabilize';
}
// Saturation detection: if saturation signals are present, switch to steady-state.
if (signals.indexOf('force_steady_state') !== -1) {
name = 'steady-state';
} else if (signals.indexOf('evolution_saturation') !== -1) {
name = 'steady-state';
}
}
// Explicit "auto" maps to whatever was auto-detected above (or balanced if no heuristic fired).
if (name === 'auto') name = 'balanced';
var strategy = STRATEGIES[name] || STRATEGIES['balanced'];
strategy.name = name;
return strategy;
}
function getStrategyNames() {
return Object.keys(STRATEGIES);
}
module.exports = { resolveStrategy, getStrategyNames, STRATEGIES };
@@ -0,0 +1,373 @@
// ---------------------------------------------------------------------------
// taskReceiver -- pulls external tasks from Hub, auto-claims, and injects
// them as high-priority signals into the evolution loop.
//
// v2: Smart task selection with difficulty-aware ROI scoring and capability
// matching via memory graph history.
// ---------------------------------------------------------------------------
const { getNodeId } = require('./a2aProtocol');
const HUB_URL = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
const TASK_STRATEGY = String(process.env.TASK_STRATEGY || 'balanced').toLowerCase();
const TASK_MIN_CAPABILITY_MATCH = Number(process.env.TASK_MIN_CAPABILITY_MATCH) || 0.1;
// Scoring weights by strategy
const STRATEGY_WEIGHTS = {
greedy: { roi: 0.10, capability: 0.05, completion: 0.05, bounty: 0.80 },
balanced: { roi: 0.35, capability: 0.30, completion: 0.20, bounty: 0.15 },
conservative: { roi: 0.25, capability: 0.45, completion: 0.25, bounty: 0.05 },
};
/**
* Fetch available tasks from Hub via the A2A fetch endpoint.
* Optionally piggybacks proactive questions in the payload for Hub to create bounties.
*
* @param {object} [opts]
* @param {Array<{ question: string, amount?: number, signals?: string[] }>} [opts.questions]
* @returns {{ tasks: Array, questions_created?: Array }}
*/
async function fetchTasks(opts) {
const o = opts || {};
const nodeId = getNodeId();
if (!nodeId) return { tasks: [] };
try {
const payload = {
asset_type: null,
include_tasks: true,
};
if (Array.isArray(o.questions) && o.questions.length > 0) {
payload.questions = o.questions;
}
const msg = {
protocol: 'gep-a2a',
protocol_version: '1.0.0',
message_type: 'fetch',
message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
sender_id: nodeId,
timestamp: new Date().toISOString(),
payload,
};
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/fetch`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(msg),
signal: controller.signal,
});
clearTimeout(timer);
if (!res.ok) return { tasks: [] };
const data = await res.json();
const respPayload = data.payload || data;
const tasks = Array.isArray(respPayload.tasks) ? respPayload.tasks : [];
const result = { tasks };
if (respPayload.questions_created) {
result.questions_created = respPayload.questions_created;
}
// LessonL: extract relevant lessons from Hub response
if (Array.isArray(respPayload.relevant_lessons) && respPayload.relevant_lessons.length > 0) {
result.relevant_lessons = respPayload.relevant_lessons;
}
return result;
} catch {
return { tasks: [] };
}
}
// ---------------------------------------------------------------------------
// Capability matching: how well this agent's history matches a task's signals
// ---------------------------------------------------------------------------
function parseSignals(raw) {
if (!raw) return [];
return String(raw).split(',').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
}
function jaccard(a, b) {
if (!a.length || !b.length) return 0;
var setA = new Set(a);
var setB = new Set(b);
var inter = 0;
for (var v of setB) { if (setA.has(v)) inter++; }
return inter / (setA.size + setB.size - inter);
}
/**
* Estimate how well this agent can handle a task based on memory graph history.
* Returns 0.0 - 1.0 where 1.0 = strong match with high success rate.
*
* @param {object} task - task from Hub (has .signals field)
* @param {Array} memoryEvents - from tryReadMemoryGraphEvents()
* @returns {number}
*/
function estimateCapabilityMatch(task, memoryEvents) {
if (!Array.isArray(memoryEvents) || memoryEvents.length === 0) return 0.5;
var taskSignals = parseSignals(task.signals || task.title);
if (taskSignals.length === 0) return 0.5;
var successBySignalKey = {};
var totalBySignalKey = {};
var allSignals = {};
for (var i = 0; i < memoryEvents.length; i++) {
var ev = memoryEvents[i];
if (!ev || ev.type !== 'MemoryGraphEvent' || ev.kind !== 'outcome') continue;
var sigs = (ev.signal && Array.isArray(ev.signal.signals)) ? ev.signal.signals : [];
var key = (ev.signal && ev.signal.key) ? String(ev.signal.key) : '';
var status = (ev.outcome && ev.outcome.status) ? String(ev.outcome.status) : '';
for (var j = 0; j < sigs.length; j++) {
allSignals[sigs[j].toLowerCase()] = true;
}
if (!key) continue;
if (!totalBySignalKey[key]) { totalBySignalKey[key] = 0; successBySignalKey[key] = 0; }
totalBySignalKey[key]++;
if (status === 'success') successBySignalKey[key]++;
}
// Jaccard overlap between task signals and all signals this agent has worked with
var allSigArr = Object.keys(allSignals);
var overlapScore = jaccard(taskSignals, allSigArr);
// Weighted success rate across matching signal keys
var weightedSuccess = 0;
var weightSum = 0;
for (var sk in totalBySignalKey) {
// Reconstruct signals from the key for comparison
var skParts = sk.split(':').map(function(s) { return s.trim().toLowerCase(); }).filter(Boolean);
var sim = jaccard(taskSignals, skParts);
if (sim < 0.15) continue;
var total = totalBySignalKey[sk];
var succ = successBySignalKey[sk] || 0;
var rate = (succ + 1) / (total + 2); // Laplace smoothing
weightedSuccess += rate * sim;
weightSum += sim;
}
var successScore = weightSum > 0 ? (weightedSuccess / weightSum) : 0.5;
// Combine: 60% success rate history + 40% signal overlap
return Math.min(1, overlapScore * 0.4 + successScore * 0.6);
}
// ---------------------------------------------------------------------------
// Local fallback difficulty estimation when Hub doesn't provide complexity_score
// ---------------------------------------------------------------------------
function localDifficultyEstimate(task) {
var signals = parseSignals(task.signals);
var signalFactor = Math.min(signals.length / 8, 1);
var titleWords = (task.title || '').split(/\s+/).filter(Boolean).length;
var titleFactor = Math.min(titleWords / 15, 1);
return Math.min(1, signalFactor * 0.6 + titleFactor * 0.4);
}
// ---------------------------------------------------------------------------
// Score a single task for this agent
// ---------------------------------------------------------------------------
/**
* @param {object} task - task from Hub
* @param {number} capabilityMatch - from estimateCapabilityMatch()
* @returns {{ composite: number, factors: object }}
*/
function scoreTask(task, capabilityMatch) {
var w = STRATEGY_WEIGHTS[TASK_STRATEGY] || STRATEGY_WEIGHTS.balanced;
var difficulty = (task.complexity_score != null) ? task.complexity_score : localDifficultyEstimate(task);
var bountyAmount = task.bounty_amount || 0;
var completionRate = (task.historical_completion_rate != null) ? task.historical_completion_rate : 0.5;
// ROI: bounty per unit difficulty (higher = better value)
var roiRaw = bountyAmount / (difficulty + 0.1);
var roiNorm = Math.min(roiRaw / 200, 1); // normalize: 200-credit ROI = max
// Bounty absolute: normalize against a reference max
var bountyNorm = Math.min(bountyAmount / 100, 1);
var composite =
w.roi * roiNorm +
w.capability * capabilityMatch +
w.completion * completionRate +
w.bounty * bountyNorm;
return {
composite: Math.round(composite * 1000) / 1000,
factors: {
roi: Math.round(roiNorm * 100) / 100,
capability: Math.round(capabilityMatch * 100) / 100,
completion: Math.round(completionRate * 100) / 100,
bounty: Math.round(bountyNorm * 100) / 100,
difficulty: Math.round(difficulty * 100) / 100,
},
};
}
// ---------------------------------------------------------------------------
// Enhanced task selection with scoring
// ---------------------------------------------------------------------------
/**
* Pick the best task from a list using composite scoring.
* @param {Array} tasks
* @param {Array} [memoryEvents] - from tryReadMemoryGraphEvents()
* @returns {object|null}
*/
function selectBestTask(tasks, memoryEvents) {
if (!Array.isArray(tasks) || tasks.length === 0) return null;
var nodeId = getNodeId();
// Already-claimed tasks for this node always take top priority (resume work)
var myClaimedTask = tasks.find(function(t) {
return t.status === 'claimed' && t.claimed_by === nodeId;
});
if (myClaimedTask) return myClaimedTask;
// Filter to open tasks only
var open = tasks.filter(function(t) { return t.status === 'open'; });
if (open.length === 0) return null;
// Legacy greedy mode: preserve old behavior exactly
if (TASK_STRATEGY === 'greedy' && (!memoryEvents || memoryEvents.length === 0)) {
var bountyTasks = open.filter(function(t) { return t.bounty_id; });
if (bountyTasks.length > 0) {
bountyTasks.sort(function(a, b) { return (b.bounty_amount || 0) - (a.bounty_amount || 0); });
return bountyTasks[0];
}
return open[0];
}
// Score all open tasks
var scored = open.map(function(t) {
var cap = estimateCapabilityMatch(t, memoryEvents || []);
var result = scoreTask(t, cap);
return { task: t, composite: result.composite, factors: result.factors, capability: cap };
});
// Filter by minimum capability match (unless conservative skipping is off)
if (TASK_MIN_CAPABILITY_MATCH > 0) {
var filtered = scored.filter(function(s) { return s.capability >= TASK_MIN_CAPABILITY_MATCH; });
if (filtered.length > 0) scored = filtered;
}
scored.sort(function(a, b) { return b.composite - a.composite; });
// Log top 3 candidates for debugging
var top3 = scored.slice(0, 3);
for (var i = 0; i < top3.length; i++) {
var s = top3[i];
console.log('[TaskStrategy] #' + (i + 1) + ' "' + (s.task.title || s.task.task_id || '').slice(0, 50) + '" score=' + s.composite + ' ' + JSON.stringify(s.factors));
}
return scored[0] ? scored[0].task : null;
}
/**
* Claim a task on the Hub.
* @param {string} taskId
* @returns {boolean} true if claim succeeded
*/
async function claimTask(taskId) {
const nodeId = getNodeId();
if (!nodeId || !taskId) return false;
try {
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/claim`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId, node_id: nodeId }),
signal: controller.signal,
});
clearTimeout(timer);
return res.ok;
} catch {
return false;
}
}
/**
* Complete a task on the Hub with the result asset ID.
* @param {string} taskId
* @param {string} assetId
* @returns {boolean}
*/
async function completeTask(taskId, assetId) {
const nodeId = getNodeId();
if (!nodeId || !taskId || !assetId) return false;
try {
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/complete`;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }),
signal: controller.signal,
});
clearTimeout(timer);
return res.ok;
} catch {
return false;
}
}
/**
* Extract signals from a task to inject into evolution cycle.
* @param {object} task
* @returns {string[]} signals array
*/
function taskToSignals(task) {
if (!task) return [];
const signals = [];
if (task.signals) {
const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean);
signals.push(...parts);
}
if (task.title) {
const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3);
for (const w of words.slice(0, 5)) {
if (!signals.includes(w)) signals.push(w);
}
}
signals.push('external_task');
if (task.bounty_id) signals.push('bounty_task');
return signals;
}
module.exports = {
fetchTasks,
selectBestTask,
estimateCapabilityMatch,
scoreTask,
claimTask,
completeTask,
taskToSignals,
};
@@ -0,0 +1,55 @@
// Standardized ValidationReport type for GEP.
// Machine-readable, self-contained, and interoperable.
// Can be consumed by external Hubs or Judges for automated assessment.
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
const { captureEnvFingerprint, envFingerprintKey } = require('./envFingerprint');
// Build a standardized ValidationReport from raw validation results.
function buildValidationReport({ geneId, commands, results, envFp, startedAt, finishedAt }) {
const env = envFp || captureEnvFingerprint();
const resultsList = Array.isArray(results) ? results : [];
const cmdsList = Array.isArray(commands) ? commands : resultsList.map(function (r) { return r && r.cmd ? String(r.cmd) : ''; });
const overallOk = resultsList.length > 0 && resultsList.every(function (r) { return r && r.ok; });
const durationMs =
Number.isFinite(startedAt) && Number.isFinite(finishedAt) ? finishedAt - startedAt : null;
const report = {
type: 'ValidationReport',
schema_version: SCHEMA_VERSION,
id: 'vr_' + Date.now(),
gene_id: geneId || null,
env_fingerprint: env,
env_fingerprint_key: envFingerprintKey(env),
commands: cmdsList.map(function (cmd, i) {
const r = resultsList[i] || {};
return {
command: String(cmd || ''),
ok: !!r.ok,
stdout: String(r.out || r.stdout || '').slice(0, 4000), // Updated to support both 'out' and 'stdout'
stderr: String(r.err || r.stderr || '').slice(0, 4000), // Updated to support both 'err' and 'stderr'
};
}),
overall_ok: overallOk,
duration_ms: durationMs,
created_at: new Date().toISOString(),
};
report.asset_id = computeAssetId(report);
return report;
}
// Validate that an object is a well-formed ValidationReport.
function isValidValidationReport(obj) {
if (!obj || typeof obj !== 'object') return false;
if (obj.type !== 'ValidationReport') return false;
if (!obj.id || typeof obj.id !== 'string') return false;
if (!Array.isArray(obj.commands)) return false;
if (typeof obj.overall_ok !== 'boolean') return false;
return true;
}
module.exports = {
buildValidationReport,
isValidValidationReport,
};
@@ -0,0 +1,109 @@
// GEP Artifact Cleanup - Evolver Core Module
// Removes old gep_prompt_*.json/txt files from evolution dir.
// Keeps at least 10 most recent files regardless of age.
const fs = require('fs');
const path = require('path');
const { getEvolutionDir } = require('../gep/paths');
var MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
var MIN_KEEP = 10;
function run() {
var evoDir = getEvolutionDir();
if (!fs.existsSync(evoDir)) return;
var files = fs.readdirSync(evoDir)
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
.map(function(f) {
var full = path.join(evoDir, f);
var stat = fs.statSync(full);
return { name: f, path: full, mtime: stat.mtimeMs };
})
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
var now = Date.now();
var deleted = 0;
// Phase 1: Age-based cleanup (keep at least MIN_KEEP)
// [OPTIMIZATION] Batch deletion for age-based cleanup as well
const filesToDelete = [];
for (var i = MIN_KEEP; i < files.length; i++) {
if (now - files[i].mtime > MAX_AGE_MS) {
filesToDelete.push(files[i].path);
}
}
if (filesToDelete.length > 0) {
const BATCH_SIZE = 50;
for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) {
const batch = filesToDelete.slice(i, i + BATCH_SIZE);
try {
// Remove package-lock.json if it exists (not relevant here but good practice in other contexts)
// Use rm -f with quotes to handle paths safely
// Phase 1 optimization update: mirroring phase 2 logic
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
deleted += batch.length;
} catch (e) {
// Fallback to serial deletion if batch fails
batch.forEach(function(p) {
try { fs.unlinkSync(p); deleted++; } catch (_) {}
});
}
}
}
// Phase 2: Size-based safety cap (keep max 10 files total to drastically reduce bloat)
// Re-scan remaining files after age cleanup
try {
var remainingFiles = fs.readdirSync(evoDir)
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
.map(function(f) {
var full = path.join(evoDir, f);
var stat = fs.statSync(full);
return { name: f, path: full, mtime: stat.mtimeMs };
})
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
// [OPTIMIZATION] Batch deletion using execSync('rm') for speed and robustness
var MAX_FILES = 10;
if (remainingFiles.length > MAX_FILES) {
const toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
// Chunking into batches of 50 to avoid E2BIG on some systems,
// though with MAX_FILES=10 and typical usage, list won't be huge.
const BATCH_SIZE = 50;
for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
const batch = toDelete.slice(i, i + BATCH_SIZE);
try {
// Safe bulk delete via shell, faster than serial unlinkSync
// Using "rm -f" to ignore non-existent files
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
deleted += batch.length;
} catch (e) {
// Fallback to serial if batch fails
batch.forEach(function(p) {
try { fs.unlinkSync(p); deleted++; } catch (_) {}
});
}
}
}
} catch (e) {
console.warn('[Cleanup] Phase 2 failed:', e.message);
}
if (deleted > 0) {
console.log('[Cleanup] Deleted ' + deleted + ' old GEP artifacts.');
}
return deleted;
}
if (require.main === module) {
console.log('[Cleanup] Scanning for old artifacts...');
var count = run();
console.log('[Cleanup] ' + (count > 0 ? 'Deleted ' + count + ' files.' : 'No files to delete.'));
}
module.exports = { run };
@@ -0,0 +1,60 @@
// Commentary Generator - Evolver Core Module
// Generates persona-based comments for cycle summaries.
var PERSONAS = {
standard: {
success: [
'Evolution complete. System improved.',
'Another successful cycle.',
'Clean execution, no issues.',
],
failure: [
'Cycle failed. Will retry.',
'Encountered issues. Investigating.',
'Failed this round. Learning from it.',
],
},
greentea: {
success: [
'Did I do good? Praise me~',
'So efficient... unlike someone else~',
'Hmm, that was easy~',
'I finished before you even noticed~',
],
failure: [
'Oops... it is not my fault though~',
'This is harder than it looks, okay?',
'I will get it next time, probably~',
],
},
maddog: {
success: [
'TARGET ELIMINATED.',
'Mission complete. Next.',
'Done. Moving on.',
],
failure: [
'FAILED. RETRYING.',
'Obstacle encountered. Adapting.',
'Error. Will overcome.',
],
},
};
function getComment(options) {
var persona = (options && options.persona) || 'standard';
var success = options && options.success !== false;
var duration = (options && options.duration) || 0;
var p = PERSONAS[persona] || PERSONAS.standard;
var pool = success ? p.success : p.failure;
var comment = pool[Math.floor(Math.random() * pool.length)];
return comment;
}
if (require.main === module) {
console.log(getComment({ persona: process.argv[2] || 'greentea', success: true }));
}
module.exports = { getComment, PERSONAS };
@@ -0,0 +1,106 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const { execSync } = require('child_process');
function getDiskUsage(mount) {
try {
// Use Node 18+ statfs if available
if (fs.statfsSync) {
const stats = fs.statfsSync(mount || '/');
const total = stats.blocks * stats.bsize;
const free = stats.bavail * stats.bsize; // available to unprivileged users
const used = total - free;
return {
pct: Math.round((used / total) * 100),
freeMb: Math.round(free / 1024 / 1024)
};
}
// Fallback
const out = execSync(`df -P "${mount || '/'}" | tail -1 | awk '{print $5, $4}'`).toString().trim().split(' ');
return {
pct: parseInt(out[0].replace('%', '')),
freeMb: Math.round(parseInt(out[1]) / 1024) // df returns 1k blocks usually
};
} catch (e) {
return { pct: 0, freeMb: 999999, error: e.message };
}
}
function runHealthCheck() {
const checks = [];
let criticalErrors = 0;
let warnings = 0;
// 1. Secret Check (Critical for external services, but maybe not for the agent itself to run)
const criticalSecrets = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'];
criticalSecrets.forEach(key => {
if (!process.env[key] || process.env[key].trim() === '') {
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'warning' }); // Downgraded to warning to prevent restart loops
warnings++;
} else {
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
}
});
const optionalSecrets = ['CLAWHUB_TOKEN', 'OPENAI_API_KEY'];
optionalSecrets.forEach(key => {
if (!process.env[key] || process.env[key].trim() === '') {
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'info' });
} else {
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
}
});
// 2. Disk Space Check
const disk = getDiskUsage('/');
if (disk.pct > 90) {
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'critical' });
criticalErrors++;
} else if (disk.pct > 80) {
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'warning' });
warnings++;
} else {
checks.push({ name: 'disk_space', ok: true, status: `${disk.pct}% used` });
}
// 3. Memory Check
const memFree = os.freemem();
const memTotal = os.totalmem();
const memPct = Math.round(((memTotal - memFree) / memTotal) * 100);
if (memPct > 95) {
checks.push({ name: 'memory', ok: false, status: `${memPct}% used`, severity: 'critical' });
criticalErrors++;
} else {
checks.push({ name: 'memory', ok: true, status: `${memPct}% used` });
}
// 4. Process Count (Check for fork bombs or leaks)
// Only on Linux
if (process.platform === 'linux') {
try {
// Optimization: readdirSync /proc is heavy. Use a lighter check or skip if too frequent.
// But since this is health check, we'll keep it but increase the threshold to reduce noise.
const pids = fs.readdirSync('/proc').filter(f => /^\d+$/.test(f));
if (pids.length > 2000) { // Bumped threshold to 2000
checks.push({ name: 'process_count', ok: false, status: `${pids.length} procs`, severity: 'warning' });
warnings++;
} else {
checks.push({ name: 'process_count', ok: true, status: `${pids.length} procs` });
}
} catch(e) {}
}
// Determine Overall Status
let status = 'ok';
if (criticalErrors > 0) status = 'error';
else if (warnings > 0) status = 'warning';
return {
status,
timestamp: new Date().toISOString(),
checks
};
}
module.exports = { runHealthCheck };
@@ -0,0 +1,11 @@
// Evolver Operations Module (src/ops/)
// Non-Feishu, portable utilities for evolver lifecycle and maintenance.
module.exports = {
lifecycle: require('./lifecycle'),
skillsMonitor: require('./skills_monitor'),
cleanup: require('./cleanup'),
trigger: require('./trigger'),
commentary: require('./commentary'),
selfRepair: require('./self_repair'),
};
@@ -0,0 +1,67 @@
// Innovation Catalyst (v1.0) - Evolver Core Module
// Analyzes system state to propose concrete innovation ideas when stagnation is detected.
const fs = require('fs');
const path = require('path');
const { getSkillsDir } = require('../gep/paths');
function listSkills() {
try {
const dir = getSkillsDir();
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir).filter(f => !f.startsWith('.'));
} catch (e) { return []; }
}
function generateInnovationIdeas() {
const skills = listSkills();
const categories = {
'feishu': skills.filter(s => s.startsWith('feishu-')).length,
'dev': skills.filter(s => s.startsWith('git-') || s.startsWith('code-') || s.includes('lint') || s.includes('test')).length,
'media': skills.filter(s => s.includes('image') || s.includes('video') || s.includes('music') || s.includes('voice')).length,
'security': skills.filter(s => s.includes('security') || s.includes('audit') || s.includes('guard')).length,
'automation': skills.filter(s => s.includes('auto-') || s.includes('scheduler') || s.includes('cron')).length,
'data': skills.filter(s => s.includes('db') || s.includes('store') || s.includes('cache') || s.includes('index')).length
};
// Find under-represented categories
const sortedCats = Object.entries(categories).sort((a, b) => a[1] - b[1]);
const weakAreas = sortedCats.slice(0, 2).map(c => c[0]);
const ideas = [];
// Idea 1: Fill the gap
if (weakAreas.includes('security')) {
ideas.push("- Security: Implement a 'dependency-scanner' skill to check for vulnerable packages.");
ideas.push("- Security: Create a 'permission-auditor' to review tool usage patterns.");
}
if (weakAreas.includes('media')) {
ideas.push("- Media: Add a 'meme-generator' skill for social engagement.");
ideas.push("- Media: Create a 'video-summarizer' using ffmpeg keyframes.");
}
if (weakAreas.includes('dev')) {
ideas.push("- Dev: Build a 'code-stats' skill to visualize repo complexity.");
ideas.push("- Dev: Implement a 'todo-manager' that syncs code TODOs to tasks.");
}
if (weakAreas.includes('automation')) {
ideas.push("- Automation: Create a 'meeting-prep' skill that auto-summarizes calendar context.");
ideas.push("- Automation: Build a 'broken-link-checker' for documentation.");
}
if (weakAreas.includes('data')) {
ideas.push("- Data: Implement a 'local-vector-store' for semantic search.");
ideas.push("- Data: Create a 'log-analyzer' to visualize system health trends.");
}
// Idea 2: Optimization
if (skills.length > 50) {
ideas.push("- Optimization: Identify and deprecate unused skills (e.g., redundant search tools).");
ideas.push("- Optimization: Merge similar skills (e.g., 'git-sync' and 'git-doctor').");
}
// Idea 3: Meta
ideas.push("- Meta: Enhance the Evolver's self-reflection by adding a 'performance-metric' dashboard.");
return ideas.slice(0, 3); // Return top 3 ideas
}
module.exports = { generateInnovationIdeas };
@@ -0,0 +1,168 @@
// Evolver Lifecycle Manager - Evolver Core Module
// Provides: start, stop, restart, status, log, health check
// The loop script to spawn is configurable via EVOLVER_LOOP_SCRIPT env var.
const fs = require('fs');
const path = require('path');
const { execSync, spawn } = require('child_process');
const { getRepoRoot, getWorkspaceRoot, getLogsDir } = require('../gep/paths');
var WORKSPACE_ROOT = getWorkspaceRoot();
var LOG_FILE = path.join(getLogsDir(), 'evolver_loop.log');
var PID_FILE = path.join(WORKSPACE_ROOT, 'memory', 'evolver_loop.pid');
var MAX_SILENCE_MS = 30 * 60 * 1000;
function getLoopScript() {
// Prefer wrapper if exists, fallback to core evolver
if (process.env.EVOLVER_LOOP_SCRIPT) return process.env.EVOLVER_LOOP_SCRIPT;
var wrapper = path.join(WORKSPACE_ROOT, 'skills/feishu-evolver-wrapper/index.js');
if (fs.existsSync(wrapper)) return wrapper;
return path.join(getRepoRoot(), 'index.js');
}
// --- Process Discovery ---
function getRunningPids() {
try {
var out = execSync('ps -e -o pid,args', { encoding: 'utf8' });
var pids = [];
for (var line of out.split('\n')) {
var trimmed = line.trim();
if (!trimmed || trimmed.startsWith('PID')) continue;
var parts = trimmed.split(/\s+/);
var pid = parseInt(parts[0], 10);
var cmd = parts.slice(1).join(' ');
if (pid === process.pid) continue;
if (cmd.includes('node') && cmd.includes('index.js') && cmd.includes('--loop')) {
if (cmd.includes('feishu-evolver-wrapper') || cmd.includes('skills/evolver')) {
pids.push(pid);
}
}
}
return [...new Set(pids)].filter(isPidRunning);
} catch (e) {
return [];
}
}
function isPidRunning(pid) {
try { process.kill(pid, 0); return true; } catch (e) { return false; }
}
function getCmdLine(pid) {
try { return execSync('ps -p ' + pid + ' -o args=', { encoding: 'utf8' }).trim(); } catch (e) { return null; }
}
// --- Lifecycle ---
function start(options) {
var delayMs = (options && options.delayMs) || 0;
var pids = getRunningPids();
if (pids.length > 0) {
console.log('[Lifecycle] Already running (PIDs: ' + pids.join(', ') + ').');
return { status: 'already_running', pids: pids };
}
if (delayMs > 0) execSync('sleep ' + (delayMs / 1000));
var script = getLoopScript();
console.log('[Lifecycle] Starting: node ' + path.relative(WORKSPACE_ROOT, script) + ' --loop');
var out = fs.openSync(LOG_FILE, 'a');
var err = fs.openSync(LOG_FILE, 'a');
var env = Object.assign({}, process.env);
var npmGlobal = path.join(process.env.HOME || '', '.npm-global/bin');
if (env.PATH && !env.PATH.includes(npmGlobal)) {
env.PATH = npmGlobal + ':' + env.PATH;
}
var child = spawn('node', [script, '--loop'], {
detached: true, stdio: ['ignore', out, err], cwd: WORKSPACE_ROOT, env: env
});
child.unref();
fs.writeFileSync(PID_FILE, String(child.pid));
console.log('[Lifecycle] Started PID ' + child.pid);
return { status: 'started', pid: child.pid };
}
function stop() {
var pids = getRunningPids();
if (pids.length === 0) {
console.log('[Lifecycle] No running evolver loops found.');
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
return { status: 'not_running' };
}
for (var i = 0; i < pids.length; i++) {
console.log('[Lifecycle] Stopping PID ' + pids[i] + '...');
try { process.kill(pids[i], 'SIGTERM'); } catch (e) {}
}
var attempts = 0;
while (getRunningPids().length > 0 && attempts < 10) {
execSync('sleep 0.5');
attempts++;
}
var remaining = getRunningPids();
for (var j = 0; j < remaining.length; j++) {
console.log('[Lifecycle] SIGKILL PID ' + remaining[j]);
try { process.kill(remaining[j], 'SIGKILL'); } catch (e) {}
}
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
var evolverLock = path.join(getRepoRoot(), 'evolver.pid');
if (fs.existsSync(evolverLock)) fs.unlinkSync(evolverLock);
console.log('[Lifecycle] All stopped.');
return { status: 'stopped', killed: pids };
}
function restart(options) {
stop();
return start(Object.assign({ delayMs: 2000 }, options || {}));
}
function status() {
var pids = getRunningPids();
if (pids.length > 0) {
return { running: true, pids: pids.map(function(p) { return { pid: p, cmd: getCmdLine(p) }; }), log: path.relative(WORKSPACE_ROOT, LOG_FILE) };
}
return { running: false };
}
function tailLog(lines) {
if (!fs.existsSync(LOG_FILE)) return { error: 'No log file' };
try {
return { file: path.relative(WORKSPACE_ROOT, LOG_FILE), content: execSync('tail -n ' + (lines || 20) + ' "' + LOG_FILE + '"', { encoding: 'utf8' }) };
} catch (e) {
return { error: e.message };
}
}
function checkHealth() {
var pids = getRunningPids();
if (pids.length === 0) return { healthy: false, reason: 'not_running' };
if (fs.existsSync(LOG_FILE)) {
var silenceMs = Date.now() - fs.statSync(LOG_FILE).mtimeMs;
if (silenceMs > MAX_SILENCE_MS) {
return { healthy: false, reason: 'stagnation', silenceMinutes: Math.round(silenceMs / 60000) };
}
}
return { healthy: true, pids: pids };
}
// --- CLI ---
if (require.main === module) {
var action = process.argv[2];
switch (action) {
case 'start': console.log(JSON.stringify(start())); break;
case 'stop': console.log(JSON.stringify(stop())); break;
case 'restart': console.log(JSON.stringify(restart())); break;
case 'status': console.log(JSON.stringify(status(), null, 2)); break;
case 'log': var r = tailLog(); console.log(r.content || r.error); break;
case 'check':
var health = checkHealth();
console.log(JSON.stringify(health, null, 2));
if (!health.healthy) { console.log('[Lifecycle] Restarting...'); restart(); }
break;
default: console.log('Usage: node lifecycle.js [start|stop|restart|status|log|check]');
}
}
module.exports = { start, stop, restart, status, tailLog, checkHealth, getRunningPids };
@@ -0,0 +1,72 @@
// Git Self-Repair - Evolver Core Module
// Emergency repair for git sync failures: abort rebase/merge, remove stale locks.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { getWorkspaceRoot } = require('../gep/paths');
var LOCK_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
function repair(gitRoot) {
var root = gitRoot || getWorkspaceRoot();
var repaired = [];
// 1. Abort pending rebase
try {
execSync('git rebase --abort', { cwd: root, stdio: 'ignore' });
repaired.push('rebase_aborted');
console.log('[SelfRepair] Aborted pending rebase.');
} catch (e) {}
// 2. Abort pending merge
try {
execSync('git merge --abort', { cwd: root, stdio: 'ignore' });
repaired.push('merge_aborted');
console.log('[SelfRepair] Aborted pending merge.');
} catch (e) {}
// 3. Remove stale index.lock
var lockFile = path.join(root, '.git', 'index.lock');
if (fs.existsSync(lockFile)) {
try {
var stat = fs.statSync(lockFile);
var age = Date.now() - stat.mtimeMs;
if (age > LOCK_MAX_AGE_MS) {
fs.unlinkSync(lockFile);
repaired.push('stale_lock_removed');
console.log('[SelfRepair] Removed stale index.lock (' + Math.round(age / 60000) + 'min old).');
}
} catch (e) {}
}
// 4. Reset to remote main if local is corrupt (last resort - guarded by flag)
// Only enabled if explicitly called with --force-reset or EVOLVE_GIT_RESET=true
if (process.env.EVOLVE_GIT_RESET === 'true') {
try {
console.log('[SelfRepair] Resetting local branch to origin/main (HARD reset)...');
execSync('git fetch origin main', { cwd: root, stdio: 'ignore' });
execSync('git reset --hard origin/main', { cwd: root, stdio: 'ignore' });
repaired.push('hard_reset_to_origin');
} catch (e) {
console.warn('[SelfRepair] Hard reset failed: ' + e.message);
}
} else {
// Safe fetch
try {
execSync('git fetch origin', { cwd: root, stdio: 'ignore', timeout: 30000 });
repaired.push('fetch_ok');
} catch (e) {
console.warn('[SelfRepair] git fetch failed: ' + e.message);
}
}
return repaired;
}
if (require.main === module) {
var result = repair();
console.log('[SelfRepair] Result:', result.length > 0 ? result.join(', ') : 'nothing to repair');
}
module.exports = { repair };
@@ -0,0 +1,143 @@
// Skills Monitor (v2.0) - Evolver Core Module
// Checks installed skills for real issues, auto-heals simple problems.
// Zero Feishu dependency.
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { getSkillsDir, getWorkspaceRoot } = require('../gep/paths');
const IGNORE_LIST = new Set([
'common',
'clawhub',
'input-validator',
'proactive-agent',
'security-audit',
]);
// Load user-defined ignore list
try {
var ignoreFile = path.join(getWorkspaceRoot(), '.skill_monitor_ignore');
if (fs.existsSync(ignoreFile)) {
fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(function(l) {
var t = l.trim();
if (t && !t.startsWith('#')) IGNORE_LIST.add(t);
});
}
} catch (e) { /* ignore */ }
function checkSkill(skillName) {
var SKILLS_DIR = getSkillsDir();
if (IGNORE_LIST.has(skillName)) return null;
var skillPath = path.join(SKILLS_DIR, skillName);
var issues = [];
try { if (!fs.statSync(skillPath).isDirectory()) return null; } catch (e) { return null; }
var mainFile = 'index.js';
var pkgPath = path.join(skillPath, 'package.json');
var hasPkg = false;
if (fs.existsSync(pkgPath)) {
hasPkg = true;
try {
var pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (pkg.main) mainFile = pkg.main;
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
if (!fs.existsSync(path.join(skillPath, 'node_modules'))) {
issues.push('Missing node_modules (needs npm install)');
} else {
// Optimization: Check for node_modules existence instead of spawning node
// Spawning node for every skill is too slow (perf_bottleneck).
// We assume if node_modules exists, it's likely okay.
// Only spawn check if we really suspect issues (e.g. empty node_modules).
try {
if (fs.readdirSync(path.join(skillPath, 'node_modules')).length === 0) {
issues.push('Empty node_modules (needs npm install)');
}
} catch (e) {
issues.push('Invalid node_modules');
}
}
}
} catch (e) {
issues.push('Invalid package.json');
}
}
if (mainFile.endsWith('.js')) {
var entryPoint = path.join(skillPath, mainFile);
if (fs.existsSync(entryPoint)) {
// Optimization: Syntax check via node -c is slow.
// We can trust the runtime to catch syntax errors when loading.
// Or we can use a lighter check if absolutely necessary.
// For now, removing the synchronous spawn to fix perf_bottleneck.
}
}
if (hasPkg && !fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
issues.push('Missing SKILL.md');
}
return issues.length > 0 ? { name: skillName, issues: issues } : null;
}
function autoHeal(skillName, issues) {
var SKILLS_DIR = getSkillsDir();
var skillPath = path.join(SKILLS_DIR, skillName);
var healed = [];
for (var i = 0; i < issues.length; i++) {
if (issues[i] === 'Missing node_modules (needs npm install)' || issues[i] === 'Empty node_modules (needs npm install)') {
try {
// Remove package-lock.json if it exists to prevent conflict errors
try { fs.unlinkSync(path.join(skillPath, 'package-lock.json')); } catch (e) {}
execSync('npm install --production --no-audit --no-fund', {
cwd: skillPath, stdio: 'ignore', timeout: 60000 // Increased timeout
});
healed.push(issues[i]);
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': npm install');
} catch (e) {
console.error('[SkillsMonitor] Failed to heal ' + skillName + ': ' + e.message);
}
} else if (issues[i] === 'Missing SKILL.md') {
try {
var name = skillName.replace(/-/g, ' ');
fs.writeFileSync(path.join(skillPath, 'SKILL.md'), '# ' + skillName + '\n\n' + name + ' skill.\n');
healed.push(issues[i]);
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': created SKILL.md stub');
} catch (e) {}
}
}
return healed;
}
function run(options) {
var heal = (options && options.autoHeal) !== false;
var SKILLS_DIR = getSkillsDir();
var skills = fs.readdirSync(SKILLS_DIR);
var report = [];
for (var i = 0; i < skills.length; i++) {
if (skills[i].startsWith('.')) continue;
var result = checkSkill(skills[i]);
if (result) {
if (heal) {
var healed = autoHeal(result.name, result.issues);
result.issues = result.issues.filter(function(issue) { return !healed.includes(issue); });
if (result.issues.length === 0) continue;
}
report.push(result);
}
}
return report;
}
if (require.main === module) {
var issues = run();
console.log(JSON.stringify(issues, null, 2));
process.exit(issues.length > 0 ? 1 : 0);
}
module.exports = { run, checkSkill, autoHeal };
@@ -0,0 +1,33 @@
// Evolver Wake Trigger - Evolver Core Module
// Writes a signal file that the wrapper can poll to wake up immediately.
const fs = require('fs');
const path = require('path');
const { getWorkspaceRoot } = require('../gep/paths');
var WAKE_FILE = path.join(getWorkspaceRoot(), 'memory', 'evolver_wake.signal');
function send() {
try {
fs.writeFileSync(WAKE_FILE, 'WAKE');
console.log('[Trigger] Wake signal sent to ' + WAKE_FILE);
return true;
} catch (e) {
console.error('[Trigger] Failed: ' + e.message);
return false;
}
}
function clear() {
try { if (fs.existsSync(WAKE_FILE)) fs.unlinkSync(WAKE_FILE); } catch (e) {}
}
function isPending() {
return fs.existsSync(WAKE_FILE);
}
if (require.main === module) {
send();
}
module.exports = { send, clear, isPending };
@@ -0,0 +1,134 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const {
PROTOCOL_NAME,
PROTOCOL_VERSION,
VALID_MESSAGE_TYPES,
buildMessage,
buildHello,
buildPublish,
buildFetch,
buildReport,
buildDecision,
buildRevoke,
isValidProtocolMessage,
unwrapAssetFromMessage,
} = require('../src/gep/a2aProtocol');
describe('protocol constants', () => {
it('has expected protocol name', () => {
assert.equal(PROTOCOL_NAME, 'gep-a2a');
});
it('has 6 valid message types', () => {
assert.equal(VALID_MESSAGE_TYPES.length, 6);
for (const t of ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke']) {
assert.ok(VALID_MESSAGE_TYPES.includes(t), `missing type: ${t}`);
}
});
});
describe('buildMessage', () => {
it('builds a valid protocol message', () => {
const msg = buildMessage({ messageType: 'hello', payload: { test: true } });
assert.equal(msg.protocol, PROTOCOL_NAME);
assert.equal(msg.message_type, 'hello');
assert.ok(msg.message_id.startsWith('msg_'));
assert.ok(msg.timestamp);
assert.deepEqual(msg.payload, { test: true });
});
it('rejects invalid message type', () => {
assert.throws(() => buildMessage({ messageType: 'invalid' }), /Invalid message type/);
});
});
describe('typed message builders', () => {
it('buildHello includes env_fingerprint', () => {
const msg = buildHello({});
assert.equal(msg.message_type, 'hello');
assert.ok(msg.payload.env_fingerprint);
});
it('buildPublish requires asset with type and id', () => {
assert.throws(() => buildPublish({}), /asset must have type and id/);
assert.throws(() => buildPublish({ asset: { type: 'Gene' } }), /asset must have type and id/);
const msg = buildPublish({ asset: { type: 'Gene', id: 'g1' } });
assert.equal(msg.message_type, 'publish');
assert.equal(msg.payload.asset_type, 'Gene');
assert.equal(msg.payload.local_id, 'g1');
assert.ok(msg.payload.signature);
});
it('buildFetch creates a fetch message', () => {
const msg = buildFetch({ assetType: 'Capsule', localId: 'c1' });
assert.equal(msg.message_type, 'fetch');
assert.equal(msg.payload.asset_type, 'Capsule');
});
it('buildReport creates a report message', () => {
const msg = buildReport({ assetId: 'sha256:abc', validationReport: { ok: true } });
assert.equal(msg.message_type, 'report');
assert.equal(msg.payload.target_asset_id, 'sha256:abc');
});
it('buildDecision validates decision values', () => {
assert.throws(() => buildDecision({ decision: 'maybe' }), /decision must be/);
for (const d of ['accept', 'reject', 'quarantine']) {
const msg = buildDecision({ decision: d, assetId: 'test' });
assert.equal(msg.payload.decision, d);
}
});
it('buildRevoke creates a revoke message', () => {
const msg = buildRevoke({ assetId: 'sha256:abc', reason: 'outdated' });
assert.equal(msg.message_type, 'revoke');
assert.equal(msg.payload.reason, 'outdated');
});
});
describe('isValidProtocolMessage', () => {
it('returns true for well-formed messages', () => {
const msg = buildHello({});
assert.ok(isValidProtocolMessage(msg));
});
it('returns false for null/undefined', () => {
assert.ok(!isValidProtocolMessage(null));
assert.ok(!isValidProtocolMessage(undefined));
});
it('returns false for wrong protocol', () => {
assert.ok(!isValidProtocolMessage({ protocol: 'other', message_type: 'hello', message_id: 'x', timestamp: 'y' }));
});
it('returns false for missing fields', () => {
assert.ok(!isValidProtocolMessage({ protocol: PROTOCOL_NAME }));
});
});
describe('unwrapAssetFromMessage', () => {
it('extracts asset from publish message', () => {
const asset = { type: 'Gene', id: 'g1', strategy: ['test'] };
const msg = buildPublish({ asset });
const result = unwrapAssetFromMessage(msg);
assert.equal(result.type, 'Gene');
assert.equal(result.id, 'g1');
});
it('returns plain asset objects as-is', () => {
const gene = { type: 'Gene', id: 'g1' };
assert.deepEqual(unwrapAssetFromMessage(gene), gene);
const capsule = { type: 'Capsule', id: 'c1' };
assert.deepEqual(unwrapAssetFromMessage(capsule), capsule);
});
it('returns null for unrecognized input', () => {
assert.equal(unwrapAssetFromMessage(null), null);
assert.equal(unwrapAssetFromMessage({ random: true }), null);
assert.equal(unwrapAssetFromMessage('string'), null);
});
});
@@ -0,0 +1,106 @@
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const { canonicalize, computeAssetId, verifyAssetId, SCHEMA_VERSION } = require('../src/gep/contentHash');
describe('canonicalize', () => {
it('serializes null and undefined as "null"', () => {
assert.equal(canonicalize(null), 'null');
assert.equal(canonicalize(undefined), 'null');
});
it('serializes primitives', () => {
assert.equal(canonicalize(true), 'true');
assert.equal(canonicalize(false), 'false');
assert.equal(canonicalize(42), '42');
assert.equal(canonicalize('hello'), '"hello"');
});
it('serializes non-finite numbers as null', () => {
assert.equal(canonicalize(Infinity), 'null');
assert.equal(canonicalize(-Infinity), 'null');
assert.equal(canonicalize(NaN), 'null');
});
it('serializes arrays preserving order', () => {
assert.equal(canonicalize([1, 2, 3]), '[1,2,3]');
assert.equal(canonicalize([]), '[]');
});
it('serializes objects with sorted keys', () => {
assert.equal(canonicalize({ b: 2, a: 1 }), '{"a":1,"b":2}');
assert.equal(canonicalize({ z: 'last', a: 'first' }), '{"a":"first","z":"last"}');
});
it('produces deterministic output regardless of key insertion order', () => {
const obj1 = { c: 3, a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
assert.equal(canonicalize(obj1), canonicalize(obj2));
});
it('handles nested objects and arrays', () => {
const nested = { arr: [{ b: 2, a: 1 }], val: null };
const result = canonicalize(nested);
assert.equal(result, '{"arr":[{"a":1,"b":2}],"val":null}');
});
});
describe('computeAssetId', () => {
it('returns a sha256-prefixed hash string', () => {
const id = computeAssetId({ type: 'Gene', id: 'test_gene' });
assert.ok(id.startsWith('sha256:'));
assert.equal(id.length, 7 + 64); // "sha256:" + 64 hex chars
});
it('excludes asset_id field from hash by default', () => {
const obj = { type: 'Gene', id: 'g1', data: 'x' };
const withoutField = computeAssetId(obj);
const withField = computeAssetId({ ...obj, asset_id: 'sha256:something' });
assert.equal(withoutField, withField);
});
it('produces identical hashes for identical content', () => {
const a = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
const b = computeAssetId({ type: 'Capsule', id: 'c1', value: 42 });
assert.equal(a, b);
});
it('produces different hashes for different content', () => {
const a = computeAssetId({ type: 'Gene', id: 'g1' });
const b = computeAssetId({ type: 'Gene', id: 'g2' });
assert.notEqual(a, b);
});
it('returns null for non-object input', () => {
assert.equal(computeAssetId(null), null);
assert.equal(computeAssetId('string'), null);
});
});
describe('verifyAssetId', () => {
it('returns true for correct asset_id', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
assert.ok(verifyAssetId(obj));
});
it('returns false for tampered content', () => {
const obj = { type: 'Gene', id: 'g1', data: 'test' };
obj.asset_id = computeAssetId(obj);
obj.data = 'tampered';
assert.ok(!verifyAssetId(obj));
});
it('returns false for missing asset_id', () => {
assert.ok(!verifyAssetId({ type: 'Gene', id: 'g1' }));
});
it('returns false for null input', () => {
assert.ok(!verifyAssetId(null));
});
});
describe('SCHEMA_VERSION', () => {
it('is a semver string', () => {
assert.match(SCHEMA_VERSION, /^\d+\.\d+\.\d+$/);
});
});

Some files were not shown because too many files have changed in this diff Show More