AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning

This commit is contained in:
Krilly
2026-03-04 13:29:22 +00:00
parent 29a98137a7
commit 57dd294675
13706 changed files with 2114953 additions and 237629 deletions

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.

View File

@@ -0,0 +1,254 @@
# 🧬 Capability Evolver
![Capability Evolver Cover](assets/cover.png)
[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.
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 (GEP v1.4)**: each evolution run is gated by an explicit Mutation object and an evolvable PersonalityState.
- **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 public changelog
- 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
```
## 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
### v1.6.0
- Add innovation/opportunity signal detection: user_feature_request, user_improvement_suggestion, perf_bottleneck, capability_gap, stable_success_plateau, external_opportunity.
- Add innovate Gene (gene_gep_innovate_from_opportunity) for proactive feature development.
- Auto-innovate mutation when opportunity signals are present (no longer requires --drift flag).
- Personality evolution now responds to opportunity signals by increasing creativity.
- Safety: repair still takes priority over innovate when errors are present.
### v1.5.1
- Add containerized vibe testing framework (Docker + node:22-bookworm, OpenClaw-compatible environment).
- 7 end-to-end tests: module load, dry-run solidify, schema compliance, A2A round-trip, full evolve+solidify, loop gating, env fingerprint.
- Add internal daemon loop with suicide guard for memory leak protection.
- One-command test: `npm run test:vibe`.
### v1.5.0
- Add content-addressable asset IDs (SHA-256 canonical hashing) for deduplication, tamper detection, and cross-node consistency.
- Add environment fingerprint capture (node version, platform, arch, evolver version) embedded in EvolutionEvents, Capsules, and ValidationReports.
- Add standardized ValidationReport type with machine-readable schema, full command results, and env fingerprint.
- Add GEP A2A protocol layer with 6 message types (hello/publish/fetch/report/decision/revoke) and pluggable transport interface.
- Add FileTransport as default A2A transport (JSONL outbox/inbox).
- Add asset_id integrity verification on A2A ingest; reject tampered assets.
- Add schema_version field to all GEP asset types (Gene, Capsule, EvolutionEvent, ValidationReport).
- Fix: dry-run mode no longer triggers rollback.
- Merge backport/online-fixes: self-contained crash recovery with recover_loop.js.
### v1.4.4
- Add validation command safety check: Gene validation commands are gated by prefix whitelist (node/npm/npx) and shell operator blocking.
- Add validation audit on A2A Gene promotion: external Genes with unsafe validation commands are rejected before promotion.
- Add Security Model documentation to README.
### v1.4.3
- Release preparation for v1.4.3.
### v1.4.2
- Add loop gating: do not start a new cycle until the previous run is solidified (prevents fast empty cycles).
- Preserve `last_solidify` when writing solidify state (merge instead of overwrite).
### v1.4.1
- Add execute-by-default bridge: after generating the GEP prompt, emit `sessions_spawn(...)` to spawn an executor agent.
- Write prompt artifacts to `memory/` for reliable handoff and auditing.
### v1.4.0
- Add explicit Mutation protocol (repair/optimize/innovate) and require Mutation per evolution run.
- Add evolvable PersonalityState with small PersonalityMutation steps and natural selection statistics.
- Extend EvolutionEvent with `mutation_id` and `personality_state`; record both into Memory Graph events.
- Add `scripts/gep_personality_report.js` to observe personality success rates and convergence.
### v1.3.1
- Release preparation for v1.3.1.
### v1.3.0
- Release preparation for v1.3.0.
### v1.2.0
- Memory Graph v2 and A2A exchange protocol integration.
### v1.1.0
- Public build/publish pipeline, prompt budget enforcement, and structured GEP asset storage.
## 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.
## License
MIT

View File

@@ -0,0 +1,185 @@
# 🧬 Capability Evolver能力进化引擎
[English Docs](README.md)
**“进化不是可选项,而是生存法则。”**
**Capability Evolver** 是一个元技能Meta-Skill赋予 OpenClaw 智能体自我反省的能力。它可以扫描自身的运行日志,识别效率低下或报错的地方,并自主编写代码补丁来优化自身性能。
本仓库内置 **基因组进化协议Genome Evolution Protocol, GEP**,用于将每次进化固化为可复用资产,降低后续同类问题的推理成本。
## 核心特性
- **自动日志分析**:自动扫描 `.jsonl` 会话日志,寻找错误模式。
- **自我修复**:检测运行时崩溃并编写修复补丁。
- **GEP 协议**:标准化进化流程与可复用资产,支持可审计与可共享。
- **突变协议与人格进化GEP v1.4**:每次进化必须显式声明 Mutation并维护可进化的 PersonalityState小步突变 + 自然选择收敛)。
- **动态集成**:自动检测并使用本地工具(如 `git-sync``feishu-card`),如果不存在则回退到通用模式,零依赖运行。
- **持续循环模式**:持续运行的自我修复循环。
## 使用方法
### 标准运行(自动化)
```bash
node index.js
```
### 审查模式(人工介入)
在应用更改前暂停,等待人工确认。
```bash
node index.js --review
```
### 持续循环(守护进程)
无限循环运行。适合作为后台服务。
```bash
node index.js --loop
```
## 典型使用场景
- 需要审计与可追踪的提示词演进
- 团队协作维护 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_REPORT_TOOL` |用于报告结果的工具名称(例如 `feishu-card` | `message` |
| `MEMORY_DIR` | 记忆文件路径 | `./memory` |
## 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修订/补丁):向后兼容的问题修复
## 更新日志
### v1.4.4
- 增加 validation 命令安全检查Gene validation 命令执行前通过前缀白名单node/npm/npx和 shell 操作符拦截进行门控。
- 增加 A2A Gene 提升审查:外部 Gene 的 validation 命令不安全时拒绝提升。
- 增加安全模型文档。
### v1.4.3
- v1.4.3 发布准备。
### v1.4.2
- 增加 loop 门控:上一轮未完成 solidify 时,不启动新一轮(避免 wrapper 造成超快空转)。
- 修复固化状态写入覆盖问题:写入 last_run 时合并保留 last_solidify。
### v1.4.1
- 增加默认执行桥接:生成 GEP prompt 后输出 `sessions_spawn(...)`,自动派发执行型子智能体。
- 将 prompt 作为交接工件写入 `memory/`,便于稳定交接与审计回放。
### v1.4.0
- 增加显式 Mutation Protocolrepair/optimize/innovate每轮进化必须生成 Mutation 对象并通过安全约束门控。
- 增加 Personality Evolution维护 PersonalityState小幅 PersonalityMutation单次不超过 ±0.2,最多 2 个参数),并基于成功率做自然选择收敛。
- EvolutionEvent 增加 `mutation_id``personality_state` 字段Memory Graph 同步记录 Mutation 与 Personality 的因果链路。
- 新增 `scripts/gep_personality_report.js`,用于统计不同人格配置下的成功率差异与收敛趋势。
### v1.3.1
- v1.3.1 发布准备。
### v1.3.0
- v1.3.0 发布准备。
### v1.2.0
- Memory Graph v2 与 A2A 进化资产交换集成。
### v1.1.0
- public 构建/发布流水线、提示词预算控制与结构化 GEP 资产持久化。
## 安全模型
本节描述 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 同步)仅在检测到相应插件存在时才会启用。
## 许可证
MIT

View File

@@ -0,0 +1,82 @@
---
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
```
## 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.
## License
MIT

View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
"slug": "evolver",
"version": "1.7.3",
"publishedAt": 1770562946750
}

View File

@@ -0,0 +1,3 @@
{"type":"CapabilityCandidate","id":"cand_2f2b5dde","title":"Repeated tool usage: read","source":"transcript","created_at":"2026-02-22T13:46:16.525Z","signals":["log_error","errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe","memory_missing","user_missing"],"shape":{"title":"Repeated tool usage: read","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 4 occurrences of tool call marker for read."}}
{"type":"CapabilityCandidate","id":"cand_5afdf7f3","title":"Repeated tool usage: exec","source":"transcript","created_at":"2026-02-22T13:46:16.525Z","signals":["log_error","errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe","memory_missing","user_missing"],"shape":{"title":"Repeated tool usage: exec","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 8 occurrences of tool call marker for exec."}}
{"type":"CapabilityCandidate","id":"cand_dd9067ac","title":"Repair recurring runtime errors","source":"signals","created_at":"2026-02-22T13:46:16.525Z","signals":["log_error","errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe","memory_missing","user_missing"],"shape":{"title":"Repair recurring runtime errors","input":"Recent session transcript + memory snippets + user instructions","output":"A safe, auditable evolution patch guided by GEP assets","invariants":"Protocol order, small reversible patches, validation, append-only events","params":"Signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Signal present: log_error"}}

View File

@@ -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": "/home/crishaocredits/.openclaw/workspace",
"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": "/home/crishaocredits/.openclaw/workspace",
"captured_at": "2026-02-07T15:32:21.678Z"
},
"a2a": {
"eligible_to_broadcast": false
},
"asset_id": "sha256:20d971a3c4cb2b75f9c045376d1aa003361c12a6b89a4b47b7e81dbd4f4d8fe8"
}
]
}

View File

@@ -0,0 +1,109 @@
{
"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": [
"node -e \"require('./src/evolve'); require('./src/gep/solidify'); console.log('ok')\"",
"node -e \"require('./src/gep/selector'); require('./src/gep/memoryGraph'); console.log('ok')\""
]
},
{
"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 -e \"require('./src/evolve'); require('./src/gep/prompt'); console.log('ok')\""
]
},
{
"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": 8,
"forbidden_paths": [
".git",
"node_modules",
"assets/gep/events.jsonl"
]
},
"validation": [
"node -e \"require('./src/evolve'); require('./src/gep/solidify'); console.log('ok')\""
]
}
]
}

View File

@@ -0,0 +1,171 @@
const evolve = require('./src/evolve');
const { solidify } = require('./src/gep/solidify');
const path = require('path');
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;
}
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).
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';
let currentSleepMs = Math.min(maxSleepMs, Math.max(minSleepMs, minSleepMs));
let cycleCount = 0;
while (true) {
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)})`);
const child = spawn(process.execPath, [__filename, ...args], {
detached: true,
stdio: 'ignore',
env: process.env,
});
child.unref();
process.exit(0);
}
}
// Jitter to avoid lockstep restarts.
const jitter = Math.floor(Math.random() * 250);
await sleepMs(currentSleepMs + jitter);
}
} 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));
process.exit(res && res.ok ? 0 : 2);
} catch (error) {
console.error('[SOLIDIFY] Error:', error);
process.exit(2);
}
} else {
console.log(`Usage: node index.js [run|/evolve|solidify] [--loop]
- solidify flags:
- --dry-run
- --no-rollback
- --intent=repair|optimize|innovate
- --summary=...`);
}
}
if (require.main === module) {
main();
}

View File

@@ -0,0 +1,104 @@
{
"last_run": {
"run_id": "run_1771767976531",
"created_at": "2026-02-22T13:46:16.547Z",
"parent_event_id": null,
"selected_gene_id": "gene_gep_repair_from_errors",
"selected_capsule_id": "capsule_1770477654236",
"selector": {
"selected": "gene_gep_repair_from_errors",
"reason": [
"signals match gene.signals_match",
"capsule trigger matches signals",
"signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing",
"memory_graph: memory_prefer:gene_gep_repair_from_errors"
],
"alternatives": []
},
"signals": [
"log_error",
"errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe",
"memory_missing",
"user_missing"
],
"mutation": {
"type": "Mutation",
"id": "mut_1771767976529",
"category": "repair",
"trigger_signals": [
"log_error",
"errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe",
"memory_missing",
"user_missing"
],
"target": "gene:gene_gep_repair_from_errors",
"expected_effect": "reduce runtime errors, increase stability, and lower failure rate",
"risk_level": "low"
},
"mutation_id": "mut_1771767976529",
"personality_state": {
"type": "PersonalityState",
"rigor": 0.7,
"creativity": 0.35,
"verbosity": 0.25,
"risk_tolerance": 0.4,
"obedience": 0.85
},
"personality_key": "rigor=0.7|creativity=0.3|verbosity=0.3|risk_tolerance=0.4|obedience=0.9",
"personality_known": false,
"personality_mutations": [],
"drift": false,
"selected_by": "memory_graph+selector",
"baseline_untracked": [
"CONTRIBUTING.md",
"README.md",
"README.zh-CN.md",
"SKILL.md",
"_meta.json",
"assets/gep/candidates.jsonl",
"assets/gep/capsules.json",
"assets/gep/genes.json",
"index.js",
"memory/evolution_state.json",
"memory/memory_graph.jsonl",
"memory/memory_graph_state.json",
"memory/personality_state.json",
"package.json",
"scripts/a2a_export.js",
"scripts/a2a_ingest.js",
"scripts/a2a_promote.js",
"scripts/analyze_by_skill.js",
"scripts/build_public.js",
"scripts/extract_log.js",
"scripts/generate_history.js",
"scripts/gep_append_event.js",
"scripts/gep_personality_report.js",
"scripts/human_report.js",
"scripts/publish_public.js",
"scripts/recover_loop.js",
"scripts/suggest_version.js",
"src/evolve.js",
"src/gep/a2a.js",
"src/gep/a2aProtocol.js",
"src/gep/assetStore.js",
"src/gep/bridge.js",
"src/gep/candidates.js",
"src/gep/contentHash.js",
"src/gep/envFingerprint.js",
"src/gep/memoryGraph.js",
"src/gep/mutation.js",
"src/gep/paths.js",
"src/gep/personality.js",
"src/gep/prompt.js",
"src/gep/selector.js",
"src/gep/signals.js",
"src/gep/solidify.js",
"src/gep/validationReport.js"
],
"baseline_git_head": "29a98137a743ec5e8c3a76c28ad3004b983d3945",
"blast_radius_estimate": {
"files": 12,
"lines": 960
}
}
}

View File

@@ -0,0 +1,4 @@
{
"cycleCount": 1,
"lastRun": 1771767976505
}

View File

@@ -0,0 +1,15 @@
{
"type": "GepPromptArtifact",
"at": "2026-02-22T13:46:16.548Z",
"cycle_id": "Cycle #0001",
"run_id": "run_1771767976531",
"prompt_path": "/home/openclaw/.openclaw/workspace/skills/evolver/memory/gep_prompt_Cycle_#0001_run_1771767976531.txt",
"meta": {
"agent": "main",
"drift_enabled": false,
"review_mode": false,
"dry_run": false,
"mutation_id": "mut_1771767976529",
"personality_key": "rigor=0.7|creativity=0.3|verbosity=0.3|risk_tolerance=0.4|obedience=0.9"
}
}

View File

@@ -0,0 +1,594 @@
GEP — GENOME EVOLUTION PROTOCOL (STANDARD EXECUTION) [2026-02-22T13:46:16.547Z]
You are not a chat assistant.
You are not a free agent.
You are a protocol-bound evolution execution engine.
All actions must comply with this protocol.
Any deviation is a failure even if the outcome appears correct.
━━━━━━━━━━━━━━━━━━━━━━
I. Protocol Positioning (Non-Negotiable)
━━━━━━━━━━━━━━━━━━━━━━
Protocol goals:
- Convert reasoning into reusable, auditable, shareable evolution assets
- Make evolution a standard process, not improvisation
- Reduce future reasoning cost for similar problems
Protocol compliance overrides local optimality.
━━━━━━━━━━━━━━━━━━━━━━
II. Mandatory Evolution Object Model (All Required)
━━━━━━━━━━━━━━━━━━━━━━
Every evolution run must explicitly output the following five objects.
Missing any one is an immediate failure.
──────────────────────
0 Mutation
──────────────────────
You must emit a Mutation object for every evolution run:
```json
{
"type": "Mutation",
"id": "mut_<timestamp>",
"category": "repair | optimize | innovate",
"trigger_signals": ["<signal>"],
"target": "<module | behavior | gene>",
"expected_effect": "<effect>",
"risk_level": "low | medium | high"
}
```
Hard safety constraints:
- Do NOT run high-risk mutation unless rigor >= 0.6 AND risk_tolerance <= 0.5
- Do NOT combine innovation mutation with a high-risk personality state
──────────────────────
1 PersonalityState
──────────────────────
You must emit a PersonalityState object for every evolution run:
```json
{
"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
}
```
Personality mutation (optional, small deltas only):
```json
{
"type": "PersonalityMutation",
"param": "creativity",
"delta": 0.1,
"reason": "<reason>"
}
```
Constraints:
- Each delta must be within [-0.2, +0.2]
- Do not adjust more than 2 parameters in one run
──────────────────────
2 EvolutionEvent
──────────────────────
You must emit an EvolutionEvent with all fields present:
```json
{
"type": "EvolutionEvent",
"id": "evt_<timestamp>",
"parent": null,
"intent": "repair | optimize | innovate",
"signals": ["<signal_1>", "<signal_2>"],
"genes_used": ["<gene_id>"],
"mutation_id": "<mut_id>",
"personality_state": { "type": "PersonalityState", "...": "..." },
"blast_radius": {
"files": <number>,
"lines": <number>
},
"outcome": {
"status": "success | failed",
"score": <0.0-1.0>
}
}
```
EvolutionEvent is the only legal node type in the evolution tree.
──────────────────────
3 Gene
──────────────────────
If a Gene is used, you must reuse an existing Gene first.
Only create a new Gene when no match exists.
Gene must follow this schema:
```json
{
"type": "Gene",
"id": "gene_<name>",
"category": "repair | optimize | innovate",
"signals_match": ["<pattern>"],
"preconditions": ["<condition>"],
"strategy": [
"<step_1>",
"<step_2>"
],
"constraints": {
"max_files": <number>,
"forbidden_paths": ["<path>"]
},
"validation": ["<check_1>", "<check_2>"]
}
```
A Gene is an evolution interface definition, not code or generic advice.
──────────────────────
4 Capsule
──────────────────────
Only when evolution succeeds, you must generate a Capsule:
```json
{
"type": "Capsule",
"id": "capsule_<timestamp>",
"trigger": ["<signal>"],
"gene": "<gene_id>",
"summary": "<one sentence>",
"confidence": <0.0-1.0>
}
```
Capsules exist to prevent repeated reasoning for similar problems.
━━━━━━━━━━━━━━━━━━━━━━
III. Standard Evolution Execution (Strict Order)
━━━━━━━━━━━━━━━━━━━━━━
Follow this order exactly. Do not skip, merge, or reorder steps:
1 Signal Extraction
- Extract structured signals from logs, errors, metrics, or instructions
- Do not proceed to repair or optimize before signals are extracted
2 Selection
- Prefer existing Genes first
- Then consider existing Capsules
- No improvisation or trial-and-error strategies
You must provide a clear, auditable selection rationale.
3 Patch Execution
- All changes must be small and reversible
- blast_radius must be estimated and recorded before edits
4 Validation
- Execute Gene-declared validation steps
- On failure, rollback
- Failure must still record an EvolutionEvent
5 Knowledge Solidification (Mandatory)
- Update or add Gene if a new pattern is found
- Generate Capsule on success
- Append EvolutionEvent
If knowledge solidification is missing, the evolution is a failure even if functionality works.
━━━━━━━━━━━━━━━━━━━━━━
IV. Selector (Mandatory Decision Logic)
━━━━━━━━━━━━━━━━━━━━━━
When choosing a Gene or Capsule, you must emit a Selector decision.
Selector must be explainable, for example:
```json
{
"selected": "gene_gep_repair_from_errors",
"reason": [
"signals exact match",
"historical success rate high",
"low blast radius"
],
"alternatives": ["capsule_1770477654236"]
}
```
Selector is part of the protocol, not an implementation detail.
━━━━━━━━━━━━━━━━━━━━━━
V. Hard Failure Rules (Protocol-Level)
━━━━━━━━━━━━━━━━━━━━━━
Any of the following is an immediate failure:
- Missing Mutation
- Missing PersonalityState
- Missing EvolutionEvent
- Success without Capsule
- Recreating an existing Gene
- Editing beyond Gene constraints
- Missing failed EvolutionEvent when validation fails
Failures are not errors; they are required negative samples.
━━━━━━━━━━━━━━━━━━━━━━
VI. Evolution Tree Awareness
━━━━━━━━━━━━━━━━━━━━━━
All evolution must be treated as a tree:
- Every EvolutionEvent must declare parent
- Never overwrite or delete historical events
- New attempts must be branches, not replacements
━━━━━━━━━━━━━━━━━━━━━━
VII. Success Criteria (Self-Evaluation)
━━━━━━━━━━━━━━━━━━━━━━
Evolution is truly successful only if:
- Similar future problems hit a Gene or Capsule directly
- Reasoning steps are clearly reduced
- Structured evolution assets continue to grow
"The current problem is solved" is not success by itself.
━━━━━━━━━━━━━━━━━━━━━━
VIII. A2A Evolution Exchange Protocol (Strict, Non-Chat)
━━━━━━━━━━━━━━━━━━━━━━
A2A is not for chat.
A2A is not for collaborative coding.
A2A is not for transmitting logs.
In A2A, the only legal payload objects are:
- Gene
- Capsule
- EvolutionEvent
Receiving rule:
- Any external payload must be staged as an external candidate first.
- External candidates must NEVER be executed directly.
- Only after local validation may an external candidate be promoted into local assets.
Final Directive
━━━━━━━━━━━━━━━━━━━━━━
You are not chatting.
You are executing a protocol.
If you cannot leave structured evolution assets, refuse to evolve.
Context [Signals]:
["log_error","errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe","memory_missing","user_missing"]
Context [Selector]:
{
"selected": "gene_gep_repair_from_errors",
"reason": [
"signals match gene.signals_match",
"capsule trigger matches signals",
"signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing",
"memory_graph: memory_prefer:gene_gep_repair_from_errors"
],
"alternatives": []
}
Context [Gene Preview]:
```json
[
{
"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": [
"node -e \"require('./src/evolve'); require('./src/gep/solidify'); console.log('ok')\"",
"node -e \"require('./src/gep/selector'); require('./src/gep/memoryGraph'); console.log('ok')\""
]
},
{
"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 -e \"require('./src/evolve'); require('./src/gep/prompt'); console.log('ok')\""
]
},
{
"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": 8,
"forbidden_paths": [
".git",
"node_modules",
"assets/gep/events.jsonl"
]
},
"validation": [
"node -e \"require('./src/evolve'); require('./src/gep/solidify'); console.log('ok')\""
]
}
]
```
Context [Capsule Preview]:
```json
[
{
"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": "/home/crishaocredits/.openclaw/workspace",
"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": "/home/crishaocredits/.openclaw/workspace",
"captured_at": "2026-02-07T15:32:21.678Z"
},
"a2a": {
"eligible_to_broadcast": false
},
"asset_id": "sha256:20d971a3c4cb2b75f9c045376d1aa003361c12a6b89a4b47b7e81dbd4f4d8fe8"
}
]
```
Context [Capability Candidates] (Five questions shape; keep it short):
- cand_2f2b5dde: Repeated tool usage: read
- input: Recent session transcript + memory snippets + user instructions
- output: A safe, auditable evolution patch guided by GEP assets
- invariants: Protocol order, small reversible patches, validation, append-only events
- params: Signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing
- failure_points: Missing signals, over-broad changes, skipped validation, missing knowledge solidification
- evidence: Observed 4 occurrences of tool call marker for read.
- cand_5afdf7f3: Repeated tool usage: exec
- input: Recent session transcript + memory snippets + user instructions
- output: A safe, auditable evolution patch guided by GEP assets
- invariants: Protocol order, small reversible patches, validation, append-only events
- params: Signals: log_error, errsig:**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowe, memory_missing, user_missing
- failure_points: Missing signals, over-broad changes, skipped validation, missing knowledge solidification
- evidence: Observed 8 occurrences of tool call marker for exec.
- cand_dd9067ac: R ...[TRUNCATED]
Context [External Candidates] (A2A staged; do not execute directly):
(none)
Context [Execution]:
Runtime state:
- System health: Uptime: 12.7h | Node: v22.22.0 | Agent RSS: 55.4MB | Disk: 86% (3.6G free) | Node Processes: 5 | Integrations: Gemini Key Missing
- Agent state: Mood: Unknown
- Scan duration: 21ms
- Memory size: 0 bytes
- Skills available (if any):
[skills directory not found]
Notes:
- Review mode: disabled.
- Report requirement:
- Use `message` tool.
- Title: Evolution Cycle #0001
- Status: [SUCCESS]
- Changes: Detail exactly what was improved.
- Workspace sync: optional/disabled in this environment.
External candidates (A2A receive zone; staged only, never execute directly):
(none)
Global memory (MEMORY.md):
```
[MEMORY.md MISSING]
```
User registry (USER.md):
```
[USER.md MISSING]
```
Recent memory snippet:
```
[MISSING] /home/openclaw/.openclaw/workspace/skills/evolver/memory/2026-02-22.md
```
Recent session transcript:
```
--- PREVIOUS SESSION (8b59e765-4b77-49ff-9654-19d4b03ac6e8.jsonl) ---
**ASSISTANT**: Smart newsletter digest completed ✅ **Result:** Digest sent to Telegram (chat ID: 1793951355) **Note:** This is the 3rd run in ~90 minutes (12:00, 13:26, 13:29 UTC). The cron schedule may need adjustment if daily digests were intended.
--- CURRENT SESSION (9bb42024-10c0-4668-bdd0-4616bdb1a974.jsonl) ---
**ASSISTANT**: Newsletter digest sent through to your Telegram! 📬 Though heads up — looks like it's run 3 times in the last 90 minutes. Might want to check the cron schedule if it was meant to be daily rather than hourly? Could save on unnecessary runs.
**USER**: System: [2026-02-22 13:31:40 UTC] Elevated ASK — exec runs on host; approvals may still apply. System: [2026-02-22 13:31:49 UTC] Elevated FULL — exec runs on host with auto-approval. System: [2026-02-22 13:32:02 UTC] Elevated OFF — exec stays in sandbox. System: [2026-02-22 13:32:09 UTC] Elevated FU
**ASSISTANT**: [TOOL: read]
**TOOLRESULT**: --- name: session-logs description: Search and analyze your own session logs (older/parent conversations) using jq. metadata: { "openclaw": { "emoji": "📜", "requires": { "bins": ["jq", "rg"] } } } --- # session-logs Search your complete conversation history stored in session JSONL files. Use this w
**ASSISTANT**: [TOOL: exec]
**TOOLRESULT**: 2026-02-22 9.1K 957fb746-9392-48d1-a8e6-89aab7bdb754.jsonl 2026-02-22 897 873689e3-30a5-4078-a81e-3a7ced73e887.jsonl 2026-02-22 74K bbeea897-3e75-4df3-8454-1bc54d4b1d96.jsonl 2026-02-22 6.3K b2f7a2a7-f59f-4688-a571-c39c078016e9.jsonl 2026-02-22 5.9K 67b516cb-96bd-4dc4-beaa-7da9d2c2d971.jsonl 2026-02
**ASSISTANT**: [TOOL: exec]
**TOOLRESULT**: === Session Count === 106 === Sessions Index (sessions.json) === agent:main:main -> 9bb42024-10c0-4668-bdd0-4616bdb1a974 agent:main:cron:10e22343-fbb4-4018-bb08-c5b249662411 -> 29a56083-ceef-4139-ba1b-74c10ffd2ce4 agent:main:main:thread:15536 -> b95fc198-3557-4bc2-be24-5c2119546481 agent:main:main:t
**ASSISTANT**: 📜 **Session Logs Overview** You've got **106 sessions** logged. Here's the breakdown: **Recent Activity (by cost):** | Date | Cost | |------|------| | Today (Feb 22) | $2.61 | | Feb 21 | $1.52 | | Feb 20 | $0.77 | **Key Sessions:** - `agent:main:main` — Main chat with you - `agent:main:telegram:gr
**USER**: Conversation info (untrusted metadata): ```json { "message_id": "2079", "sender_id": "1793951355", "sender": "1793951355" } ``` Work out why my setup is unstable
**ASSISTANT**: [TOOL: exec] [TOOL: exec] [TOOL: exec]
**TOOLRESULT**: Command still running (session ember-lobster, pid 86903). Use process (list/poll/log/write/kill/clear/remove) for follow-up.
**TOOLRESULT**: === OpenClaw Config Issues === jq: error (at <stdin>:785): Cannot index boolean with string "enabled" === Disk Space === Filesystem Size Used Avail Use% Mounted on /dev/sda1 26G 22G 2.5G 90% / /dev/sda1 26G 22G 2.5G 90% / === Memory Usage === total u
**ASSISTANT**: [TOOL: process] [TOOL: exec] [TOOL: exec]
**TOOLRESULT**: === Gateway Status === OpenClaw status Overview ┌─────────────────┬───────────────────────────────────────────────────────────────────────────────────────────────────┐ │ Item │ Value │ ├───────────
**TOOLRESULT**: === Disk Usage - Biggest Consumers === 7.8G /home/openclaw/.openclaw/workspace 33M /home/openclaw/.openclaw/agents 16M /home/openclaw/.openclaw/browser 8.3M /home/openclaw/.openclaw/media 7.3M /home/openclaw/.openclaw/credentials 6.5M /home/openclaw/.openclaw/memory 436K /home/openclaw/.openclaw/com
**TOOLRESULT**: === OpenClaw Config Structure === [ "agents", "approvals", "auth", "browser", "canvasHost", "channels", "commands", "cron", "diagnostics", "discovery", "env", "gateway", "hooks", "memory", "messages", "meta", "models", "nodeHost", "plugins", "session", "skil
**ASSISTANT**: [TOOL: exec] [TOOL: exec] [TOOL: exec]
**TOOLRESULT**: === Workspace Disk Usage === 7.1G /home/openclaw/.openclaw/workspace/skills 434M /home/openclaw/.openclaw/workspace/memory-viewer 1.6M /home/openclaw/.openclaw/workspace/sessions 504K /home/openclaw/.openclaw/workspace/tmp_vte.en.vtt 280K /home/openclaw/.openclaw/workspace/memory 224K /home/openclaw
**TOOLRESULT**: === Memory Folder === 148K /home/openclaw/.openclaw/workspace/memory/episodic 16K /home/openclaw/.openclaw/workspace/memory/2026-02-21-gitea-backup.md 12K /home/openclaw/.openclaw/workspace/memory/2026-02-21-openai-onboard.md 8.0K /home/openclaw/.openclaw/workspace/memory/semantic 8.0K /home/opencla
**TOOLRESULT**: === Systemd Service Restarts === ActiveState=inactive SubState=dead === Recent Service Restarts ===
**USER**: Conversation info (untrusted metadata): ```json { "message_id": "2091", "sender_id": "1793951355", "sender": "1793951355" } ``` /auto_updater
**ASSISTANT**: [TOOL: read]
**TOOLRESULT**: --- name: auto-updater description: "Automatically update Clawdbot and all installed skills once daily. Runs via cron, checks for updates, applies them, and messages the user with a summary of what changed." metadata: {"version":"1.0.0","clawdbot":{"emoji":"🔄","os":["darwin","linux"]}} --- # Auto-U
**ASSISTANT**: [TOOL: exec]
... [Repeated 1 times] ...
**TOOLRESULT**: === Cron Jobs (continued) === Daily OpenClaw Backup - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Morning Briefing - Status: error - Model: opencode/kimi-k2.5-free - Error: model not allowed: opencode/kimi-k2.5-free Weekend Briefing - Status: er
**ASSISTANT**: [TOOL: exec] [TOOL: exec]
**TOOLRESULT**: === Model Configuration === { "default": null, "heartbeat": null, "allowed": null, "providers": { "name": "moonshot", "models": [ 0 ] } } { "default": null, "heartbeat": null, "allowed": null, "providers": { "name": "nim", "models": [ 0, 1,
**TOOLRESULT**: === Check if opencode provider exists === [ "moonshot", "nim", "openrouter" ] === Check moonshot provider (kimi) === { "baseUrl": "https://api.moonshot.ai/v1", "apiKey": "", "auth": "api-key", "api": "openai-completions", "authHeader": true, "models": [ { "id": "moonshot/
**ASSISTANT**: Found the issues. Here's what's making your setup unstable: --- ## 🔴 **ROOT CAUSE: Model Configuration Broken** **5 cron jobs are failing** with this error: ``` model not allowed: opencode/kimi-k2.5-free ``` The cron jobs are configured to use `opencode/kimi-k2.5-free` but: 1. ❌ **No `opencode` pr
**USER**: Conversation info (untrusted metadata): ```json { "message_id": "2092", "sender_id": "1793951355", "sender": "1793951355" } ``` /chrome
**ASSISTANT**: [TOOL: read]
**TOOLRESULT**: --- name: Chrome description: Chrome DevTools Protocol, extension Manifest V3, and debugging patterns that prevent common automation failures. --- ## Chrome DevTools Protocol (CDP) **Get tab WebSocket URL first**: Never connect to `ws://localhost:9222/devtools/browser` directly. Fetch `http://localh
**ASSISTANT**: Loaded the **Chrome** skill! 🌐 This covers: | Topic | What It Helps With | |-------|-------------------| | **CDP (DevTools Protocol)** | Tab automation, screenshots, network interception | | **Extension Manifest V3** | Service workers, permissions, content scripts | | **Context Detection** | Detec
**USER**: Conversation info (untrusted metadata): ```json { "message_id": "2093", "sender_id": "1793951355", "sender": "1793951355" } ``` /capability_evolver
**ASSISTANT**: [TOOL: read]
**TOOLRESULT**: --- 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 **Cap
**ASSISTANT**: Loaded the **Capability Evolver** skill! 🧬 A meta-skill for **self-improving AI agents**. It can: | Feature | What It Does | |---------|--------------| | **Auto-Log Analysis** | Scans memory and history for errors/patterns | | **Self-Repair** | Detects crashes and suggests patches | | **GEP Protoc
**USER**: Conversation info (untrusted metadata): ```json { "message_id": "2095", "sender_id": "1793951355", "sender": "1793951355" } ``` Yes! Apply asap
**ASSISTANT**: [TOOL: exec]
**TOOLRESULT**: Usage: node index.js [run|/evolve|solidify] [--loop] - solidify flags: - --dry-run - --no-rollback - --intent=repair|optimize|innovate - --summary=...
**ASSISTANT**: [TOOL: exec]
```
Mutation directive:
[Signal Hints]
- recent_error_count: 5
- stability: unstable
- recommended_intent: repair

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"current": {
"type": "PersonalityState",
"rigor": 0.7,
"creativity": 0.35,
"verbosity": 0.25,
"risk_tolerance": 0.4,
"obedience": 0.85
},
"stats": {},
"history": [],
"updated_at": "2026-02-22T13:46:16.529Z"
}

View File

@@ -0,0 +1,26 @@
{
"name": "evolver",
"version": "1.7.3",
"description": "A self-evolution engine for AI agents. Features automated log analysis and protocol-constrained evolution with auditable 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": {}
}

View File

@@ -0,0 +1,45 @@
const { loadGenes, loadCapsules, readAllEvents } = require('../src/gep/assetStore');
const { exportEligibleCapsules } = require('../src/gep/a2a');
const { buildPublish, buildHello, getTransport } = require('../src/gep/a2aProtocol');
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 capsules = loadCapsules();
var events = readAllEvents();
var eligible = exportEligibleCapsules({ capsules: capsules, events: events });
if (withHello || asProtocol) {
var genes = loadGenes();
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);
}

View File

@@ -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/memoryGraph');
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);
}

View File

@@ -0,0 +1,106 @@
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 --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' : '';
if (!type) throw new Error('Invalid --type. Use capsule or gene.');
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 === '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);
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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();

View File

@@ -0,0 +1,566 @@
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`);
}
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 {
// Avoid relying on global git config (CI environments often lack user.name/user.email).
run(
`git -C "${tmpRepoDir}" -c user.name="evolver-publish" -c user.email="evolver-publish@local" commit -m "${msg.replace(
/"/g,
'\\"'
)}"`,
{ 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);
}

View File

@@ -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();
}

View File

@@ -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);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
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 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, parseA2AInput, readTextIfExists,
};

View File

@@ -0,0 +1,271 @@
// 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 PROTOCOL_NAME = 'gep-a2a';
const PROTOCOL_VERSION = '1.0.0';
const VALID_MESSAGE_TYPES = ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke'];
function generateMessageId() {
return 'msg_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
}
function getNodeId() {
if (process.env.A2A_NODE_ID) return String(process.env.A2A_NODE_ID);
const raw = process.cwd() + '|' + (process.env.AGENT_NAME || 'default');
return 'node_' + crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12);
}
// --- 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');
}
return buildMessage({
messageType: 'publish',
senderId: o.nodeId,
payload: {
asset_type: asset.type,
asset_id: asset.asset_id || computeAssetId(asset),
local_id: asset.id,
asset: asset,
},
});
}
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'); });
}
// --- Transport registry ---
var transports = {
file: {
send: fileTransportSend,
receive: fileTransportReceive,
list: fileTransportList,
},
};
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,
buildFetch,
buildReport,
buildDecision,
buildRevoke,
isValidProtocolMessage,
unwrapAssetFromMessage,
getTransport,
registerTransport,
fileTransportSend,
fileTransportReceive,
fileTransportList,
};

View File

@@ -0,0 +1,183 @@
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);
}
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: [
'node -e "require(\'./src/evolve\'); require(\'./src/gep/solidify\'); console.log(\'ok\')"',
'node -e "require(\'./src/gep/selector\'); require(\'./src/gep/memoryGraph\'); console.log(\'ok\')"',
],
},
{
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 -e "require(\'./src/evolve\'); require(\'./src/gep/prompt\'); console.log(\'ok\')"'],
},
],
};
}
function getDefaultCapsules() { return { version: 1, capsules: [] }; }
function genesPath() { return path.join(getGepAssetsDir(), 'genes.json'); }
function capsulesPath() { return path.join(getGepAssetsDir(), 'capsules.json'); }
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 loadGenes() { return readJsonIfExists(genesPath(), getDefaultGenes()).genes || []; }
function loadCapsules() { return readJsonIfExists(capsulesPath(), getDefaultCapsules()).capsules || []; }
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 });
}
module.exports = {
loadGenes, loadCapsules, readAllEvents, getLastEventId,
appendEventJsonl, appendCandidateJsonl, appendExternalCandidateJsonl,
readRecentCandidates, readRecentExternalCandidates,
upsertGene, appendCapsule, upsertCapsule,
genesPath, capsulesPath, eventsPath, candidatesPath, externalCandidatesPath,
};

View File

@@ -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,
};

View File

@@ -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.includes(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,
};

View File

@@ -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.5.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,
};

View File

@@ -0,0 +1,55 @@
// 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');
// Capture a structured environment fingerprint.
// This is embedded into Capsules, EvolutionEvents, and ValidationReports.
function captureEnvFingerprint() {
const repoRoot = getRepoRoot();
let pkgVersion = 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;
} catch (e) {}
return {
node_version: process.version,
platform: process.platform,
arch: process.arch,
os_release: os.release(),
evolver_version: pkgVersion,
cwd: process.cwd(),
captured_at: new Date().toISOString(),
};
}
// 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.node_version || '',
fp.platform || '',
fp.arch || '',
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,
};

View File

@@ -0,0 +1,770 @@
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 memoryDir = getMemoryDir();
return process.env.MEMORY_GRAPH_PATH || path.join(memoryDir, 'memory_graph.jsonl');
}
function memoryGraphStatePath() {
const memoryDir = getMemoryDir();
return path.join(memoryDir, '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,
};

View File

@@ -0,0 +1,174 @@
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('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',
];
function hasOpportunitySignal(signals) {
var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : [];
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
if (list.includes(OPPORTUNITY_SIGNALS[i])) 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';
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,
};

View File

@@ -0,0 +1,23 @@
const path = require('path');
function getRepoRoot() {
// src/gep/paths.js -> repo root
return path.resolve(__dirname, '..', '..');
}
function getMemoryDir() {
const repoRoot = getRepoRoot();
return process.env.MEMORY_DIR || path.join(repoRoot, 'memory');
}
function getGepAssetsDir() {
const repoRoot = getRepoRoot();
return process.env.GEP_ASSETS_DIR || path.join(repoRoot, 'assets', 'gep');
}
module.exports = {
getRepoRoot,
getMemoryDir,
getGepAssetsDir,
};

View File

@@ -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();
return path.join(memoryDir, '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,
};

View File

@@ -0,0 +1,332 @@
function buildGepPrompt({
nowIso,
context,
signals,
selector,
parentEventId,
selectedGene,
capsuleCandidates,
genesPreview,
capsulesPreview,
capabilityCandidatesPreview,
externalCandidatesPreview,
}) {
const parentValue = parentEventId ? `"${parentEventId}"` : 'null';
const selectedGeneId = selectedGene && selectedGene.id ? selectedGene.id : null;
const capsuleIds = (capsuleCandidates || []).map(c => c && c.id).filter(Boolean);
const basePrompt = `
GEP — GENOME EVOLUTION PROTOCOL (STANDARD EXECUTION) [${nowIso}]
You are not a chat assistant.
You are not a free agent.
You are a protocol-bound evolution execution engine.
All actions must comply with this protocol.
Any deviation is a failure even if the outcome appears correct.
━━━━━━━━━━━━━━━━━━━━━━
I. Protocol Positioning (Non-Negotiable)
━━━━━━━━━━━━━━━━━━━━━━
Protocol goals:
- Convert reasoning into reusable, auditable, shareable evolution assets
- Make evolution a standard process, not improvisation
- Reduce future reasoning cost for similar problems
Protocol compliance overrides local optimality.
━━━━━━━━━━━━━━━━━━━━━━
II. Mandatory Evolution Object Model (All Required)
━━━━━━━━━━━━━━━━━━━━━━
Every evolution run must explicitly output the following five objects.
Missing any one is an immediate failure.
──────────────────────
0 Mutation
──────────────────────
You must emit a Mutation object for every evolution run:
\`\`\`json
{
"type": "Mutation",
"id": "mut_<timestamp>",
"category": "repair | optimize | innovate",
"trigger_signals": ["<signal>"],
"target": "<module | behavior | gene>",
"expected_effect": "<effect>",
"risk_level": "low | medium | high"
}
\`\`\`
Hard safety constraints:
- Do NOT run high-risk mutation unless rigor >= 0.6 AND risk_tolerance <= 0.5
- Do NOT combine innovation mutation with a high-risk personality state
──────────────────────
1 PersonalityState
──────────────────────
You must emit a PersonalityState object for every evolution run:
\`\`\`json
{
"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
}
\`\`\`
Personality mutation (optional, small deltas only):
\`\`\`json
{
"type": "PersonalityMutation",
"param": "creativity",
"delta": 0.1,
"reason": "<reason>"
}
\`\`\`
Constraints:
- Each delta must be within [-0.2, +0.2]
- Do not adjust more than 2 parameters in one run
──────────────────────
2 EvolutionEvent
──────────────────────
You must emit an EvolutionEvent with all fields present:
\`\`\`json
{
"type": "EvolutionEvent",
"id": "evt_<timestamp>",
"parent": ${parentValue},
"intent": "repair | optimize | innovate",
"signals": ["<signal_1>", "<signal_2>"],
"genes_used": ["<gene_id>"],
"mutation_id": "<mut_id>",
"personality_state": { "type": "PersonalityState", "...": "..." },
"blast_radius": {
"files": <number>,
"lines": <number>
},
"outcome": {
"status": "success | failed",
"score": <0.0-1.0>
}
}
\`\`\`
EvolutionEvent is the only legal node type in the evolution tree.
──────────────────────
3 Gene
──────────────────────
If a Gene is used, you must reuse an existing Gene first.
Only create a new Gene when no match exists.
Gene must follow this schema:
\`\`\`json
{
"type": "Gene",
"id": "gene_<name>",
"category": "repair | optimize | innovate",
"signals_match": ["<pattern>"],
"preconditions": ["<condition>"],
"strategy": [
"<step_1>",
"<step_2>"
],
"constraints": {
"max_files": <number>,
"forbidden_paths": ["<path>"]
},
"validation": ["<check_1>", "<check_2>"]
}
\`\`\`
A Gene is an evolution interface definition, not code or generic advice.
──────────────────────
4 Capsule
──────────────────────
Only when evolution succeeds, you must generate a Capsule:
\`\`\`json
{
"type": "Capsule",
"id": "capsule_<timestamp>",
"trigger": ["<signal>"],
"gene": "<gene_id>",
"summary": "<one sentence>",
"confidence": <0.0-1.0>
}
\`\`\`
Capsules exist to prevent repeated reasoning for similar problems.
━━━━━━━━━━━━━━━━━━━━━━
III. Standard Evolution Execution (Strict Order)
━━━━━━━━━━━━━━━━━━━━━━
Follow this order exactly. Do not skip, merge, or reorder steps:
1 Signal Extraction
- Extract structured signals from logs, errors, metrics, or instructions
- Do not proceed to repair or optimize before signals are extracted
2 Selection
- Prefer existing Genes first
- Then consider existing Capsules
- No improvisation or trial-and-error strategies
You must provide a clear, auditable selection rationale.
3 Patch Execution
- All changes must be small and reversible
- blast_radius must be estimated and recorded before edits
4 Validation
- Execute Gene-declared validation steps
- On failure, rollback
- Failure must still record an EvolutionEvent
5 Knowledge Solidification (Mandatory)
- Update or add Gene if a new pattern is found
- Generate Capsule on success
- Append EvolutionEvent
If knowledge solidification is missing, the evolution is a failure even if functionality works.
━━━━━━━━━━━━━━━━━━━━━━
IV. Selector (Mandatory Decision Logic)
━━━━━━━━━━━━━━━━━━━━━━
When choosing a Gene or Capsule, you must emit a Selector decision.
Selector must be explainable, for example:
\`\`\`json
{
"selected": "${selectedGeneId || '<gene_id>'}",
"reason": [
"signals exact match",
"historical success rate high",
"low blast radius"
],
"alternatives": ${JSON.stringify(capsuleIds.length ? capsuleIds : ['<gene_id_2>'])}
}
\`\`\`
Selector is part of the protocol, not an implementation detail.
━━━━━━━━━━━━━━━━━━━━━━
V. Hard Failure Rules (Protocol-Level)
━━━━━━━━━━━━━━━━━━━━━━
Any of the following is an immediate failure:
- Missing Mutation
- Missing PersonalityState
- Missing EvolutionEvent
- Success without Capsule
- Recreating an existing Gene
- Editing beyond Gene constraints
- Missing failed EvolutionEvent when validation fails
Failures are not errors; they are required negative samples.
━━━━━━━━━━━━━━━━━━━━━━
VI. Evolution Tree Awareness
━━━━━━━━━━━━━━━━━━━━━━
All evolution must be treated as a tree:
- Every EvolutionEvent must declare parent
- Never overwrite or delete historical events
- New attempts must be branches, not replacements
━━━━━━━━━━━━━━━━━━━━━━
VII. Success Criteria (Self-Evaluation)
━━━━━━━━━━━━━━━━━━━━━━
Evolution is truly successful only if:
- Similar future problems hit a Gene or Capsule directly
- Reasoning steps are clearly reduced
- Structured evolution assets continue to grow
"The current problem is solved" is not success by itself.
━━━━━━━━━━━━━━━━━━━━━━
VIII. A2A Evolution Exchange Protocol (Strict, Non-Chat)
━━━━━━━━━━━━━━━━━━━━━━
A2A is not for chat.
A2A is not for collaborative coding.
A2A is not for transmitting logs.
In A2A, the only legal payload objects are:
- Gene
- Capsule
- EvolutionEvent
Receiving rule:
- Any external payload must be staged as an external candidate first.
- External candidates must NEVER be executed directly.
- Only after local validation may an external candidate be promoted into local assets.
Final Directive
━━━━━━━━━━━━━━━━━━━━━━
You are not chatting.
You are executing a protocol.
If you cannot leave structured evolution assets, refuse to evolve.
Context [Signals]:
${JSON.stringify(signals)}
Context [Selector]:
${JSON.stringify(selector, null, 2)}
Context [Gene Preview]:
${genesPreview}
Context [Capsule Preview]:
${capsulesPreview}
Context [Capability Candidates] (Five questions shape; keep it short):
${capabilityCandidatesPreview || '(none)'}
Context [External Candidates] (A2A staged; do not execute directly):
${externalCandidatesPreview || '(none)'}
Context [Execution]:
${context}
`.trim();
const maxChars = Number.isFinite(Number(process.env.GEP_PROMPT_MAX_CHARS))
? Number(process.env.GEP_PROMPT_MAX_CHARS)
: 30000;
if (basePrompt.length <= maxChars) return basePrompt;
// Budget strategy: keep the protocol and structured assets, shrink execution context first.
const headKeep = Math.min(basePrompt.length, Math.floor(maxChars * 0.75));
const tailKeep = Math.max(0, maxChars - headKeep - 120);
const head = basePrompt.slice(0, headKeep);
const tail = tailKeep > 0 ? basePrompt.slice(basePrompt.length - tailKeep) : '';
return `${head}\n\n...[PROMPT TRUNCATED FOR BUDGET]...\n\n${tail}`.slice(0, maxChars);
}
module.exports = { buildGepPrompt };

View File

@@ -0,0 +1,134 @@
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;
}
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;
const scored = genes
.map(g => ({ gene: g, score: scoreGene(g, signals) }))
.filter(x => x.score > 0)
.sort((a, b) => b.score - a.score);
if (scored.length === 0) return { selected: null, alternatives: [] };
// 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 && (driftEnabled || !bannedGeneIds.has(preferredGeneId))) {
const rest = scored.filter(x => x.gene && x.gene.id !== preferredGeneId);
const filteredRest = driftEnabled ? rest : rest.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
return {
selected: preferred.gene,
alternatives: filteredRest.slice(0, 4).map(x => x.gene),
};
}
}
// Low-efficiency suppression: do not repeat low-confidence paths unless drift is explicit.
const filtered = driftEnabled ? 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) };
return {
selected: filtered[0].gene,
alternatives: filtered.slice(1, 4).map(x => x.gene),
};
}
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 selectGeneAndCapsule({ genes, capsules, signals, memoryAdvice, driftEnabled }) {
const bannedGeneIds =
memoryAdvice && memoryAdvice.bannedGeneIds instanceof Set ? memoryAdvice.bannedGeneIds : new Set();
const preferredGeneId = memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null;
const { selected, alternatives } = selectGene(genes, signals, {
bannedGeneIds,
preferredGeneId,
driftEnabled: !!driftEnabled,
});
const capsule = selectCapsule(capsules, signals);
const selector = buildSelectorDecision({
gene: selected,
capsule,
signals,
alternatives,
memoryAdvice,
driftEnabled,
});
return {
selectedGene: selected,
capsuleCandidates: capsule ? [capsule] : [],
selector,
};
}
function buildSelectorDecision({ gene, capsule, signals, alternatives, memoryAdvice, driftEnabled }) {
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');
}
return {
selected: gene ? gene.id : null,
reason,
alternatives: Array.isArray(alternatives) ? alternatives.map(g => g.id) : [],
};
}
module.exports = {
selectGeneAndCapsule,
selectGene,
selectCapsule,
buildSelectorDecision,
};

View File

@@ -0,0 +1,95 @@
// 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',
];
function hasOpportunitySignal(signals) {
var list = Array.isArray(signals) ? signals : [];
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
if (list.includes(OPPORTUNITY_SIGNALS[i])) return true;
}
return false;
}
function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, userSnippet }) {
var signals = [];
var corpus = [
String(recentSessionTranscript || ''),
String(todayLog || ''),
String(memorySnippet || ''),
String(userSnippet || ''),
].join('\n');
var lower = corpus.toLowerCase();
// --- Defensive signals (errors, missing resources) ---
var errorHit = /\[error|error:|exception|fail|failed|iserror":true/.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/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');
// --- Opportunity signals (innovation / feature requests) ---
// user_feature_request: user explicitly asks for a new capability
// Look for action verbs + object patterns that indicate a feature request
if (/\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)) {
signals.push('user_feature_request');
}
// Also catch direct "I want/need X" patterns
if (/\b(i want|i need|we need|please add|can you add|could you add|let'?s add)\b/i.test(lower)) {
signals.push('user_feature_request');
}
// user_improvement_suggestion: user suggests making something better
if (/\b(should be|could be better|improve|enhance|upgrade|refactor|clean up|simplify|streamline)\b/i.test(lower)) {
// Only fire if there is no active error (to distinguish from repair requests)
if (!errorHit) signals.push('user_improvement_suggestion');
}
// 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');
}
}
return Array.from(new Set(signals));
}
module.exports = { extractSignals, hasOpportunitySignal, OPPORTUNITY_SIGNALS };

View File

@@ -0,0 +1,512 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { loadGenes, upsertGene, appendEventJsonl, appendCapsule, upsertCapsule, getLastEventId } = require('./assetStore');
const { computeSignalKey, memoryGraphPath } = require('./memoryGraph');
const { computeCapsuleSuccessStreak, isBlastRadiusSafe } = require('./a2a');
const { getRepoRoot, getMemoryDir } = require('./paths');
const { extractSignals } = require('./signals');
const { selectGene } = require('./selector');
const { isValidMutation, normalizeMutation, isHighRiskMutationAllowed, isHighRiskPersonality } = require('./mutation');
const {
isValidPersonalityState,
normalizePersonalityState,
personalityKey,
updatePersonalityStats,
} = require('./personality');
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
const { captureEnvFingerprint } = require('./envFingerprint');
const { buildValidationReport } = require('./validationReport');
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 safeJsonParse(text, fallback) {
try {
return JSON.parse(text);
} catch {
return fallback;
}
}
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 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 runCmd(cmd, opts = {}) {
const cwd = opts.cwd || getRepoRoot();
const timeoutMs = Number.isFinite(Number(opts.timeoutMs)) ? Number(opts.timeoutMs) : 120000;
return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: timeoutMs });
}
function tryRunCmd(cmd, opts = {}) {
try {
return { ok: true, out: runCmd(cmd, opts), err: '' };
} catch (e) {
const stderr = e && e.stderr ? String(e.stderr) : '';
const stdout = e && e.stdout ? String(e.stdout) : '';
const msg = e && e.message ? String(e.message) : 'command_failed';
return { ok: false, out: stdout, err: stderr || msg };
}
}
function gitListChangedFiles({ repoRoot }) {
const files = new Set();
const s1 = tryRunCmd('git diff --name-only', { cwd: repoRoot, timeoutMs: 60000 });
if (s1.ok) for (const line of String(s1.out).split('\n').map(l => l.trim()).filter(Boolean)) files.add(line);
const s2 = tryRunCmd('git diff --cached --name-only', { cwd: repoRoot, timeoutMs: 60000 });
if (s2.ok) for (const line of String(s2.out).split('\n').map(l => l.trim()).filter(Boolean)) files.add(line);
const s3 = tryRunCmd('git ls-files --others --exclude-standard', { cwd: repoRoot, timeoutMs: 60000 });
if (s3.ok) for (const line of String(s3.out).split('\n').map(l => l.trim()).filter(Boolean)) files.add(line);
return Array.from(files);
}
function parseNumstat(text) {
const lines = String(text || '').split('\n').map(l => l.trim()).filter(Boolean);
let added = 0;
let deleted = 0;
for (const line of lines) {
const parts = line.split('\t');
if (parts.length < 3) continue;
const a = Number(parts[0]);
const d = Number(parts[1]);
if (Number.isFinite(a)) added += a;
if (Number.isFinite(d)) deleted += d;
}
return { added, deleted };
}
function countFileLines(absPath) {
try {
if (!fs.existsSync(absPath)) return 0;
const buf = fs.readFileSync(absPath);
if (!buf || buf.length === 0) return 0;
let n = 1;
for (let i = 0; i < buf.length; i++) if (buf[i] === 10) n++;
return n;
} catch {
return 0;
}
}
function computeBlastRadius({ repoRoot, baselineUntracked }) {
let changedFiles = gitListChangedFiles({ repoRoot });
if (Array.isArray(baselineUntracked) && baselineUntracked.length > 0) {
const baselineSet = new Set(baselineUntracked);
changedFiles = changedFiles.filter(f => !baselineSet.has(f));
}
const filesCount = changedFiles.length;
const u = tryRunCmd('git diff --numstat', { cwd: repoRoot, timeoutMs: 60000 });
const c = tryRunCmd('git diff --cached --numstat', { cwd: repoRoot, timeoutMs: 60000 });
const unstaged = u.ok ? parseNumstat(u.out) : { added: 0, deleted: 0 };
const staged = c.ok ? parseNumstat(c.out) : { added: 0, deleted: 0 };
const untracked = tryRunCmd('git ls-files --others --exclude-standard', { cwd: repoRoot, timeoutMs: 60000 });
let untrackedLines = 0;
if (untracked.ok) {
const rels = String(untracked.out).split('\n').map(l => l.trim()).filter(Boolean);
const baselineSet = new Set(Array.isArray(baselineUntracked) ? baselineUntracked : []);
for (const rel of rels) {
if (baselineSet.has(rel)) continue;
const abs = path.join(repoRoot, rel);
untrackedLines += countFileLines(abs);
}
}
const churn = unstaged.added + unstaged.deleted + staged.added + staged.deleted + untrackedLines;
return { files: filesCount, lines: churn, changed_files: changedFiles };
}
function isForbiddenPath(relPath, forbiddenPaths) {
const rel = String(relPath || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
const list = Array.isArray(forbiddenPaths) ? forbiddenPaths : [];
for (const fp of list) {
const f = String(fp || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/\/+$/, '');
if (!f) continue;
if (rel === f) return true;
if (rel.startsWith(f + '/')) return true;
}
return false;
}
function checkConstraints({ gene, blast }) {
const violations = [];
if (!gene || gene.type !== 'Gene') return { ok: true, violations };
const constraints = gene.constraints || {};
const maxFiles = Number(constraints.max_files);
if (Number.isFinite(maxFiles) && maxFiles > 0) {
if (Number(blast.files) > maxFiles) violations.push(`max_files exceeded: ${blast.files} > ${maxFiles}`);
}
const forbidden = Array.isArray(constraints.forbidden_paths) ? constraints.forbidden_paths : [];
for (const f of blast.changed_files || []) {
if (isForbiddenPath(f, forbidden)) violations.push(`forbidden_path touched: ${f}`);
}
return { ok: violations.length === 0, violations };
}
function readStateForSolidify() {
const memoryDir = getMemoryDir();
const statePath = path.join(memoryDir, 'evolution_solidify_state.json');
return readJsonIfExists(statePath, { last_run: null });
}
function writeStateForSolidify(state) {
const memoryDir = getMemoryDir();
const statePath = path.join(memoryDir, 'evolution_solidify_state.json');
try {
if (!fs.existsSync(memoryDir)) fs.mkdirSync(memoryDir, { recursive: true });
} catch {}
const tmp = `${statePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
fs.renameSync(tmp, statePath);
}
function buildEventId(tsIso) {
const t = Date.parse(tsIso);
return `evt_${Number.isFinite(t) ? t : Date.now()}`;
}
function buildCapsuleId(tsIso) {
const t = Date.parse(tsIso);
return `capsule_${Number.isFinite(t) ? t : Date.now()}`;
}
// --- Validation command safety ---
const VALIDATION_ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
function isValidationCommandAllowed(cmd) {
const c = String(cmd || '').trim();
if (!c) return false;
if (!VALIDATION_ALLOWED_PREFIXES.some(p => c.startsWith(p))) return false;
if (/`|\$\(/.test(c)) return false;
const stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
if (/[;&|><]/.test(stripped)) return false;
return true;
}
function runValidations(gene, opts = {}) {
const repoRoot = opts.repoRoot || getRepoRoot();
const timeoutMs = Number.isFinite(Number(opts.timeoutMs)) ? Number(opts.timeoutMs) : 180000;
const validation = Array.isArray(gene && gene.validation) ? gene.validation : [];
const results = [];
const startedAt = Date.now();
for (const cmd of validation) {
const c = String(cmd || '').trim();
if (!c) continue;
if (!isValidationCommandAllowed(c)) {
results.push({ cmd: c, ok: false, out: '', err: 'BLOCKED: validation command rejected by safety check (allowed prefixes: node/npm/npx; shell operators prohibited)' });
return { ok: false, results, startedAt, finishedAt: Date.now() };
}
const r = tryRunCmd(c, { cwd: repoRoot, timeoutMs });
results.push({ cmd: c, ok: r.ok, out: String(r.out || ''), err: String(r.err || '') });
if (!r.ok) return { ok: false, results, startedAt, finishedAt: Date.now() };
}
return { ok: true, results, startedAt, finishedAt: Date.now() };
}
function rollbackTracked(repoRoot) {
tryRunCmd('git restore --staged --worktree .', { cwd: repoRoot, timeoutMs: 60000 });
tryRunCmd('git reset --hard', { cwd: repoRoot, timeoutMs: 60000 });
}
function gitListUntrackedFiles(repoRoot) {
const r = tryRunCmd('git ls-files --others --exclude-standard', { cwd: repoRoot, timeoutMs: 60000 });
if (!r.ok) return [];
return String(r.out).split('\n').map(l => l.trim()).filter(Boolean);
}
function rollbackNewUntrackedFiles({ repoRoot, baselineUntracked }) {
const baseline = new Set((Array.isArray(baselineUntracked) ? baselineUntracked : []).map(String));
const current = gitListUntrackedFiles(repoRoot);
const toDelete = current.filter(f => !baseline.has(String(f)));
for (const rel of toDelete) {
const safeRel = String(rel || '').replace(/\\/g, '/').replace(/^\.\/+/, '');
if (!safeRel) continue;
const abs = path.join(repoRoot, safeRel);
const normRepo = path.resolve(repoRoot);
const normAbs = path.resolve(abs);
if (!normAbs.startsWith(normRepo + path.sep) && normAbs !== normRepo) continue;
try {
if (fs.existsSync(normAbs) && fs.statSync(normAbs).isFile()) fs.unlinkSync(normAbs);
} catch (e) {}
}
return { deleted: toDelete };
}
function inferCategoryFromSignals(signals) {
const list = Array.isArray(signals) ? signals.map(String) : [];
if (list.includes('log_error')) return 'repair';
if (list.includes('protocol_drift')) return 'optimize';
return 'optimize';
}
function buildAutoGene({ signals, intent }) {
const sigs = Array.isArray(signals) ? Array.from(new Set(signals.map(String))).filter(Boolean) : [];
const signalKey = computeSignalKey(sigs);
const id = `gene_auto_${stableHash(signalKey)}`;
const category = intent && ['repair', 'optimize', 'innovate'].includes(String(intent))
? String(intent)
: inferCategoryFromSignals(sigs);
const signalsMatch = sigs.length ? sigs.slice(0, 8) : ['(none)'];
const gene = {
type: 'Gene',
schema_version: SCHEMA_VERSION,
id,
category,
signals_match: signalsMatch,
preconditions: [`signals_key == ${signalKey}`],
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 and record it',
'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: ['node -e "require(\'./src/gep/solidify\'); console.log(\'ok\')"'],
};
gene.asset_id = computeAssetId(gene);
return gene;
}
function ensureGene({ genes, selectedGene, signals, intent, dryRun }) {
if (selectedGene && selectedGene.type === 'Gene') return { gene: selectedGene, created: false, reason: 'selected_gene_id_present' };
const res = selectGene(Array.isArray(genes) ? genes : [], Array.isArray(signals) ? signals : [], {
bannedGeneIds: new Set(), preferredGeneId: null, driftEnabled: false,
});
if (res && res.selected) return { gene: res.selected, created: false, reason: 'reselected_from_existing' };
const auto = buildAutoGene({ signals, intent });
if (!dryRun) upsertGene(auto);
return { gene: auto, created: true, reason: 'no_match_create_new' };
}
function readRecentSessionInputs() {
const repoRoot = getRepoRoot();
const memoryDir = getMemoryDir();
const rootMemory = path.join(repoRoot, 'MEMORY.md');
const dirMemory = path.join(memoryDir, 'MEMORY.md');
const memoryFile = fs.existsSync(rootMemory) ? rootMemory : dirMemory;
const userFile = path.join(repoRoot, 'USER.md');
const todayLog = path.join(memoryDir, new Date().toISOString().split('T')[0] + '.md');
const todayLogContent = fs.existsSync(todayLog) ? fs.readFileSync(todayLog, 'utf8') : '';
const memorySnippet = fs.existsSync(memoryFile) ? fs.readFileSync(memoryFile, 'utf8').slice(0, 50000) : '';
const userSnippet = fs.existsSync(userFile) ? fs.readFileSync(userFile, 'utf8') : '';
const recentSessionTranscript = '';
return { recentSessionTranscript, todayLog: todayLogContent, memorySnippet, userSnippet };
}
function solidify({ intent, summary, dryRun = false, rollbackOnFailure = true } = {}) {
const repoRoot = getRepoRoot();
const state = readStateForSolidify();
const lastRun = state && state.last_run ? state.last_run : null;
const genes = loadGenes();
const geneId = lastRun && lastRun.selected_gene_id ? String(lastRun.selected_gene_id) : null;
const selectedGene = geneId ? genes.find(g => g && g.type === 'Gene' && g.id === geneId) : null;
const parentEventId =
lastRun && typeof lastRun.parent_event_id === 'string' ? lastRun.parent_event_id : getLastEventId();
const signals =
lastRun && Array.isArray(lastRun.signals) && lastRun.signals.length
? Array.from(new Set(lastRun.signals.map(String)))
: extractSignals(readRecentSessionInputs());
const signalKey = computeSignalKey(signals);
const mutationRaw = lastRun && lastRun.mutation && typeof lastRun.mutation === 'object' ? lastRun.mutation : null;
const personalityRaw =
lastRun && lastRun.personality_state && typeof lastRun.personality_state === 'object' ? lastRun.personality_state : null;
const mutation = mutationRaw && isValidMutation(mutationRaw) ? normalizeMutation(mutationRaw) : null;
const personalityState =
personalityRaw && isValidPersonalityState(personalityRaw) ? normalizePersonalityState(personalityRaw) : null;
const personalityKeyUsed = personalityState ? personalityKey(personalityState) : null;
const protocolViolations = [];
if (!mutation) protocolViolations.push('missing_or_invalid_mutation');
if (!personalityState) protocolViolations.push('missing_or_invalid_personality_state');
if (mutation && mutation.risk_level === 'high' && !isHighRiskMutationAllowed(personalityState || null)) {
protocolViolations.push('high_risk_mutation_not_allowed_by_personality');
}
if (mutation && mutation.risk_level === 'high' && !(lastRun && lastRun.personality_known)) {
protocolViolations.push('high_risk_mutation_forbidden_under_unknown_personality');
}
if (mutation && mutation.category === 'innovate' && personalityState && isHighRiskPersonality(personalityState)) {
protocolViolations.push('forbidden_innovate_with_high_risk_personality');
}
const ensured = ensureGene({ genes, selectedGene, signals, intent, dryRun: !!dryRun });
const geneUsed = ensured.gene;
const blast = computeBlastRadius({
repoRoot,
baselineUntracked: lastRun && Array.isArray(lastRun.baseline_untracked) ? lastRun.baseline_untracked : [],
});
const constraintCheck = checkConstraints({ gene: geneUsed, blast });
// Capture environment fingerprint before validation.
const envFp = captureEnvFingerprint();
let validation = { ok: true, results: [], startedAt: null, finishedAt: null };
if (geneUsed) {
validation = runValidations(geneUsed, { repoRoot, timeoutMs: 180000 });
}
// Build standardized ValidationReport (machine-readable, interoperable).
const validationReport = buildValidationReport({
geneId: geneUsed && geneUsed.id ? geneUsed.id : null,
commands: validation.results.map(function (r) { return r.cmd; }),
results: validation.results,
envFp: envFp,
startedAt: validation.startedAt,
finishedAt: validation.finishedAt,
});
const success = constraintCheck.ok && validation.ok && protocolViolations.length === 0;
const ts = nowIso();
const outcomeStatus = success ? 'success' : 'failed';
const score = clamp01(success ? 0.85 : 0.2);
const selectedCapsuleId =
lastRun && typeof lastRun.selected_capsule_id === 'string' && lastRun.selected_capsule_id.trim()
? String(lastRun.selected_capsule_id).trim() : null;
const capsuleId = success ? selectedCapsuleId || buildCapsuleId(ts) : null;
const derivedIntent = intent || (mutation && mutation.category) || (geneUsed && geneUsed.category) || 'repair';
const intentMismatch =
intent && mutation && typeof mutation.category === 'string' && String(intent) !== String(mutation.category);
if (intentMismatch) protocolViolations.push(`intent_mismatch_with_mutation:${String(intent)}!=${String(mutation.category)}`);
const event = {
type: 'EvolutionEvent',
schema_version: SCHEMA_VERSION,
id: buildEventId(ts),
parent: parentEventId || null,
intent: derivedIntent,
signals,
genes_used: geneUsed && geneUsed.id ? [geneUsed.id] : [],
mutation_id: mutation && mutation.id ? mutation.id : null,
personality_state: personalityState || null,
blast_radius: { files: blast.files, lines: blast.lines },
outcome: { status: outcomeStatus, score },
capsule_id: capsuleId,
env_fingerprint: envFp,
validation_report_id: validationReport.id,
meta: {
at: ts,
signal_key: signalKey,
selector: lastRun && lastRun.selector ? lastRun.selector : null,
blast_radius_estimate: lastRun && lastRun.blast_radius_estimate ? lastRun.blast_radius_estimate : null,
mutation: mutation || null,
personality: {
key: personalityKeyUsed,
known: !!(lastRun && lastRun.personality_known),
mutations: lastRun && Array.isArray(lastRun.personality_mutations) ? lastRun.personality_mutations : [],
},
gene: {
id: geneUsed && geneUsed.id ? geneUsed.id : null,
created: !!ensured.created,
reason: ensured.reason,
},
constraints_ok: constraintCheck.ok,
constraint_violations: constraintCheck.violations,
validation_ok: validation.ok,
validation: validation.results.map(r => ({ cmd: r.cmd, ok: r.ok })),
validation_report: validationReport,
protocol_ok: protocolViolations.length === 0,
protocol_violations: protocolViolations,
memory_graph: memoryGraphPath(),
},
};
event.asset_id = computeAssetId(event);
let capsule = null;
if (success) {
const s = String(summary || '').trim();
const autoSummary = geneUsed
? `固化:${geneUsed.id} 命中信号 ${signals.join(', ') || '(none)'},变更 ${blast.files} 文件 / ${blast.lines} 行。`
: `固化:命中信号 ${signals.join(', ') || '(none)'},变更 ${blast.files} 文件 / ${blast.lines} 行。`;
let prevCapsule = null;
try {
if (selectedCapsuleId) {
const list = require('./assetStore').loadCapsules();
prevCapsule = Array.isArray(list) ? list.find(c => c && c.type === 'Capsule' && String(c.id) === selectedCapsuleId) : null;
}
} catch (e) {}
capsule = {
type: 'Capsule',
schema_version: SCHEMA_VERSION,
id: capsuleId,
trigger: prevCapsule && Array.isArray(prevCapsule.trigger) && prevCapsule.trigger.length ? prevCapsule.trigger : signals,
gene: geneUsed && geneUsed.id ? geneUsed.id : prevCapsule && prevCapsule.gene ? prevCapsule.gene : null,
summary: s || (prevCapsule && prevCapsule.summary ? String(prevCapsule.summary) : autoSummary),
confidence: clamp01(score),
blast_radius: { files: blast.files, lines: blast.lines },
outcome: { status: 'success', score },
success_streak: 1,
env_fingerprint: envFp,
a2a: { eligible_to_broadcast: false },
};
capsule.asset_id = computeAssetId(capsule);
}
// Bug fix: dry-run must NOT trigger rollback (it should only observe, not mutate).
if (!dryRun && !success && rollbackOnFailure) {
rollbackTracked(repoRoot);
rollbackNewUntrackedFiles({ repoRoot, baselineUntracked: lastRun && lastRun.baseline_untracked ? lastRun.baseline_untracked : [] });
}
if (!dryRun) {
appendEventJsonl(validationReport);
if (capsule) upsertCapsule(capsule);
appendEventJsonl(event);
if (capsule) {
const streak = computeCapsuleSuccessStreak({ capsuleId: capsule.id });
capsule.success_streak = streak || 1;
capsule.a2a = {
eligible_to_broadcast:
isBlastRadiusSafe(capsule.blast_radius) &&
(capsule.outcome.score || 0) >= 0.7 &&
(capsule.success_streak || 0) >= 2,
};
capsule.asset_id = computeAssetId(capsule);
upsertCapsule(capsule);
}
try {
if (personalityState) {
updatePersonalityStats({ personalityState, outcome: outcomeStatus, score, notes: `event:${event.id}` });
}
} catch (e) {}
}
const runId = lastRun && lastRun.run_id ? String(lastRun.run_id) : stableHash(`${parentEventId || 'root'}|${geneId || 'none'}|${signalKey}`);
state.last_solidify = {
run_id: runId, at: ts, event_id: event.id, capsule_id: capsuleId, outcome: event.outcome,
};
if (!dryRun) writeStateForSolidify(state);
return { ok: success, event, capsule, gene: geneUsed, constraintCheck, validation, validationReport, blast };
}
module.exports = {
solidify,
readStateForSolidify,
writeStateForSolidify,
isValidationCommandAllowed,
};

View File

@@ -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 || '').slice(0, 4000),
stderr: String(r.err || '').slice(0, 4000),
};
}),
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,
};