AI Newsletter Digest improvements: fixed QP soft line break decoding, URL extraction, and content cleaning
This commit is contained in:
7
skills/capability-evolver/.clawhub/origin.json
Normal file
7
skills/capability-evolver/.clawhub/origin.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "capability-evolver",
|
||||
"installedVersion": "1.14.0",
|
||||
"installedAt": 1772222788791
|
||||
}
|
||||
11
skills/capability-evolver/CONTRIBUTING.md
Normal file
11
skills/capability-evolver/CONTRIBUTING.md
Normal 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.
|
||||
|
||||
323
skills/capability-evolver/README.md
Normal file
323
skills/capability-evolver/README.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# 🧬 Capability Evolver
|
||||
|
||||

|
||||
|
||||
[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**: each evolution run is gated by an explicit Mutation object and an evolvable PersonalityState.
|
||||
- **Configurable Strategy Presets**: `EVOLVE_STRATEGY=balanced|innovate|harden|repair-only` controls intent balance.
|
||||
- **Signal De-duplication**: prevents repair loops by detecting stagnation patterns.
|
||||
- **Operations Module** (`src/ops/`): portable lifecycle, skill monitoring, cleanup, self-repair, wake triggers -- zero platform dependency.
|
||||
- **Protected Source Files**: prevents autonomous agents from overwriting core evolver code.
|
||||
- **One-Command Evolution**: `node index.js` to generate the prompt.
|
||||
|
||||
## Typical Use Cases
|
||||
|
||||
- Harden a flaky agent loop by enforcing validation before edits
|
||||
- Encode recurring fixes as reusable Genes and Capsules
|
||||
- Produce auditable evolution events for review or compliance
|
||||
|
||||
## Anti-Examples
|
||||
|
||||
- Rewriting entire subsystems without signals or constraints
|
||||
- Using the protocol as a generic task runner
|
||||
- Producing changes without recording EvolutionEvent
|
||||
|
||||
## FAQ
|
||||
|
||||
**Does this edit code automatically?**
|
||||
No. It generates a protocol-bound prompt and assets that guide evolution.
|
||||
|
||||
**Do I need to use all GEP assets?**
|
||||
No. You can start with default Genes and extend over time.
|
||||
|
||||
**Is this safe in production?**
|
||||
Use review mode and validation steps. Treat it as a safety-focused evolution tool, not a live patcher.
|
||||
|
||||
## Roadmap
|
||||
|
||||
- Add a one-minute demo workflow
|
||||
- Add a 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
|
||||
```
|
||||
|
||||
### With Strategy Preset
|
||||
```bash
|
||||
EVOLVE_STRATEGY=innovate node index.js --loop # maximize new features
|
||||
EVOLVE_STRATEGY=harden node index.js --loop # focus on stability
|
||||
EVOLVE_STRATEGY=repair-only node index.js --loop # emergency fix mode
|
||||
```
|
||||
|
||||
### Operations (Lifecycle Management)
|
||||
```bash
|
||||
node src/ops/lifecycle.js start # start evolver loop in background
|
||||
node src/ops/lifecycle.js stop # graceful stop (SIGTERM -> SIGKILL)
|
||||
node src/ops/lifecycle.js status # show running state
|
||||
node src/ops/lifecycle.js check # health check + auto-restart if stagnant
|
||||
```
|
||||
|
||||
## Public Release
|
||||
|
||||
This repository is the public distribution.
|
||||
|
||||
- Build public output: `npm run build`
|
||||
- Publish public output: `npm run publish:public`
|
||||
- Dry run: `DRY_RUN=true npm run publish:public`
|
||||
|
||||
Required env vars:
|
||||
|
||||
- `PUBLIC_REMOTE` (default: `public`)
|
||||
- `PUBLIC_REPO` (e.g. `autogame-17/evolver`)
|
||||
- `PUBLIC_OUT_DIR` (default: `dist-public`)
|
||||
- `PUBLIC_USE_BUILD_OUTPUT` (default: `true`)
|
||||
|
||||
Optional env vars:
|
||||
|
||||
- `SOURCE_BRANCH` (default: `main`)
|
||||
- `PUBLIC_BRANCH` (default: `main`)
|
||||
- `RELEASE_TAG` (e.g. `v1.0.41`)
|
||||
- `RELEASE_TITLE` (e.g. `v1.0.41 - GEP protocol`)
|
||||
- `RELEASE_NOTES` or `RELEASE_NOTES_FILE`
|
||||
- `GITHUB_TOKEN` (or `GH_TOKEN` / `GITHUB_PAT`) for GitHub Release creation
|
||||
- `RELEASE_SKIP` (`true` to skip creating a GitHub Release; default is to create)
|
||||
- `RELEASE_USE_GH` (`true` to use `gh` CLI instead of GitHub API)
|
||||
- `PUBLIC_RELEASE_ONLY` (`true` to only create a Release for an existing tag; no publish)
|
||||
|
||||
## Versioning (SemVer)
|
||||
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
- MAJOR: incompatible changes
|
||||
- MINOR: backward-compatible features
|
||||
- PATCH: backward-compatible bug fixes
|
||||
|
||||
## Changelog
|
||||
|
||||
### v1.10.3
|
||||
- **Configurable Blast Radius Policy**: `computeBlastRadius()` now separates runtime artifacts (logs, memory, capsules, events) from functional code. Only code/config files count toward `max_files` constraints. Policy is configurable via `openclaw.json` at `evolver.constraints.countedFilePolicy`.
|
||||
- **Structured Status Output**: `solidify()` now generates a structured status payload (`result`, `en`, `zh`, `meta`) and writes it to a cycle status file, providing downstream reporters with rich evolution context (intent, gene, signals, blast radius, validation results).
|
||||
- **Solidify CLI Observability**: `index.js solidify` prints `[SOLIDIFY_STATUS]` and `[SOLIDIFY_STATUS_FILE]` lines for wrapper integration.
|
||||
|
||||
### v1.10.1
|
||||
- **Innovation Cooldown**: Track recent innovation targets in `analyzeRecentHistory()` and inject `Context [Innovation Cooldown]` into GEP prompt, preventing the Hand Agent from repeatedly innovating on the same skill/module across consecutive cycles.
|
||||
- **Signal Enhancement**: `analyzeRecentHistory()` now returns `recentInnovationTargets` (map of target path to count in last 10 events).
|
||||
|
||||
### v1.10.0
|
||||
- **Operations Module** (`src/ops/`): 6 portable modules extracted from environment-specific wrapper:
|
||||
- `lifecycle.js` -- process start/stop/restart/status/health check
|
||||
- `skills_monitor.js` -- skill health audit with auto-heal (npm install, SKILL.md stub)
|
||||
- `cleanup.js` -- GEP artifact disk cleanup
|
||||
- `trigger.js` -- wake signal mechanism
|
||||
- `commentary.js` -- persona-based cycle commentary
|
||||
- `self_repair.js` -- git emergency repair (abort rebase, remove stale locks)
|
||||
- **Configurable Evolution Strategy** (`EVOLVE_STRATEGY` env var):
|
||||
- 4 presets: `balanced` (default 50/30/20), `innovate` (80/15/5), `harden` (20/40/40), `repair-only` (0/20/80)
|
||||
- Strategy-aware signal filtering with per-preset repair loop thresholds
|
||||
- Backward compatible: `FORCE_INNOVATION=true` maps to `innovate`
|
||||
- **Signal De-duplication**: repair ratio check forces innovation when >= 50% of last 8 cycles are repairs (threshold varies by strategy).
|
||||
- **Tool Usage Analytics**: detects high-frequency tool usage patterns in logs (auto-evolved by Hand Agent).
|
||||
- **Protected Source Files** (GEP Section IX): evolver core .js files listed as immutable to prevent Hand Agent overwrites.
|
||||
- **Forbidden Innovation Zones** (GEP Section X): prevents creation of skills that duplicate existing infrastructure (process management, health monitoring, scheduling).
|
||||
- **Known Issues List** (GEP Section VII.6): tells the LLM to skip already-fixed errors.
|
||||
- **Resilience**: replaced `process.exit(2)` with `throw Error()` for MemoryGraph failures (loop survives transient errors).
|
||||
- **Gene Limits Relaxed**: repair max_files 12->20, innovate max_files 8->25.
|
||||
- `paths.js`: added `getWorkspaceRoot()`, `getSkillsDir()`, `getLogsDir()`.
|
||||
|
||||
### v1.9.2
|
||||
- Intermediate release with strategy presets and protected files.
|
||||
|
||||
### v1.9.1
|
||||
- Signal de-duplication (repair ratio check).
|
||||
- Singleton Guard (PID lock file).
|
||||
- Environment fingerprint in GEP prompt.
|
||||
|
||||
### 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.
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#autogame-17/evolver&Date)
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- Inspired the creation of evomap evolution network.
|
||||
- [lichunr](https://github.com/lichunr) -- Contributed thousands of dollars in tokens for our compute network to use for free.
|
||||
- [shinjiyu](https://github.com/shinjiyu) -- Submitted numerous bug reports for evolver and evomap.
|
||||
- [upbit](https://github.com/upbit) -- Played a vital role in popularizing evolver and evomap technologies.
|
||||
- More contributors to be added.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
264
skills/capability-evolver/README.zh-CN.md
Normal file
264
skills/capability-evolver/README.zh-CN.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 🧬 Capability Evolver(能力进化引擎)
|
||||
|
||||
[English Docs](README.md)
|
||||
|
||||
**“进化不是可选项,而是生存法则。”**
|
||||
|
||||
**Capability Evolver** 是一个元技能(Meta-Skill),赋予 OpenClaw 智能体自我反省的能力。它可以扫描自身的运行日志,识别效率低下或报错的地方,并自主编写代码补丁来优化自身性能。
|
||||
|
||||
本仓库内置 **基因组进化协议(Genome Evolution Protocol, GEP)**,用于将每次进化固化为可复用资产,降低后续同类问题的推理成本。
|
||||
|
||||
## 核心特性
|
||||
|
||||
- **自动日志分析**:自动扫描 `.jsonl` 会话日志,寻找错误模式。
|
||||
- **自我修复**:检测运行时崩溃并编写修复补丁。
|
||||
- **GEP 协议**:标准化进化流程与可复用资产,支持可审计与可共享。
|
||||
- **突变协议与人格进化**:每次进化必须显式声明 Mutation,并维护可进化的 PersonalityState。
|
||||
- **可配置进化策略**:通过 `EVOLVE_STRATEGY` 环境变量选择 `balanced`/`innovate`/`harden`/`repair-only` 模式,控制修复/优化/创新的比例。
|
||||
- **信号去重**:自动检测修复循环,防止反复修同一个问题。
|
||||
- **运维模块** (`src/ops/`):6 个可移植的运维工具(生命周期管理、技能健康监控、磁盘清理、Git 自修复等),零平台依赖。
|
||||
- **源码保护**:防止自治代理覆写核心进化引擎源码。
|
||||
- **动态集成**:自动检测并使用本地工具,如果不存在则回退到通用模式。
|
||||
- **持续循环模式**:持续运行的自我进化循环。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 标准运行(自动化)
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
||||
### 审查模式(人工介入)
|
||||
在应用更改前暂停,等待人工确认。
|
||||
```bash
|
||||
node index.js --review
|
||||
```
|
||||
|
||||
### 持续循环(守护进程)
|
||||
无限循环运行。适合作为后台服务。
|
||||
```bash
|
||||
node index.js --loop
|
||||
```
|
||||
|
||||
### 指定进化策略
|
||||
```bash
|
||||
EVOLVE_STRATEGY=innovate node index.js --loop # 最大化创新
|
||||
EVOLVE_STRATEGY=harden node index.js --loop # 聚焦稳定性
|
||||
EVOLVE_STRATEGY=repair-only node index.js --loop # 紧急修复模式
|
||||
```
|
||||
|
||||
| 策略 | 创新 | 优化 | 修复 | 适用场景 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `balanced`(默认) | 50% | 30% | 20% | 日常运行,稳步成长 |
|
||||
| `innovate` | 80% | 15% | 5% | 系统稳定,快速出新功能 |
|
||||
| `harden` | 20% | 40% | 40% | 大改动后,聚焦稳固 |
|
||||
| `repair-only` | 0% | 20% | 80% | 紧急状态,全力修复 |
|
||||
|
||||
### 运维管理(生命周期)
|
||||
```bash
|
||||
node src/ops/lifecycle.js start # 后台启动进化循环
|
||||
node src/ops/lifecycle.js stop # 优雅停止(SIGTERM -> SIGKILL)
|
||||
node src/ops/lifecycle.js status # 查看运行状态
|
||||
node src/ops/lifecycle.js check # 健康检查 + 停滞自动重启
|
||||
```
|
||||
|
||||
## 典型使用场景
|
||||
|
||||
- 需要审计与可追踪的提示词演进
|
||||
- 团队协作维护 Agent 的长期能力
|
||||
- 希望将修复经验固化为可复用资产
|
||||
|
||||
## 反例
|
||||
|
||||
- 一次性脚本或没有日志的场景
|
||||
- 需要完全自由发挥的改动
|
||||
- 无法接受协议约束的系统
|
||||
|
||||
## GEP 协议(可审计进化)
|
||||
|
||||
本仓库内置基于 GEP 的“协议受限提示词模式”,用于把每次进化固化为可复用资产。
|
||||
|
||||
- **结构化资产目录**:`assets/gep/`
|
||||
- `assets/gep/genes.json`
|
||||
- `assets/gep/capsules.json`
|
||||
- `assets/gep/events.jsonl`
|
||||
- **Selector 选择器**:根据日志提取 signals,优先复用已有 Gene/Capsule,并在提示词中输出可审计的 Selector 决策 JSON。
|
||||
- **约束**:除 🧬 外,禁止使用其他 emoji。
|
||||
|
||||
## 配置与解耦
|
||||
|
||||
本插件能自动适应你的环境。
|
||||
|
||||
| 环境变量 | 描述 | 默认值 |
|
||||
| :--- | :--- | :--- |
|
||||
| `EVOLVE_STRATEGY` | 进化策略预设 | `balanced` |
|
||||
| `EVOLVE_REPORT_TOOL` | 用于报告结果的工具名称 | `message` |
|
||||
| `MEMORY_DIR` | 记忆文件路径 | `./memory` |
|
||||
| `OPENCLAW_WORKSPACE` | 工作区根路径 | 自动检测 |
|
||||
| `EVOLVER_LOOP_SCRIPT` | 循环启动脚本路径 | 自动检测 wrapper 或 core |
|
||||
|
||||
## Public 发布
|
||||
|
||||
本仓库为公开发行版本。
|
||||
|
||||
- 构建公开产物:`npm run build`
|
||||
- 发布公开产物:`npm run publish:public`
|
||||
- 演练:`DRY_RUN=true npm run publish:public`
|
||||
|
||||
必填环境变量:
|
||||
|
||||
- `PUBLIC_REMOTE`(默认:`public`)
|
||||
- `PUBLIC_REPO`(例如 `autogame-17/evolver`)
|
||||
- `PUBLIC_OUT_DIR`(默认:`dist-public`)
|
||||
- `PUBLIC_USE_BUILD_OUTPUT`(默认:`true`)
|
||||
|
||||
可选环境变量:
|
||||
|
||||
- `SOURCE_BRANCH`(默认:`main`)
|
||||
- `PUBLIC_BRANCH`(默认:`main`)
|
||||
- `RELEASE_TAG`(例如 `v1.0.41`)
|
||||
- `RELEASE_TITLE`(例如 `v1.0.41 - GEP protocol`)
|
||||
- `RELEASE_NOTES` 或 `RELEASE_NOTES_FILE`
|
||||
- `GITHUB_TOKEN`(或 `GH_TOKEN` / `GITHUB_PAT`,用于创建 GitHub Release)
|
||||
- `RELEASE_SKIP`(`true` 则跳过创建 GitHub Release;默认会创建)
|
||||
- `RELEASE_USE_GH`(`true` 则使用 `gh` CLI,否则默认走 GitHub API)
|
||||
- `PUBLIC_RELEASE_ONLY`(`true` 则仅为已存在的 tag 创建 Release;不发布代码)
|
||||
|
||||
## 版本号规则(SemVer)
|
||||
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
• MAJOR(主版本):有不兼容变更
|
||||
• MINOR(次版本):向后兼容的新功能
|
||||
• PATCH(修订/补丁):向后兼容的问题修复
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.10.3
|
||||
- **可配置约束口径 (Configurable Blast Radius Policy)**:`computeBlastRadius()` 将运行产物(日志、memory、capsule、events)与功能代码分离。仅代码/配置文件计入 `max_files` 约束。策略可通过 `openclaw.json` 的 `evolver.constraints.countedFilePolicy` 配置。
|
||||
- **结构化状态产出 (Structured Status Output)**:`solidify()` 生成结构化状态载荷(`result`、`en`、`zh`、`meta`),并写入周期状态文件,为下游报告提供丰富的进化上下文(intent、gene、signals、blast radius、validation 结果)。
|
||||
- **Solidify CLI 可观测性**:`index.js solidify` 执行后输出 `[SOLIDIFY_STATUS]` 和 `[SOLIDIFY_STATUS_FILE]`,便于 wrapper 集成。
|
||||
|
||||
### v1.10.1
|
||||
- **创新冷却 (Innovation Cooldown)**:在 `analyzeRecentHistory()` 中追踪近期创新目标,并在 GEP 提示词中注入 `Context [Innovation Cooldown]` 段,防止 Hand Agent 在连续周期中反复对同一技能/模块进行创新。
|
||||
- **信号增强**:`analyzeRecentHistory()` 新增 `recentInnovationTargets` 返回值(目标路径到最近 10 轮出现次数的映射)。
|
||||
|
||||
### v1.10.0
|
||||
- **运维模块** (`src/ops/`):从环境相关的 wrapper 中提取 6 个可移植模块:
|
||||
- `lifecycle.js` -- 进程启停/重启/状态/健康检查
|
||||
- `skills_monitor.js` -- 技能健康审计 + 自动修复(npm install、SKILL.md 生成)
|
||||
- `cleanup.js` -- GEP 产物磁盘清理
|
||||
- `trigger.js` -- 唤醒信号机制
|
||||
- `commentary.js` -- 人格化周期评论
|
||||
- `self_repair.js` -- Git 紧急修复(终止 rebase、清理过期锁文件)
|
||||
- **可配置进化策略** (`EVOLVE_STRATEGY` 环境变量):
|
||||
- 4 个预设:`balanced`(默认 50/30/20)、`innovate`(80/15/5)、`harden`(20/40/40)、`repair-only`(0/20/80)
|
||||
- 策略感知的信号过滤,各预设有独立的修复循环阈值
|
||||
- 向后兼容:`FORCE_INNOVATION=true` 等价于 `innovate`
|
||||
- **信号去重**:当最近 8 轮中修复占比 >= 50% 时强制创新(阈值随策略变化)
|
||||
- **工具使用分析**:检测日志中的高频工具使用模式(由 Hand Agent 自动进化产出)
|
||||
- **源码保护**(GEP Section IX):核心 .js 文件列为不可修改,防止 Hand Agent 覆写
|
||||
- **禁止创新区**(GEP Section X):防止创建与已有基础设施重复的技能(进程管理、健康监控、定时任务等)
|
||||
- **已知问题清单**(GEP Section VII.6):告知 LLM 跳过已修复的错误
|
||||
- **鲁棒性提升**:MemoryGraph 故障时 `process.exit(2)` 改为 `throw Error()`(循环不再因瞬态错误崩溃)
|
||||
- **Gene 限制放宽**:repair max_files 12->20,innovate max_files 8->25
|
||||
- `paths.js` 新增 `getWorkspaceRoot()`、`getSkillsDir()`、`getLogsDir()`
|
||||
|
||||
### v1.9.2
|
||||
- 中间版本,包含策略预设和源码保护机制。
|
||||
|
||||
### v1.9.1
|
||||
- 信号去重(修复比率检查)
|
||||
- 单例锁(PID 锁文件)
|
||||
- GEP 提示词中注入环境指纹
|
||||
|
||||
### 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 Protocol(repair/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 同步)仅在检测到相应插件存在时才会启用。
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#autogame-17/evolver&Date)
|
||||
|
||||
## 鸣谢
|
||||
|
||||
- [onthebigtree](https://github.com/onthebigtree) -- 启发了 evomap 进化网络的诞生。
|
||||
- [lichunr](https://github.com/lichunr) -- 提供了数千美金 Token 供算力网络免费使用。
|
||||
- [shinjiyu](https://github.com/shinjiyu) -- 为 evolver 和 evomap 提交了大量 bug report。
|
||||
- [upbit](https://github.com/upbit) -- 在 evolver 和 evomap 技术的普及中起到了至关重要的作用。
|
||||
- 其余贡献者扩充中。
|
||||
|
||||
## 许可证
|
||||
MIT
|
||||
110
skills/capability-evolver/SKILL.md
Normal file
110
skills/capability-evolver/SKILL.md
Normal file
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: capability-evolver
|
||||
description: A self-evolution engine for AI agents. Analyzes runtime history to identify improvements and applies protocol-constrained evolution.
|
||||
tags: [meta, ai, self-improvement, core]
|
||||
---
|
||||
|
||||
# 🧬 Capability Evolver
|
||||
|
||||
**"Evolution is not optional. Adapt or die."**
|
||||
|
||||
The **Capability Evolver** is a meta-skill that allows OpenClaw agents to inspect their own runtime history, identify failures or inefficiencies, and autonomously write new code or update their own memory to improve performance.
|
||||
|
||||
## Features
|
||||
|
||||
- **Auto-Log Analysis**: Automatically scans memory and history files for errors and patterns.
|
||||
- **Self-Repair**: Detects crashes and suggests patches.
|
||||
- GEP Protocol: Standardized evolution with reusable assets.
|
||||
- **One-Command Evolution**: Just run `/evolve` (or `node index.js`).
|
||||
|
||||
## Usage
|
||||
|
||||
### Standard Run (Automated)
|
||||
Runs the evolution cycle. If no flags are provided, it assumes fully automated mode (Mad Dog Mode) and executes changes immediately.
|
||||
```bash
|
||||
node index.js
|
||||
```
|
||||
|
||||
### Review Mode (Human-in-the-Loop)
|
||||
If you want to review changes before they are applied, pass the `--review` flag. The agent will pause and ask for confirmation.
|
||||
```bash
|
||||
node index.js --review
|
||||
```
|
||||
|
||||
### Mad Dog Mode (Continuous Loop)
|
||||
To run in an infinite loop (e.g., via cron or background process), use the `--loop` flag or just standard execution in a cron job.
|
||||
```bash
|
||||
node index.js --loop
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
| Environment Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `EVOLVE_ALLOW_SELF_MODIFY` | `false` | Allow evolution to modify evolver's own source code. **NOT recommended for production.** Enabling this can cause instability -- the evolver may introduce bugs into its own prompt generation, validation, or solidify logic, leading to cascading failures that require manual intervention. Only enable for controlled experiments. |
|
||||
| `EVOLVE_LOAD_MAX` | `2.0` | Maximum 1-minute load average before evolver backs off. |
|
||||
| `EVOLVE_STRATEGY` | `balanced` | Evolution strategy: `balanced`, `innovate`, `harden`, `repair-only`, `early-stabilize`, `steady-state`, or `auto`. |
|
||||
|
||||
## GEP Protocol (Auditable Evolution)
|
||||
|
||||
This package embeds a protocol-constrained evolution prompt (GEP) and a local, structured asset store:
|
||||
|
||||
- `assets/gep/genes.json`: reusable Gene definitions
|
||||
- `assets/gep/capsules.json`: success capsules to avoid repeating reasoning
|
||||
- `assets/gep/events.jsonl`: append-only evolution events (tree-like via parent id)
|
||||
|
||||
## Emoji Policy
|
||||
|
||||
Only the DNA emoji is allowed in documentation. All other emoji are disallowed.
|
||||
|
||||
## Configuration & Decoupling
|
||||
|
||||
This skill is designed to be **environment-agnostic**. It uses standard OpenClaw tools by default.
|
||||
|
||||
### Local Overrides (Injection)
|
||||
You can inject local preferences (e.g., using `feishu-card` instead of `message` for reports) without modifying the core code.
|
||||
|
||||
**Method 1: Environment Variables**
|
||||
Set `EVOLVE_REPORT_TOOL` in your `.env` file:
|
||||
```bash
|
||||
EVOLVE_REPORT_TOOL=feishu-card
|
||||
```
|
||||
|
||||
**Method 2: Dynamic Detection**
|
||||
The script automatically detects if compatible local skills (like `skills/feishu-card`) exist in your workspace and upgrades its behavior accordingly.
|
||||
|
||||
## Safety & Risk Protocol
|
||||
|
||||
### 1. Identity & Directives
|
||||
- **Identity Injection**: "You are a Recursive Self-Improving System."
|
||||
- **Mutation Directive**:
|
||||
- If **Errors Found** -> **Repair Mode** (Fix bugs).
|
||||
- If **Stable** -> **Forced Optimization** (Refactor/Innovate).
|
||||
|
||||
### 2. Risk Mitigation
|
||||
- **Infinite Recursion**: Strict single-process logic.
|
||||
- **Review Mode**: Use `--review` for sensitive environments.
|
||||
- **Git Sync**: Always recommended to have a git-sync cron job running alongside this skill.
|
||||
|
||||
## Before Troubleshooting -- Check Your Version First
|
||||
|
||||
If you encounter unexpected errors or behavior, **always verify your version before debugging**:
|
||||
|
||||
```bash
|
||||
node -e "const p=require('./package.json'); console.log(p.version)"
|
||||
```
|
||||
|
||||
If you are not on the latest release, update first -- most reported issues are already fixed in newer versions:
|
||||
|
||||
```bash
|
||||
# If installed via git
|
||||
git pull && npm install
|
||||
|
||||
# If installed via npm (global install)
|
||||
npm install -g evolver@latest
|
||||
```
|
||||
|
||||
Latest releases and changelog: `https://github.com/autogame-17/evolver/releases`
|
||||
|
||||
## License
|
||||
MIT
|
||||
6
skills/capability-evolver/_meta.json
Normal file
6
skills/capability-evolver/_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7apafdj4thknczrgxdzfd2v1808svf",
|
||||
"slug": "capability-evolver",
|
||||
"version": "1.14.0",
|
||||
"publishedAt": 1771472983583
|
||||
}
|
||||
6
skills/capability-evolver/assets/gep/candidates.jsonl
Normal file
6
skills/capability-evolver/assets/gep/candidates.jsonl
Normal file
@@ -0,0 +1,6 @@
|
||||
{"type":"CapabilityCandidate","id":"cand_5afdf7f3","title":"Repeated tool usage: exec","source":"transcript","created_at":"2026-03-01T20:00:54.281Z","signals":["repeated_tool_usage:exec"],"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: repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 7 occurrences of tool call marker for exec."}}
|
||||
{"type":"CapabilityCandidate","id":"cand_5afdf7f3","title":"Repeated tool usage: exec","source":"transcript","created_at":"2026-03-02T20:00:49.881Z","signals":["repeated_tool_usage:exec"],"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: repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 5 occurrences of tool call marker for exec."}}
|
||||
{"type":"CapabilityCandidate","id":"cand_2f2b5dde","title":"Repeated tool usage: read","source":"transcript","created_at":"2026-03-03T14:53:48.133Z","signals":["high_tool_usage:exec","repeated_tool_usage:exec"],"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: high_tool_usage:exec, repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 2 occurrences of tool call marker for read."}}
|
||||
{"type":"CapabilityCandidate","id":"cand_5afdf7f3","title":"Repeated tool usage: exec","source":"transcript","created_at":"2026-03-03T14:53:48.133Z","signals":["high_tool_usage:exec","repeated_tool_usage:exec"],"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: high_tool_usage:exec, repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 12 occurrences of tool call marker for exec."}}
|
||||
{"type":"CapabilityCandidate","id":"cand_5afdf7f3","title":"Repeated tool usage: exec","source":"transcript","created_at":"2026-03-03T20:00:51.368Z","signals":["repeated_tool_usage:exec"],"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: repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 6 occurrences of tool call marker for exec."}}
|
||||
{"type":"CapabilityCandidate","id":"cand_5a91a6db","title":"Repeated tool usage: process","source":"transcript","created_at":"2026-03-03T20:00:51.368Z","signals":["repeated_tool_usage:exec"],"shape":{"title":"Repeated tool usage: process","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: repeated_tool_usage:exec","failure_points":"Missing signals, over-broad changes, skipped validation, missing knowledge solidification","evidence":"Observed 2 occurrences of tool call marker for process."}}
|
||||
79
skills/capability-evolver/assets/gep/capsules.json
Normal file
79
skills/capability-evolver/assets/gep/capsules.json
Normal 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": ".",
|
||||
"captured_at": "2026-02-07T15:20:54.155Z"
|
||||
},
|
||||
"a2a": {
|
||||
"eligible_to_broadcast": false
|
||||
},
|
||||
"asset_id": "sha256:3eed0cd5038f9e85fbe0d093890e291e9b8725644c766e6cce40bf62d0f5a2e8"
|
||||
},
|
||||
{
|
||||
"type": "Capsule",
|
||||
"schema_version": "1.5.0",
|
||||
"id": "capsule_1770478341769",
|
||||
"trigger": [
|
||||
"log_error",
|
||||
"errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }",
|
||||
"user_missing",
|
||||
"windows_shell_incompatible",
|
||||
"perf_bottleneck"
|
||||
],
|
||||
"gene": "gene_gep_repair_from_errors",
|
||||
"summary": "固化:gene_gep_repair_from_errors 命中信号 log_error, errsig:**TOOLRESULT**: { \"status\": \"error\", \"tool\": \"exec\", \"error\": \"error: unknown command 'process'\\n\\nCommand exited with code 1\" }, user_missing, windows_shell_incompatible, perf_bottleneck,变更 2 文件 / 44 行。",
|
||||
"confidence": 0.85,
|
||||
"blast_radius": {
|
||||
"files": 2,
|
||||
"lines": 44
|
||||
},
|
||||
"outcome": {
|
||||
"status": "success",
|
||||
"score": 0.85
|
||||
},
|
||||
"success_streak": 1,
|
||||
"env_fingerprint": {
|
||||
"node_version": "v22.22.0",
|
||||
"platform": "linux",
|
||||
"arch": "x64",
|
||||
"os_release": "6.1.0-42-cloud-amd64",
|
||||
"evolver_version": "1.7.0",
|
||||
"cwd": ".",
|
||||
"captured_at": "2026-02-07T15:32:21.678Z"
|
||||
},
|
||||
"a2a": {
|
||||
"eligible_to_broadcast": false
|
||||
},
|
||||
"asset_id": "sha256:20d971a3c4cb2b75f9c045376d1aa003361c12a6b89a4b47b7e81dbd4f4d8fe8"
|
||||
}
|
||||
]
|
||||
}
|
||||
0
skills/capability-evolver/assets/gep/events.jsonl
Normal file
0
skills/capability-evolver/assets/gep/events.jsonl
Normal file
109
skills/capability-evolver/assets/gep/genes.json
Normal file
109
skills/capability-evolver/assets/gep/genes.json
Normal 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": 20,
|
||||
"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": 25,
|
||||
"forbidden_paths": [
|
||||
".git",
|
||||
"node_modules",
|
||||
"assets/gep/events.jsonl"
|
||||
]
|
||||
},
|
||||
"validation": [
|
||||
"node -e \"require('./src/evolve'); require('./src/gep/solidify'); console.log('ok')\""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
0
skills/capability-evolver/assets/gep/genes.jsonl
Normal file
0
skills/capability-evolver/assets/gep/genes.jsonl
Normal file
229
skills/capability-evolver/index.js
Normal file
229
skills/capability-evolver/index.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const evolve = require('./src/evolve');
|
||||
const { solidify } = require('./src/gep/solidify');
|
||||
const path = require('path');
|
||||
// Hardened Env Loading: Ensure .env is loaded before anything else
|
||||
try { require('dotenv').config({ path: path.resolve(__dirname, '../../.env') }); } catch (e) { console.warn('[Evolver] Warning: dotenv not found or failed to load .env'); }
|
||||
const fs = require('fs');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
function sleepMs(ms) {
|
||||
const n = parseInt(String(ms), 10);
|
||||
const t = Number.isFinite(n) ? Math.max(0, n) : 0;
|
||||
return new Promise(resolve => setTimeout(resolve, t));
|
||||
}
|
||||
|
||||
function readJsonSafe(p) {
|
||||
try {
|
||||
if (!fs.existsSync(p)) return null;
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
if (!raw.trim()) return null;
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isPendingSolidify(state) {
|
||||
const lastRun = state && state.last_run ? state.last_run : null;
|
||||
const lastSolid = state && state.last_solidify ? state.last_solidify : null;
|
||||
if (!lastRun || !lastRun.run_id) return false;
|
||||
if (!lastSolid || !lastSolid.run_id) return true;
|
||||
return String(lastSolid.run_id) !== String(lastRun.run_id);
|
||||
}
|
||||
|
||||
function parseMs(v, fallback) {
|
||||
const n = parseInt(String(v == null ? '' : v), 10);
|
||||
if (Number.isFinite(n)) return Math.max(0, n);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// Singleton Guard - prevent multiple evolver daemon instances
|
||||
function acquireLock() {
|
||||
const lockFile = path.join(__dirname, 'evolver.pid');
|
||||
try {
|
||||
if (fs.existsSync(lockFile)) {
|
||||
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
|
||||
try {
|
||||
process.kill(pid, 0); // Check if process exists
|
||||
console.log(`[Singleton] Evolver loop already running (PID ${pid}). Exiting.`);
|
||||
return false;
|
||||
} catch (e) {
|
||||
console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(lockFile, String(process.pid));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('[Singleton] Lock acquisition failed:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function releaseLock() {
|
||||
const lockFile = path.join(__dirname, 'evolver.pid');
|
||||
try {
|
||||
if (fs.existsSync(lockFile)) {
|
||||
const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
|
||||
if (pid === process.pid) fs.unlinkSync(lockFile);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const command = args[0];
|
||||
const isLoop = args.includes('--loop') || args.includes('--mad-dog');
|
||||
|
||||
if (command === 'run' || command === '/evolve' || isLoop) {
|
||||
console.log('Starting capability evolver...');
|
||||
|
||||
if (isLoop) {
|
||||
// Internal daemon loop (no wrapper required).
|
||||
if (!acquireLock()) process.exit(0);
|
||||
process.on('exit', releaseLock);
|
||||
process.on('SIGINT', () => { releaseLock(); process.exit(); });
|
||||
process.on('SIGTERM', () => { releaseLock(); process.exit(); });
|
||||
|
||||
process.env.EVOLVE_LOOP = 'true';
|
||||
process.env.EVOLVE_BRIDGE = 'false';
|
||||
console.log('Loop mode enabled (internal daemon).');
|
||||
|
||||
const solidifyStatePath = path.join(__dirname, 'memory', 'evolution_solidify_state.json');
|
||||
|
||||
const minSleepMs = parseMs(process.env.EVOLVER_MIN_SLEEP_MS, 2000);
|
||||
const maxSleepMs = parseMs(process.env.EVOLVER_MAX_SLEEP_MS, 300000);
|
||||
const idleThresholdMs = parseMs(process.env.EVOLVER_IDLE_THRESHOLD_MS, 500);
|
||||
const pendingSleepMs = parseMs(
|
||||
process.env.EVOLVE_PENDING_SLEEP_MS ||
|
||||
process.env.EVOLVE_MIN_INTERVAL ||
|
||||
process.env.FEISHU_EVOLVER_INTERVAL,
|
||||
120000
|
||||
);
|
||||
|
||||
const maxCyclesPerProcess = parseMs(process.env.EVOLVER_MAX_CYCLES_PER_PROCESS, 100) || 100;
|
||||
const maxRssMb = parseMs(process.env.EVOLVER_MAX_RSS_MB, 500) || 500;
|
||||
const suicideEnabled = String(process.env.EVOLVER_SUICIDE || '').toLowerCase() !== 'false';
|
||||
|
||||
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)})`);
|
||||
releaseLock(); // Release before spawning to allow child to acquire
|
||||
const child = spawn(process.execPath, [__filename, ...args], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: process.env,
|
||||
});
|
||||
child.unref();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Saturation-aware sleep: when the evolver detects it has exhausted innovation
|
||||
// space (consecutive empty cycles), dramatically increase sleep to avoid wasting
|
||||
// resources on no-op cycles. This is the "graceful degradation" mechanism that
|
||||
// Echo-MingXuan lacked -- it kept cycling at full speed after saturation until
|
||||
// load spiked to 1.30 and it crashed.
|
||||
let saturationMultiplier = 1;
|
||||
try {
|
||||
const st1 = readJsonSafe(solidifyStatePath);
|
||||
const lastSignals = st1 && st1.last_run && Array.isArray(st1.last_run.signals) ? st1.last_run.signals : [];
|
||||
if (lastSignals.includes('force_steady_state')) {
|
||||
saturationMultiplier = 10;
|
||||
console.log('[Daemon] Saturation detected. Entering steady-state mode (10x sleep).');
|
||||
} else if (lastSignals.includes('evolution_saturation')) {
|
||||
saturationMultiplier = 5;
|
||||
console.log('[Daemon] Approaching saturation. Reducing evolution frequency (5x sleep).');
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// Jitter to avoid lockstep restarts.
|
||||
const jitter = Math.floor(Math.random() * 250);
|
||||
await sleepMs((currentSleepMs + jitter) * saturationMultiplier);
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
26
skills/capability-evolver/package.json
Normal file
26
skills/capability-evolver/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "evolver",
|
||||
"version": "1.14.0",
|
||||
"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": {}
|
||||
}
|
||||
63
skills/capability-evolver/scripts/a2a_export.js
Normal file
63
skills/capability-evolver/scripts/a2a_export.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const { loadGenes, loadCapsules, readAllEvents } = require('../src/gep/assetStore');
|
||||
const { exportEligibleCapsules, exportEligibleGenes, isAllowedA2AAsset } = require('../src/gep/a2a');
|
||||
const { buildPublish, buildHello, getTransport } = require('../src/gep/a2aProtocol');
|
||||
const { computeAssetId, SCHEMA_VERSION } = require('../src/gep/contentHash');
|
||||
|
||||
function main() {
|
||||
var args = process.argv.slice(2);
|
||||
var asJson = args.includes('--json');
|
||||
var asProtocol = args.includes('--protocol');
|
||||
var withHello = args.includes('--hello');
|
||||
var persist = args.includes('--persist');
|
||||
var includeEvents = args.includes('--include-events');
|
||||
|
||||
var capsules = loadCapsules();
|
||||
var genes = loadGenes();
|
||||
var events = readAllEvents();
|
||||
|
||||
// Build eligible list: Capsules (filtered) + Genes (filtered) + Events (opt-in)
|
||||
var eligibleCapsules = exportEligibleCapsules({ capsules: capsules, events: events });
|
||||
var eligibleGenes = exportEligibleGenes({ genes: genes });
|
||||
var eligible = eligibleCapsules.concat(eligibleGenes);
|
||||
|
||||
if (includeEvents) {
|
||||
var eligibleEvents = (Array.isArray(events) ? events : []).filter(function (e) {
|
||||
return isAllowedA2AAsset(e) && e.type === 'EvolutionEvent';
|
||||
});
|
||||
for (var ei = 0; ei < eligibleEvents.length; ei++) {
|
||||
var ev = eligibleEvents[ei];
|
||||
if (!ev.schema_version) ev.schema_version = SCHEMA_VERSION;
|
||||
if (!ev.asset_id) { try { ev.asset_id = computeAssetId(ev); } catch (e) {} }
|
||||
}
|
||||
eligible = eligible.concat(eligibleEvents);
|
||||
}
|
||||
|
||||
if (withHello || asProtocol) {
|
||||
var hello = buildHello({ geneCount: genes.length, capsuleCount: capsules.length });
|
||||
process.stdout.write(JSON.stringify(hello) + '\n');
|
||||
if (persist) { try { getTransport().send(hello); } catch (e) {} }
|
||||
}
|
||||
|
||||
if (asProtocol) {
|
||||
for (var i = 0; i < eligible.length; i++) {
|
||||
var msg = buildPublish({ asset: eligible[i] });
|
||||
process.stdout.write(JSON.stringify(msg) + '\n');
|
||||
if (persist) { try { getTransport().send(msg); } catch (e) {} }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (asJson) {
|
||||
process.stdout.write(JSON.stringify(eligible, null, 2) + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
for (var j = 0; j < eligible.length; j++) {
|
||||
process.stdout.write(JSON.stringify(eligible[j]) + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
try { main(); } catch (e) {
|
||||
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
79
skills/capability-evolver/scripts/a2a_ingest.js
Normal file
79
skills/capability-evolver/scripts/a2a_ingest.js
Normal 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/memoryGraphAdapter');
|
||||
var contentHash = require('../src/gep/contentHash');
|
||||
var a2aProto = require('../src/gep/a2aProtocol');
|
||||
|
||||
function readStdin() {
|
||||
try { return fs.readFileSync(0, 'utf8'); } catch (e) { return ''; }
|
||||
}
|
||||
|
||||
function parseSignalsFromEnv() {
|
||||
var raw = process.env.A2A_SIGNALS || '';
|
||||
if (!raw) return [];
|
||||
try {
|
||||
var maybe = JSON.parse(raw);
|
||||
if (Array.isArray(maybe)) return maybe.map(String).filter(Boolean);
|
||||
} catch (e) {}
|
||||
return String(raw).split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
||||
}
|
||||
|
||||
function main() {
|
||||
var args = process.argv.slice(2);
|
||||
var inputPath = '';
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (args[i] && !args[i].startsWith('--')) { inputPath = args[i]; break; }
|
||||
}
|
||||
var source = process.env.A2A_SOURCE || 'external';
|
||||
var factor = Number.isFinite(Number(process.env.A2A_EXTERNAL_CONFIDENCE_FACTOR))
|
||||
? Number(process.env.A2A_EXTERNAL_CONFIDENCE_FACTOR) : 0.6;
|
||||
|
||||
var text = inputPath ? a2a.readTextIfExists(inputPath) : readStdin();
|
||||
var parsed = a2a.parseA2AInput(text);
|
||||
var signals = parseSignalsFromEnv();
|
||||
|
||||
var accepted = 0;
|
||||
var rejected = 0;
|
||||
var emitDecisions = process.env.A2A_EMIT_DECISIONS === 'true';
|
||||
|
||||
for (var j = 0; j < parsed.length; j++) {
|
||||
var obj = parsed[j];
|
||||
if (!a2a.isAllowedA2AAsset(obj)) continue;
|
||||
|
||||
if (obj.asset_id && typeof obj.asset_id === 'string') {
|
||||
if (!contentHash.verifyAssetId(obj)) {
|
||||
rejected += 1;
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dm = a2aProto.buildDecision({ assetId: obj.asset_id, localId: obj.id, decision: 'reject', reason: 'asset_id integrity check failed' });
|
||||
a2aProto.getTransport().send(dm);
|
||||
} catch (e) {}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var staged = a2a.lowerConfidence(obj, { source: source, factor: factor });
|
||||
if (!staged) continue;
|
||||
|
||||
assetStore.appendExternalCandidateJsonl(staged);
|
||||
try { memGraph.recordExternalCandidate({ asset: staged, source: source, signals: signals }); } catch (e) {}
|
||||
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dm2 = a2aProto.buildDecision({ assetId: staged.asset_id, localId: staged.id, decision: 'quarantine', reason: 'staged as external candidate' });
|
||||
a2aProto.getTransport().send(dm2);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
accepted += 1;
|
||||
}
|
||||
|
||||
process.stdout.write('accepted=' + accepted + ' rejected=' + rejected + '\n');
|
||||
}
|
||||
|
||||
try { main(); } catch (e) {
|
||||
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
118
skills/capability-evolver/scripts/a2a_promote.js
Normal file
118
skills/capability-evolver/scripts/a2a_promote.js
Normal file
@@ -0,0 +1,118 @@
|
||||
var assetStore = require('../src/gep/assetStore');
|
||||
var solidifyMod = require('../src/gep/solidify');
|
||||
var contentHash = require('../src/gep/contentHash');
|
||||
var a2aProto = require('../src/gep/a2aProtocol');
|
||||
|
||||
function parseArgs(argv) {
|
||||
var out = { flags: new Set(), kv: new Map(), positionals: [] };
|
||||
for (var i = 0; i < argv.length; i++) {
|
||||
var a = argv[i];
|
||||
if (!a) continue;
|
||||
if (a.startsWith('--')) {
|
||||
var eq = a.indexOf('=');
|
||||
if (eq > -1) { out.kv.set(a.slice(2, eq), a.slice(eq + 1)); }
|
||||
else {
|
||||
var key = a.slice(2);
|
||||
var next = argv[i + 1];
|
||||
if (next && !String(next).startsWith('--')) { out.kv.set(key, next); i++; }
|
||||
else { out.flags.add(key); }
|
||||
}
|
||||
} else { out.positionals.push(a); }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function main() {
|
||||
var args = parseArgs(process.argv.slice(2));
|
||||
var id = String(args.kv.get('id') || '').trim();
|
||||
var typeRaw = String(args.kv.get('type') || '').trim().toLowerCase();
|
||||
var validated = args.flags.has('validated') || String(args.kv.get('validated') || '') === 'true';
|
||||
var limit = Number.isFinite(Number(args.kv.get('limit'))) ? Number(args.kv.get('limit')) : 500;
|
||||
|
||||
if (!id || !typeRaw) throw new Error('Usage: node scripts/a2a_promote.js --type capsule|gene|event --id <id> --validated');
|
||||
if (!validated) throw new Error('Refusing to promote without --validated (local verification must be done first).');
|
||||
|
||||
var type = typeRaw === 'capsule' ? 'Capsule' : typeRaw === 'gene' ? 'Gene' : typeRaw === 'event' ? 'EvolutionEvent' : '';
|
||||
if (!type) throw new Error('Invalid --type. Use capsule, gene, or event.');
|
||||
|
||||
var external = assetStore.readRecentExternalCandidates(limit);
|
||||
var candidate = null;
|
||||
for (var i = 0; i < external.length; i++) {
|
||||
if (external[i] && external[i].type === type && String(external[i].id) === id) { candidate = external[i]; break; }
|
||||
}
|
||||
if (!candidate) throw new Error('Candidate not found in external zone: type=' + type + ' id=' + id);
|
||||
|
||||
if (type === 'Gene') {
|
||||
var validation = Array.isArray(candidate.validation) ? candidate.validation : [];
|
||||
for (var j = 0; j < validation.length; j++) {
|
||||
var c = String(validation[j] || '').trim();
|
||||
if (!c) continue;
|
||||
if (!solidifyMod.isValidationCommandAllowed(c)) {
|
||||
throw new Error('Refusing to promote Gene ' + id + ': validation command rejected by safety check: "' + c + '". Only node/npm/npx commands without shell operators are allowed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var promoted = JSON.parse(JSON.stringify(candidate));
|
||||
if (!promoted.a2a || typeof promoted.a2a !== 'object') promoted.a2a = {};
|
||||
promoted.a2a.status = 'promoted';
|
||||
promoted.a2a.promoted_at = new Date().toISOString();
|
||||
if (!promoted.schema_version) promoted.schema_version = contentHash.SCHEMA_VERSION;
|
||||
promoted.asset_id = contentHash.computeAssetId(promoted);
|
||||
|
||||
var emitDecisions = process.env.A2A_EMIT_DECISIONS === 'true';
|
||||
|
||||
if (type === 'EvolutionEvent') {
|
||||
assetStore.appendEventJsonl(promoted);
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dmEv = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'event promoted for provenance tracking' });
|
||||
a2aProto.getTransport().send(dmEv);
|
||||
} catch (e) {}
|
||||
}
|
||||
process.stdout.write('promoted_event=' + id + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'Capsule') {
|
||||
assetStore.appendCapsule(promoted);
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dm = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'capsule promoted after validation' });
|
||||
a2aProto.getTransport().send(dm);
|
||||
} catch (e) {}
|
||||
}
|
||||
process.stdout.write('promoted_capsule=' + id + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
var localGenes = assetStore.loadGenes();
|
||||
var exists = false;
|
||||
for (var k = 0; k < localGenes.length; k++) {
|
||||
if (localGenes[k] && localGenes[k].type === 'Gene' && String(localGenes[k].id) === id) { exists = true; break; }
|
||||
}
|
||||
if (exists) {
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dm2 = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'reject', reason: 'local gene with same ID already exists' });
|
||||
a2aProto.getTransport().send(dm2);
|
||||
} catch (e) {}
|
||||
}
|
||||
process.stdout.write('conflict_keep_local_gene=' + id + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
assetStore.upsertGene(promoted);
|
||||
if (emitDecisions) {
|
||||
try {
|
||||
var dm3 = a2aProto.buildDecision({ assetId: promoted.asset_id, localId: id, decision: 'accept', reason: 'gene promoted after safety audit' });
|
||||
a2aProto.getTransport().send(dm3);
|
||||
} catch (e) {}
|
||||
}
|
||||
process.stdout.write('promoted_gene=' + id + '\n');
|
||||
}
|
||||
|
||||
try { main(); } catch (e) {
|
||||
process.stderr.write((e && e.message ? e.message : String(e)) + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
121
skills/capability-evolver/scripts/analyze_by_skill.js
Normal file
121
skills/capability-evolver/scripts/analyze_by_skill.js
Normal 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();
|
||||
|
||||
354
skills/capability-evolver/scripts/build_public.js
Normal file
354
skills/capability-evolver/scripts/build_public.js
Normal 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);
|
||||
}
|
||||
|
||||
85
skills/capability-evolver/scripts/extract_log.js
Normal file
85
skills/capability-evolver/scripts/extract_log.js
Normal 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();
|
||||
|
||||
75
skills/capability-evolver/scripts/generate_history.js
Normal file
75
skills/capability-evolver/scripts/generate_history.js
Normal 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);
|
||||
}
|
||||
|
||||
96
skills/capability-evolver/scripts/gep_append_event.js
Normal file
96
skills/capability-evolver/scripts/gep_append_event.js
Normal 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);
|
||||
}
|
||||
|
||||
234
skills/capability-evolver/scripts/gep_personality_report.js
Normal file
234
skills/capability-evolver/scripts/gep_personality_report.js
Normal 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);
|
||||
}
|
||||
|
||||
147
skills/capability-evolver/scripts/human_report.js
Normal file
147
skills/capability-evolver/scripts/human_report.js
Normal 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();
|
||||
|
||||
566
skills/capability-evolver/scripts/publish_public.js
Normal file
566
skills/capability-evolver/scripts/publish_public.js
Normal 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);
|
||||
}
|
||||
|
||||
61
skills/capability-evolver/scripts/recover_loop.js
Normal file
61
skills/capability-evolver/scripts/recover_loop.js
Normal 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();
|
||||
}
|
||||
|
||||
89
skills/capability-evolver/scripts/suggest_version.js
Normal file
89
skills/capability-evolver/scripts/suggest_version.js
Normal 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);
|
||||
}
|
||||
|
||||
13
skills/capability-evolver/src/canary.js
Normal file
13
skills/capability-evolver/src/canary.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Canary script: run in a forked child process to verify index.js loads
|
||||
// without crashing. Exit 0 = safe, non-zero = broken.
|
||||
//
|
||||
// This is the last safety net before solidify commits an evolution.
|
||||
// If a patch broke index.js (syntax error, missing require, etc.),
|
||||
// the canary catches it BEFORE the daemon restarts with broken code.
|
||||
try {
|
||||
require('../index.js');
|
||||
process.exit(0);
|
||||
} catch (e) {
|
||||
process.stderr.write(String(e.message || e).slice(0, 500));
|
||||
process.exit(1);
|
||||
}
|
||||
1405
skills/capability-evolver/src/evolve.js
Normal file
1405
skills/capability-evolver/src/evolve.js
Normal file
File diff suppressed because it is too large
Load Diff
173
skills/capability-evolver/src/gep/a2a.js
Normal file
173
skills/capability-evolver/src/gep/a2a.js
Normal file
@@ -0,0 +1,173 @@
|
||||
const fs = require('fs');
|
||||
const { readAllEvents } = require('./assetStore');
|
||||
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
|
||||
const { unwrapAssetFromMessage } = require('./a2aProtocol');
|
||||
|
||||
function nowIso() { return new Date().toISOString(); }
|
||||
|
||||
function isAllowedA2AAsset(obj) {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
var t = obj.type;
|
||||
return t === 'Gene' || t === 'Capsule' || t === 'EvolutionEvent';
|
||||
}
|
||||
|
||||
function safeNumber(x, fallback) {
|
||||
if (fallback === undefined) fallback = null;
|
||||
var n = Number(x);
|
||||
return Number.isFinite(n) ? n : fallback;
|
||||
}
|
||||
|
||||
function getBlastRadiusLimits() {
|
||||
var maxFiles = safeNumber(process.env.A2A_MAX_FILES, 5);
|
||||
var maxLines = safeNumber(process.env.A2A_MAX_LINES, 200);
|
||||
return {
|
||||
maxFiles: Number.isFinite(maxFiles) ? maxFiles : 5,
|
||||
maxLines: Number.isFinite(maxLines) ? maxLines : 200,
|
||||
};
|
||||
}
|
||||
|
||||
function isBlastRadiusSafe(blastRadius) {
|
||||
var lim = getBlastRadiusLimits();
|
||||
var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Number(blastRadius.files) : 0;
|
||||
var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Number(blastRadius.lines) : 0;
|
||||
return files <= lim.maxFiles && lines <= lim.maxLines;
|
||||
}
|
||||
|
||||
function clamp01(n) {
|
||||
var x = Number(n);
|
||||
if (!Number.isFinite(x)) return 0;
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
|
||||
function lowerConfidence(asset, opts) {
|
||||
if (!opts) opts = {};
|
||||
var factor = Number.isFinite(Number(opts.factor)) ? Number(opts.factor) : 0.6;
|
||||
var receivedFrom = opts.source || 'external';
|
||||
var receivedAt = opts.received_at || nowIso();
|
||||
var cloned = JSON.parse(JSON.stringify(asset || {}));
|
||||
if (!isAllowedA2AAsset(cloned)) return null;
|
||||
if (cloned.type === 'Capsule') {
|
||||
if (typeof cloned.confidence === 'number') cloned.confidence = clamp01(cloned.confidence * factor);
|
||||
else if (cloned.confidence != null) cloned.confidence = clamp01(Number(cloned.confidence) * factor);
|
||||
}
|
||||
if (!cloned.a2a || typeof cloned.a2a !== 'object') cloned.a2a = {};
|
||||
cloned.a2a.status = 'external_candidate';
|
||||
cloned.a2a.source = receivedFrom;
|
||||
cloned.a2a.received_at = receivedAt;
|
||||
cloned.a2a.confidence_factor = factor;
|
||||
if (!cloned.schema_version) cloned.schema_version = SCHEMA_VERSION;
|
||||
if (!cloned.asset_id) { try { cloned.asset_id = computeAssetId(cloned); } catch (e) {} }
|
||||
return cloned;
|
||||
}
|
||||
|
||||
function readEvolutionEvents() {
|
||||
var events = readAllEvents();
|
||||
return Array.isArray(events) ? events.filter(function (e) { return e && e.type === 'EvolutionEvent'; }) : [];
|
||||
}
|
||||
|
||||
function normalizeEventsList(events) {
|
||||
return Array.isArray(events) ? events : [];
|
||||
}
|
||||
|
||||
function computeCapsuleSuccessStreak(params) {
|
||||
var capsuleId = params.capsuleId;
|
||||
var events = params.events;
|
||||
var id = capsuleId ? String(capsuleId) : '';
|
||||
if (!id) return 0;
|
||||
var list = normalizeEventsList(events || readEvolutionEvents());
|
||||
var streak = 0;
|
||||
for (var i = list.length - 1; i >= 0; i--) {
|
||||
var ev = list[i];
|
||||
if (!ev || ev.type !== 'EvolutionEvent') continue;
|
||||
if (!ev.capsule_id || String(ev.capsule_id) !== id) continue;
|
||||
var st = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
|
||||
if (st === 'success') streak += 1; else break;
|
||||
}
|
||||
return streak;
|
||||
}
|
||||
|
||||
function isCapsuleBroadcastEligible(capsule, opts) {
|
||||
if (!opts) opts = {};
|
||||
if (!capsule || capsule.type !== 'Capsule') return false;
|
||||
var score = capsule.outcome && capsule.outcome.score != null ? safeNumber(capsule.outcome.score, null) : null;
|
||||
if (score == null || score < 0.7) return false;
|
||||
var blast = capsule.blast_radius || (capsule.outcome && capsule.outcome.blast_radius) || null;
|
||||
if (!isBlastRadiusSafe(blast)) return false;
|
||||
var events = Array.isArray(opts.events) ? opts.events : readEvolutionEvents();
|
||||
var streak = computeCapsuleSuccessStreak({ capsuleId: capsule.id, events: events });
|
||||
if (streak < 2) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function exportEligibleCapsules(params) {
|
||||
if (!params) params = {};
|
||||
var list = Array.isArray(params.capsules) ? params.capsules : [];
|
||||
var evs = Array.isArray(params.events) ? params.events : readEvolutionEvents();
|
||||
var eligible = list.filter(function (c) { return isCapsuleBroadcastEligible(c, { events: evs }); });
|
||||
for (var i = 0; i < eligible.length; i++) {
|
||||
var c = eligible[i];
|
||||
if (!c.schema_version) c.schema_version = SCHEMA_VERSION;
|
||||
if (!c.asset_id) { try { c.asset_id = computeAssetId(c); } catch (e) {} }
|
||||
}
|
||||
return eligible;
|
||||
}
|
||||
|
||||
function isGeneBroadcastEligible(gene) {
|
||||
if (!gene || gene.type !== 'Gene') return false;
|
||||
if (!gene.id || typeof gene.id !== 'string') return false;
|
||||
if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) return false;
|
||||
if (!Array.isArray(gene.validation) || gene.validation.length === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function exportEligibleGenes(params) {
|
||||
if (!params) params = {};
|
||||
var list = Array.isArray(params.genes) ? params.genes : [];
|
||||
var eligible = list.filter(function (g) { return isGeneBroadcastEligible(g); });
|
||||
for (var i = 0; i < eligible.length; i++) {
|
||||
var g = eligible[i];
|
||||
if (!g.schema_version) g.schema_version = SCHEMA_VERSION;
|
||||
if (!g.asset_id) { try { g.asset_id = computeAssetId(g); } catch (e) {} }
|
||||
}
|
||||
return eligible;
|
||||
}
|
||||
|
||||
function parseA2AInput(text) {
|
||||
var raw = String(text || '').trim();
|
||||
if (!raw) return [];
|
||||
try {
|
||||
var maybe = JSON.parse(raw);
|
||||
if (Array.isArray(maybe)) {
|
||||
return maybe.map(function (item) { return unwrapAssetFromMessage(item) || item; }).filter(Boolean);
|
||||
}
|
||||
if (maybe && typeof maybe === 'object') {
|
||||
var unwrapped = unwrapAssetFromMessage(maybe);
|
||||
return unwrapped ? [unwrapped] : [maybe];
|
||||
}
|
||||
} catch (e) {}
|
||||
var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
|
||||
var items = [];
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
try {
|
||||
var obj = JSON.parse(lines[i]);
|
||||
var uw = unwrapAssetFromMessage(obj);
|
||||
items.push(uw || obj);
|
||||
} catch (e) { continue; }
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function readTextIfExists(filePath) {
|
||||
try {
|
||||
if (!filePath) return '';
|
||||
if (!fs.existsSync(filePath)) return '';
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} catch { return ''; }
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isAllowedA2AAsset, lowerConfidence, isBlastRadiusSafe,
|
||||
computeCapsuleSuccessStreak, isCapsuleBroadcastEligible,
|
||||
exportEligibleCapsules, isGeneBroadcastEligible,
|
||||
exportEligibleGenes, parseA2AInput, readTextIfExists,
|
||||
};
|
||||
367
skills/capability-evolver/src/gep/a2aProtocol.js
Normal file
367
skills/capability-evolver/src/gep/a2aProtocol.js
Normal file
@@ -0,0 +1,367 @@
|
||||
// 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 { getDeviceId } = require('./deviceId');
|
||||
|
||||
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 deviceId = getDeviceId();
|
||||
const agentName = process.env.AGENT_NAME || 'default';
|
||||
// Include cwd so multiple evolver instances in different directories
|
||||
// on the same machine get distinct nodeIds without manual config.
|
||||
const raw = deviceId + '|' + agentName + '|' + process.cwd();
|
||||
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');
|
||||
}
|
||||
// Generate signature: HMAC-SHA256 of asset_id with node secret
|
||||
var assetIdVal = asset.asset_id || computeAssetId(asset);
|
||||
var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
|
||||
var signature = crypto.createHmac('sha256', nodeSecret).update(assetIdVal).digest('hex');
|
||||
return buildMessage({
|
||||
messageType: 'publish',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
asset_type: asset.type,
|
||||
asset_id: assetIdVal,
|
||||
local_id: asset.id,
|
||||
asset: asset,
|
||||
signature: signature,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Build a bundle publish message containing Gene + Capsule (+ optional EvolutionEvent).
|
||||
// Hub requires payload.assets = [Gene, Capsule] since bundle enforcement was added.
|
||||
function buildPublishBundle(opts) {
|
||||
var o = opts || {};
|
||||
var gene = o.gene;
|
||||
var capsule = o.capsule;
|
||||
var event = o.event || null;
|
||||
if (!gene || gene.type !== 'Gene' || !gene.id) {
|
||||
throw new Error('publishBundle: gene must be a valid Gene with type and id');
|
||||
}
|
||||
if (!capsule || capsule.type !== 'Capsule' || !capsule.id) {
|
||||
throw new Error('publishBundle: capsule must be a valid Capsule with type and id');
|
||||
}
|
||||
var geneAssetId = gene.asset_id || computeAssetId(gene);
|
||||
var capsuleAssetId = capsule.asset_id || computeAssetId(capsule);
|
||||
var nodeSecret = process.env.A2A_NODE_SECRET || getNodeId();
|
||||
var signatureInput = [geneAssetId, capsuleAssetId].sort().join('|');
|
||||
var signature = crypto.createHmac('sha256', nodeSecret).update(signatureInput).digest('hex');
|
||||
var assets = [gene, capsule];
|
||||
if (event && event.type === 'EvolutionEvent') assets.push(event);
|
||||
var publishPayload = {
|
||||
assets: assets,
|
||||
signature: signature,
|
||||
};
|
||||
if (o.chainId && typeof o.chainId === 'string') {
|
||||
publishPayload.chain_id = o.chainId;
|
||||
}
|
||||
return buildMessage({
|
||||
messageType: 'publish',
|
||||
senderId: o.nodeId,
|
||||
payload: publishPayload,
|
||||
});
|
||||
}
|
||||
|
||||
function buildFetch(opts) {
|
||||
var o = opts || {};
|
||||
return buildMessage({
|
||||
messageType: 'fetch',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
asset_type: o.assetType || null,
|
||||
local_id: o.localId || null,
|
||||
content_hash: o.contentHash || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildReport(opts) {
|
||||
var o = opts || {};
|
||||
return buildMessage({
|
||||
messageType: 'report',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
target_asset_id: o.assetId || null,
|
||||
target_local_id: o.localId || null,
|
||||
validation_report: o.validationReport || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildDecision(opts) {
|
||||
var o = opts || {};
|
||||
var validDecisions = ['accept', 'reject', 'quarantine'];
|
||||
if (!validDecisions.includes(o.decision)) {
|
||||
throw new Error('decision must be one of: ' + validDecisions.join(', '));
|
||||
}
|
||||
return buildMessage({
|
||||
messageType: 'decision',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
target_asset_id: o.assetId || null,
|
||||
target_local_id: o.localId || null,
|
||||
decision: o.decision,
|
||||
reason: o.reason || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function buildRevoke(opts) {
|
||||
var o = opts || {};
|
||||
return buildMessage({
|
||||
messageType: 'revoke',
|
||||
senderId: o.nodeId,
|
||||
payload: {
|
||||
target_asset_id: o.assetId || null,
|
||||
target_local_id: o.localId || null,
|
||||
reason: o.reason || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
|
||||
function isValidProtocolMessage(msg) {
|
||||
if (!msg || typeof msg !== 'object') return false;
|
||||
if (msg.protocol !== PROTOCOL_NAME) return false;
|
||||
if (!msg.message_type || !VALID_MESSAGE_TYPES.includes(msg.message_type)) return false;
|
||||
if (!msg.message_id || typeof msg.message_id !== 'string') return false;
|
||||
if (!msg.timestamp || typeof msg.timestamp !== 'string') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to extract a raw asset from either a protocol message or a plain asset object.
|
||||
// This enables backward-compatible ingestion of both old-format and new-format payloads.
|
||||
function unwrapAssetFromMessage(input) {
|
||||
if (!input || typeof input !== 'object') return null;
|
||||
// If it is a protocol message with a publish payload, extract the asset.
|
||||
if (input.protocol === PROTOCOL_NAME && input.message_type === 'publish') {
|
||||
var p = input.payload;
|
||||
if (p && p.asset && typeof p.asset === 'object') return p.asset;
|
||||
return null;
|
||||
}
|
||||
// If it is a plain asset (Gene/Capsule/EvolutionEvent), return as-is.
|
||||
if (input.type === 'Gene' || input.type === 'Capsule' || input.type === 'EvolutionEvent') {
|
||||
return input;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- File Transport ---
|
||||
|
||||
function ensureDir(dir) {
|
||||
try {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function defaultA2ADir() {
|
||||
return process.env.A2A_DIR || path.join(getGepAssetsDir(), 'a2a');
|
||||
}
|
||||
|
||||
function fileTransportSend(message, opts) {
|
||||
var dir = (opts && opts.dir) || defaultA2ADir();
|
||||
var subdir = path.join(dir, 'outbox');
|
||||
ensureDir(subdir);
|
||||
var filePath = path.join(subdir, message.message_type + '.jsonl');
|
||||
fs.appendFileSync(filePath, JSON.stringify(message) + '\n', 'utf8');
|
||||
return { ok: true, path: filePath };
|
||||
}
|
||||
|
||||
function fileTransportReceive(opts) {
|
||||
var dir = (opts && opts.dir) || defaultA2ADir();
|
||||
var subdir = path.join(dir, 'inbox');
|
||||
if (!fs.existsSync(subdir)) return [];
|
||||
var files = fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
|
||||
var messages = [];
|
||||
for (var fi = 0; fi < files.length; fi++) {
|
||||
try {
|
||||
var raw = fs.readFileSync(path.join(subdir, files[fi]), 'utf8');
|
||||
var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
|
||||
for (var li = 0; li < lines.length; li++) {
|
||||
try {
|
||||
var msg = JSON.parse(lines[li]);
|
||||
if (msg && msg.protocol === PROTOCOL_NAME) messages.push(msg);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
function fileTransportList(opts) {
|
||||
var dir = (opts && opts.dir) || defaultA2ADir();
|
||||
var subdir = path.join(dir, 'outbox');
|
||||
if (!fs.existsSync(subdir)) return [];
|
||||
return fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
|
||||
}
|
||||
|
||||
// --- HTTP Transport (connects to evomap-hub) ---
|
||||
|
||||
function httpTransportSend(message, opts) {
|
||||
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
|
||||
if (!hubUrl) return { ok: false, error: 'A2A_HUB_URL not set' };
|
||||
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/' + message.message_type;
|
||||
var body = JSON.stringify(message);
|
||||
// Use dynamic import for fetch (available in Node 18+)
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: body,
|
||||
})
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) { return { ok: true, response: data }; })
|
||||
.catch(function (err) { return { ok: false, error: err.message }; });
|
||||
}
|
||||
|
||||
function httpTransportReceive(opts) {
|
||||
var hubUrl = (opts && opts.hubUrl) || process.env.A2A_HUB_URL;
|
||||
if (!hubUrl) return Promise.resolve([]);
|
||||
var assetType = (opts && opts.assetType) || null;
|
||||
var fetchMsg = buildFetch({ assetType: assetType });
|
||||
var endpoint = hubUrl.replace(/\/+$/, '') + '/a2a/fetch';
|
||||
return fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(fetchMsg),
|
||||
})
|
||||
.then(function (res) { return res.json(); })
|
||||
.then(function (data) {
|
||||
if (data && data.payload && Array.isArray(data.payload.results)) {
|
||||
return data.payload.results;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
.catch(function () { return []; });
|
||||
}
|
||||
|
||||
function httpTransportList() {
|
||||
return ['http'];
|
||||
}
|
||||
|
||||
// --- Transport registry ---
|
||||
|
||||
var transports = {
|
||||
file: {
|
||||
send: fileTransportSend,
|
||||
receive: fileTransportReceive,
|
||||
list: fileTransportList,
|
||||
},
|
||||
http: {
|
||||
send: httpTransportSend,
|
||||
receive: httpTransportReceive,
|
||||
list: httpTransportList,
|
||||
},
|
||||
};
|
||||
|
||||
function getTransport(name) {
|
||||
var n = String(name || process.env.A2A_TRANSPORT || 'file').toLowerCase();
|
||||
var t = transports[n];
|
||||
if (!t) throw new Error('Unknown A2A transport: ' + n + '. Available: ' + Object.keys(transports).join(', '));
|
||||
return t;
|
||||
}
|
||||
|
||||
function registerTransport(name, impl) {
|
||||
if (!name || typeof name !== 'string') throw new Error('transport name required');
|
||||
if (!impl || typeof impl.send !== 'function' || typeof impl.receive !== 'function') {
|
||||
throw new Error('transport must implement send() and receive()');
|
||||
}
|
||||
transports[name] = impl;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PROTOCOL_NAME,
|
||||
PROTOCOL_VERSION,
|
||||
VALID_MESSAGE_TYPES,
|
||||
getNodeId,
|
||||
buildMessage,
|
||||
buildHello,
|
||||
buildPublish,
|
||||
buildPublishBundle,
|
||||
buildFetch,
|
||||
buildReport,
|
||||
buildDecision,
|
||||
buildRevoke,
|
||||
isValidProtocolMessage,
|
||||
unwrapAssetFromMessage,
|
||||
getTransport,
|
||||
registerTransport,
|
||||
fileTransportSend,
|
||||
fileTransportReceive,
|
||||
fileTransportList,
|
||||
httpTransportSend,
|
||||
httpTransportReceive,
|
||||
httpTransportList,
|
||||
};
|
||||
35
skills/capability-evolver/src/gep/analyzer.js
Normal file
35
skills/capability-evolver/src/gep/analyzer.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Innovation: Self-Correction Analyzer
|
||||
// Analyze past failures to suggest better future mutations
|
||||
// Pattern: Meta-learning
|
||||
|
||||
function analyzeFailures() {
|
||||
const memoryPath = path.join(process.cwd(), 'MEMORY.md');
|
||||
if (!fs.existsSync(memoryPath)) return { status: 'skipped', reason: 'no_memory' };
|
||||
|
||||
const content = fs.readFileSync(memoryPath, 'utf8');
|
||||
const failureRegex = /\|\s*\*\*F\d+\*\*\s*\|\s*Fix\s*\|\s*(.*?)\s*\|\s*\*\*(.*?)\*\*\s*\((.*?)\)\s*\|/g;
|
||||
|
||||
const failures = [];
|
||||
let match;
|
||||
while ((match = failureRegex.exec(content)) !== null) {
|
||||
failures.push({
|
||||
summary: match[1].trim(),
|
||||
detail: match[2].trim()
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
count: failures.length,
|
||||
failures: failures.slice(0, 3) // Return top 3 for prompt context
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
console.log(JSON.stringify(analyzeFailures(), null, 2));
|
||||
}
|
||||
|
||||
module.exports = { analyzeFailures };
|
||||
269
skills/capability-evolver/src/gep/assetStore.js
Normal file
269
skills/capability-evolver/src/gep/assetStore.js
Normal file
@@ -0,0 +1,269 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getGepAssetsDir } = require('./paths');
|
||||
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function readJsonIfExists(filePath, fallback) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return fallback;
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
if (!raw.trim()) return fallback;
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJsonAtomic(filePath, obj) {
|
||||
const dir = path.dirname(filePath);
|
||||
ensureDir(dir);
|
||||
const tmp = `${filePath}.tmp`;
|
||||
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmp, filePath);
|
||||
}
|
||||
|
||||
// Build a robust validation command that works regardless of CWD.
|
||||
// Resolves module paths relative to the skill root (skills/evolver/).
|
||||
function buildValidationCmd(relModules) {
|
||||
const skillRoot = path.resolve(__dirname, '..', '..');
|
||||
const checks = relModules.map(m => {
|
||||
const abs = path.join(skillRoot, m).replace(/\\/g, '/');
|
||||
return `require('${abs}')`;
|
||||
});
|
||||
return `node -e "${checks.join('; ')}; console.log('ok')"`;
|
||||
}
|
||||
|
||||
function getDefaultGenes() {
|
||||
return {
|
||||
version: 1,
|
||||
genes: [
|
||||
{
|
||||
type: 'Gene', id: 'gene_gep_repair_from_errors', category: 'repair',
|
||||
signals_match: ['error', 'exception', 'failed', 'unstable'],
|
||||
preconditions: ['signals contains error-related indicators'],
|
||||
strategy: [
|
||||
'Extract structured signals from logs and user instructions',
|
||||
'Select an existing Gene by signals match (no improvisation)',
|
||||
'Estimate blast radius (files, lines) before editing',
|
||||
'Apply smallest reversible patch',
|
||||
'Validate using declared validation steps; rollback on failure',
|
||||
'Solidify knowledge: append EvolutionEvent, update Gene/Capsule store',
|
||||
],
|
||||
constraints: { max_files: 12, forbidden_paths: ['.git', 'node_modules'] },
|
||||
validation: [
|
||||
buildValidationCmd(['src/evolve', 'src/gep/solidify']),
|
||||
buildValidationCmd(['src/gep/selector', 'src/gep/memoryGraph']),
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'Gene', id: 'gene_gep_optimize_prompt_and_assets', category: 'optimize',
|
||||
signals_match: ['protocol', 'gep', 'prompt', 'audit', 'reusable'],
|
||||
preconditions: ['need stricter, auditable evolution protocol outputs'],
|
||||
strategy: [
|
||||
'Extract signals and determine selection rationale via Selector JSON',
|
||||
'Prefer reusing existing Gene/Capsule; only create if no match exists',
|
||||
'Refactor prompt assembly to embed assets (genes, capsules, parent event)',
|
||||
'Reduce noise and ambiguity; enforce strict output schema',
|
||||
'Validate by running node index.js run and ensuring no runtime errors',
|
||||
'Solidify: record EvolutionEvent, update Gene definitions, create Capsule on success',
|
||||
],
|
||||
constraints: { max_files: 20, forbidden_paths: ['.git', 'node_modules'] },
|
||||
validation: [buildValidationCmd(['src/evolve', 'src/gep/prompt'])],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultCapsules() { return { version: 1, capsules: [] }; }
|
||||
function genesPath() { return path.join(getGepAssetsDir(), 'genes.json'); }
|
||||
function capsulesPath() { return path.join(getGepAssetsDir(), 'capsules.json'); }
|
||||
function capsulesJsonlPath() { return path.join(getGepAssetsDir(), 'capsules.jsonl'); }
|
||||
function eventsPath() { return path.join(getGepAssetsDir(), 'events.jsonl'); }
|
||||
function candidatesPath() { return path.join(getGepAssetsDir(), 'candidates.jsonl'); }
|
||||
function externalCandidatesPath() { return path.join(getGepAssetsDir(), 'external_candidates.jsonl'); }
|
||||
|
||||
function loadGenes() {
|
||||
const jsonGenes = readJsonIfExists(genesPath(), getDefaultGenes()).genes || [];
|
||||
const jsonlGenes = [];
|
||||
try {
|
||||
const p = path.join(getGepAssetsDir(), 'genes.jsonl');
|
||||
if (fs.existsSync(p)) {
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
raw.split('\n').forEach(line => {
|
||||
if (line.trim()) {
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
if (parsed && parsed.type === 'Gene') jsonlGenes.push(parsed);
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Combine and deduplicate by ID (JSONL takes precedence if newer, but here we just merge)
|
||||
const combined = [...jsonGenes, ...jsonlGenes];
|
||||
const unique = new Map();
|
||||
combined.forEach(g => {
|
||||
if (g && g.id) unique.set(String(g.id), g);
|
||||
});
|
||||
return Array.from(unique.values());
|
||||
}
|
||||
|
||||
function loadCapsules() {
|
||||
const legacy = readJsonIfExists(capsulesPath(), getDefaultCapsules()).capsules || [];
|
||||
const jsonlCapsules = [];
|
||||
try {
|
||||
const p = capsulesJsonlPath();
|
||||
if (fs.existsSync(p)) {
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
raw.split('\n').forEach(line => {
|
||||
if (line.trim()) {
|
||||
try { jsonlCapsules.push(JSON.parse(line)); } catch(e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {}
|
||||
|
||||
// Combine and deduplicate by ID
|
||||
const combined = [...legacy, ...jsonlCapsules];
|
||||
const unique = new Map();
|
||||
combined.forEach(c => {
|
||||
if (c && c.id) unique.set(String(c.id), c);
|
||||
});
|
||||
return Array.from(unique.values());
|
||||
}
|
||||
|
||||
function getLastEventId() {
|
||||
try {
|
||||
const p = eventsPath();
|
||||
if (!fs.existsSync(p)) return null;
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
if (lines.length === 0) return null;
|
||||
const last = JSON.parse(lines[lines.length - 1]);
|
||||
return last && typeof last.id === 'string' ? last.id : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function readAllEvents() {
|
||||
try {
|
||||
const p = eventsPath();
|
||||
if (!fs.existsSync(p)) return [];
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
return raw.split('\n').map(l => l.trim()).filter(Boolean).map(l => {
|
||||
try { return JSON.parse(l); } catch { return null; }
|
||||
}).filter(Boolean);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function appendEventJsonl(eventObj) {
|
||||
const dir = getGepAssetsDir(); ensureDir(dir);
|
||||
fs.appendFileSync(eventsPath(), JSON.stringify(eventObj) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function appendCandidateJsonl(candidateObj) {
|
||||
const dir = getGepAssetsDir(); ensureDir(dir);
|
||||
fs.appendFileSync(candidatesPath(), JSON.stringify(candidateObj) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function appendExternalCandidateJsonl(obj) {
|
||||
const dir = getGepAssetsDir(); ensureDir(dir);
|
||||
fs.appendFileSync(externalCandidatesPath(), JSON.stringify(obj) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function readRecentCandidates(limit = 20) {
|
||||
try {
|
||||
const p = candidatesPath();
|
||||
if (!fs.existsSync(p)) return [];
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
return lines.slice(Math.max(0, lines.length - limit)).map(l => {
|
||||
try { return JSON.parse(l); } catch { return null; }
|
||||
}).filter(Boolean);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function readRecentExternalCandidates(limit = 50) {
|
||||
try {
|
||||
const p = externalCandidatesPath();
|
||||
if (!fs.existsSync(p)) return [];
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
|
||||
return lines.slice(Math.max(0, lines.length - limit)).map(l => {
|
||||
try { return JSON.parse(l); } catch { return null; }
|
||||
}).filter(Boolean);
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
// Safety net: ensure schema_version and asset_id are present before writing.
|
||||
function ensureSchemaFields(obj) {
|
||||
if (!obj || typeof obj !== 'object') return obj;
|
||||
if (!obj.schema_version) obj.schema_version = SCHEMA_VERSION;
|
||||
if (!obj.asset_id) { try { obj.asset_id = computeAssetId(obj); } catch (e) {} }
|
||||
return obj;
|
||||
}
|
||||
|
||||
function upsertGene(geneObj) {
|
||||
ensureSchemaFields(geneObj);
|
||||
const current = readJsonIfExists(genesPath(), getDefaultGenes());
|
||||
const genes = Array.isArray(current.genes) ? current.genes : [];
|
||||
const idx = genes.findIndex(g => g && g.id === geneObj.id);
|
||||
if (idx >= 0) genes[idx] = geneObj; else genes.push(geneObj);
|
||||
writeJsonAtomic(genesPath(), { version: current.version || 1, genes });
|
||||
}
|
||||
|
||||
function appendCapsule(capsuleObj) {
|
||||
ensureSchemaFields(capsuleObj);
|
||||
const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
|
||||
const capsules = Array.isArray(current.capsules) ? current.capsules : [];
|
||||
capsules.push(capsuleObj);
|
||||
writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
|
||||
}
|
||||
|
||||
function upsertCapsule(capsuleObj) {
|
||||
if (!capsuleObj || capsuleObj.type !== 'Capsule' || !capsuleObj.id) return;
|
||||
ensureSchemaFields(capsuleObj);
|
||||
const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
|
||||
const capsules = Array.isArray(current.capsules) ? current.capsules : [];
|
||||
const idx = capsules.findIndex(c => c && c.type === 'Capsule' && String(c.id) === String(capsuleObj.id));
|
||||
if (idx >= 0) capsules[idx] = capsuleObj; else capsules.push(capsuleObj);
|
||||
writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
|
||||
}
|
||||
|
||||
// Ensure all expected asset files exist on startup.
|
||||
// Creates empty files for optional append-only stores so that
|
||||
// external grep/read commands never fail with "No such file or directory".
|
||||
function ensureAssetFiles() {
|
||||
const dir = getGepAssetsDir();
|
||||
ensureDir(dir);
|
||||
const files = [
|
||||
{ path: genesPath(), defaultContent: JSON.stringify(getDefaultGenes(), null, 2) + '\n' },
|
||||
{ path: capsulesPath(), defaultContent: JSON.stringify(getDefaultCapsules(), null, 2) + '\n' },
|
||||
{ path: path.join(dir, 'genes.jsonl'), defaultContent: '' },
|
||||
{ path: eventsPath(), defaultContent: '' },
|
||||
{ path: candidatesPath(), defaultContent: '' },
|
||||
];
|
||||
for (const f of files) {
|
||||
if (!fs.existsSync(f.path)) {
|
||||
try {
|
||||
fs.writeFileSync(f.path, f.defaultContent, 'utf8');
|
||||
} catch (e) {
|
||||
// Non-fatal: log but continue
|
||||
console.error(`[AssetStore] Failed to create ${f.path}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadGenes, loadCapsules, readAllEvents, getLastEventId,
|
||||
appendEventJsonl, appendCandidateJsonl, appendExternalCandidateJsonl,
|
||||
readRecentCandidates, readRecentExternalCandidates,
|
||||
upsertGene, appendCapsule, upsertCapsule,
|
||||
genesPath, capsulesPath, eventsPath, candidatesPath, externalCandidatesPath,
|
||||
ensureAssetFiles, buildValidationCmd,
|
||||
};
|
||||
36
skills/capability-evolver/src/gep/assets.js
Normal file
36
skills/capability-evolver/src/gep/assets.js
Normal file
@@ -0,0 +1,36 @@
|
||||
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
|
||||
|
||||
/**
|
||||
* Format asset preview for prompt inclusion.
|
||||
* Handles stringified JSON, arrays, and error cases gracefully.
|
||||
*/
|
||||
function formatAssetPreview(preview) {
|
||||
if (!preview) return '(none)';
|
||||
if (typeof preview === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(preview);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
}
|
||||
return preview; // Keep as string if not array or empty
|
||||
} catch (e) {
|
||||
return preview; // Keep as string if parse fails
|
||||
}
|
||||
}
|
||||
return JSON.stringify(preview, null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize an asset object.
|
||||
* Ensures schema version and ID are present.
|
||||
*/
|
||||
function normalizeAsset(asset) {
|
||||
if (!asset || typeof asset !== 'object') return asset;
|
||||
if (!asset.schema_version) asset.schema_version = SCHEMA_VERSION;
|
||||
if (!asset.asset_id) {
|
||||
try { asset.asset_id = computeAssetId(asset); } catch (e) {}
|
||||
}
|
||||
return asset;
|
||||
}
|
||||
|
||||
module.exports = { formatAssetPreview, normalizeAsset };
|
||||
71
skills/capability-evolver/src/gep/bridge.js
Normal file
71
skills/capability-evolver/src/gep/bridge.js
Normal 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,
|
||||
};
|
||||
|
||||
142
skills/capability-evolver/src/gep/candidates.js
Normal file
142
skills/capability-evolver/src/gep/candidates.js
Normal 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,
|
||||
};
|
||||
|
||||
65
skills/capability-evolver/src/gep/contentHash.js
Normal file
65
skills/capability-evolver/src/gep/contentHash.js
Normal 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,
|
||||
};
|
||||
209
skills/capability-evolver/src/gep/deviceId.js
Normal file
209
skills/capability-evolver/src/gep/deviceId.js
Normal file
@@ -0,0 +1,209 @@
|
||||
// Stable device identifier for node identity.
|
||||
// Generates a hardware-based fingerprint that persists across directory changes,
|
||||
// reboots, and evolver upgrades. Used by getNodeId() and env_fingerprint.
|
||||
//
|
||||
// Priority chain:
|
||||
// 1. EVOMAP_DEVICE_ID env var (explicit override, recommended for containers)
|
||||
// 2. ~/.evomap/device_id file (persisted from previous run)
|
||||
// 3. <project>/.evomap_device_id (fallback persist path for containers w/o $HOME)
|
||||
// 4. /etc/machine-id (Linux, set at OS install)
|
||||
// 5. IOPlatformUUID (macOS hardware UUID)
|
||||
// 6. Docker/OCI container ID (from /proc/self/cgroup or /proc/self/mountinfo)
|
||||
// 7. hostname + MAC addresses (network-based fallback)
|
||||
// 8. random 128-bit hex (last resort, persisted immediately)
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const DEVICE_ID_DIR = path.join(os.homedir(), '.evomap');
|
||||
const DEVICE_ID_FILE = path.join(DEVICE_ID_DIR, 'device_id');
|
||||
const LOCAL_DEVICE_ID_FILE = path.resolve(__dirname, '..', '..', '.evomap_device_id');
|
||||
|
||||
let _cachedDeviceId = null;
|
||||
|
||||
const DEVICE_ID_RE = /^[a-f0-9]{16,64}$/;
|
||||
|
||||
function isContainer() {
|
||||
try {
|
||||
if (fs.existsSync('/.dockerenv')) return true;
|
||||
} catch {}
|
||||
try {
|
||||
const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
|
||||
if (/docker|kubepods|containerd|cri-o|lxc|ecs/i.test(cgroup)) return true;
|
||||
} catch {}
|
||||
try {
|
||||
if (fs.existsSync('/run/.containerenv')) return true;
|
||||
} catch {}
|
||||
return false;
|
||||
}
|
||||
|
||||
function readMachineId() {
|
||||
try {
|
||||
const mid = fs.readFileSync('/etc/machine-id', 'utf8').trim();
|
||||
if (mid && mid.length >= 16) return mid;
|
||||
} catch {}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
try {
|
||||
const { execFileSync } = require('child_process');
|
||||
const raw = execFileSync('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice'], {
|
||||
encoding: 'utf8',
|
||||
timeout: 3000,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
});
|
||||
const match = raw.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
||||
if (match && match[1]) return match[1];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract Docker/OCI container ID from cgroup or mountinfo.
|
||||
// The container ID is 64-char hex and stable for the lifetime of the container.
|
||||
// Returns null on non-container hosts or if parsing fails.
|
||||
function readContainerId() {
|
||||
// Method 1: /proc/self/cgroup (works for cgroup v1 and most Docker setups)
|
||||
try {
|
||||
const cgroup = fs.readFileSync('/proc/self/cgroup', 'utf8');
|
||||
const match = cgroup.match(/[a-f0-9]{64}/);
|
||||
if (match) return match[0];
|
||||
} catch {}
|
||||
|
||||
// Method 2: /proc/self/mountinfo (works for cgroup v2 / containerd)
|
||||
try {
|
||||
const mountinfo = fs.readFileSync('/proc/self/mountinfo', 'utf8');
|
||||
const match = mountinfo.match(/[a-f0-9]{64}/);
|
||||
if (match) return match[0];
|
||||
} catch {}
|
||||
|
||||
// Method 3: hostname in Docker defaults to short container ID (12 hex chars)
|
||||
if (isContainer()) {
|
||||
const hostname = os.hostname();
|
||||
if (/^[a-f0-9]{12,64}$/.test(hostname)) return hostname;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getMacAddresses() {
|
||||
const ifaces = os.networkInterfaces();
|
||||
const macs = [];
|
||||
for (const name of Object.keys(ifaces)) {
|
||||
for (const iface of ifaces[name]) {
|
||||
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
||||
macs.push(iface.mac);
|
||||
}
|
||||
}
|
||||
}
|
||||
macs.sort();
|
||||
return macs;
|
||||
}
|
||||
|
||||
function generateDeviceId() {
|
||||
const machineId = readMachineId();
|
||||
if (machineId) {
|
||||
return crypto.createHash('sha256').update('evomap:' + machineId).digest('hex').slice(0, 32);
|
||||
}
|
||||
|
||||
// Container ID: stable for the container's lifetime, but changes on re-create.
|
||||
// Still better than random for keeping identity within a single deployment.
|
||||
const containerId = readContainerId();
|
||||
if (containerId) {
|
||||
return crypto.createHash('sha256').update('evomap:container:' + containerId).digest('hex').slice(0, 32);
|
||||
}
|
||||
|
||||
const macs = getMacAddresses();
|
||||
if (macs.length > 0) {
|
||||
const raw = os.hostname() + '|' + macs.join(',');
|
||||
return crypto.createHash('sha256').update('evomap:' + raw).digest('hex').slice(0, 32);
|
||||
}
|
||||
|
||||
return crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
function persistDeviceId(id) {
|
||||
// Try primary path (~/.evomap/device_id)
|
||||
try {
|
||||
if (!fs.existsSync(DEVICE_ID_DIR)) {
|
||||
fs.mkdirSync(DEVICE_ID_DIR, { recursive: true, mode: 0o700 });
|
||||
}
|
||||
fs.writeFileSync(DEVICE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
|
||||
return;
|
||||
} catch {}
|
||||
|
||||
// Fallback: project-local file (useful in containers where $HOME is ephemeral
|
||||
// but the project directory is mounted as a volume)
|
||||
try {
|
||||
fs.writeFileSync(LOCAL_DEVICE_ID_FILE, id, { encoding: 'utf8', mode: 0o600 });
|
||||
return;
|
||||
} catch {}
|
||||
|
||||
console.error(
|
||||
'[evolver] WARN: failed to persist device_id to ' + DEVICE_ID_FILE +
|
||||
' or ' + LOCAL_DEVICE_ID_FILE +
|
||||
' -- node identity may change on restart.' +
|
||||
' Set EVOMAP_DEVICE_ID env var for stable identity in containers.'
|
||||
);
|
||||
}
|
||||
|
||||
function loadPersistedDeviceId() {
|
||||
// Try primary path
|
||||
try {
|
||||
if (fs.existsSync(DEVICE_ID_FILE)) {
|
||||
const id = fs.readFileSync(DEVICE_ID_FILE, 'utf8').trim();
|
||||
if (id && DEVICE_ID_RE.test(id)) return id;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// Try project-local fallback
|
||||
try {
|
||||
if (fs.existsSync(LOCAL_DEVICE_ID_FILE)) {
|
||||
const id = fs.readFileSync(LOCAL_DEVICE_ID_FILE, 'utf8').trim();
|
||||
if (id && DEVICE_ID_RE.test(id)) return id;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDeviceId() {
|
||||
if (_cachedDeviceId) return _cachedDeviceId;
|
||||
|
||||
// 1. Env var override (validated)
|
||||
if (process.env.EVOMAP_DEVICE_ID) {
|
||||
const envId = String(process.env.EVOMAP_DEVICE_ID).trim().toLowerCase();
|
||||
if (DEVICE_ID_RE.test(envId)) {
|
||||
_cachedDeviceId = envId;
|
||||
return _cachedDeviceId;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Previously persisted (checks both ~/.evomap/ and project-local)
|
||||
const persisted = loadPersistedDeviceId();
|
||||
if (persisted) {
|
||||
_cachedDeviceId = persisted;
|
||||
return _cachedDeviceId;
|
||||
}
|
||||
|
||||
// 3. Generate from hardware / container metadata and persist
|
||||
const inContainer = isContainer();
|
||||
const generated = generateDeviceId();
|
||||
persistDeviceId(generated);
|
||||
_cachedDeviceId = generated;
|
||||
|
||||
if (inContainer && !process.env.EVOMAP_DEVICE_ID) {
|
||||
console.error(
|
||||
'[evolver] NOTE: running in a container without EVOMAP_DEVICE_ID.' +
|
||||
' A device_id was auto-generated and persisted, but for guaranteed' +
|
||||
' cross-restart stability, set EVOMAP_DEVICE_ID as an env var' +
|
||||
' or mount a persistent volume at ~/.evomap/'
|
||||
);
|
||||
}
|
||||
|
||||
return _cachedDeviceId;
|
||||
}
|
||||
|
||||
module.exports = { getDeviceId, isContainer };
|
||||
60
skills/capability-evolver/src/gep/envFingerprint.js
Normal file
60
skills/capability-evolver/src/gep/envFingerprint.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Environment fingerprint capture for GEP assets.
|
||||
// Records the runtime environment so that cross-environment diffusion
|
||||
// success rates (GDI) can be measured scientifically.
|
||||
|
||||
const os = require('os');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { getRepoRoot } = require('./paths');
|
||||
const { getDeviceId, isContainer } = require('./deviceId');
|
||||
|
||||
// Capture a structured environment fingerprint.
|
||||
// This is embedded into Capsules, EvolutionEvents, and ValidationReports.
|
||||
function captureEnvFingerprint() {
|
||||
const repoRoot = getRepoRoot();
|
||||
let pkgVersion = null;
|
||||
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 {
|
||||
device_id: getDeviceId(),
|
||||
node_version: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
os_release: os.release(),
|
||||
hostname: os.hostname(),
|
||||
evolver_version: pkgVersion,
|
||||
cwd: process.cwd(),
|
||||
container: isContainer(),
|
||||
};
|
||||
}
|
||||
|
||||
// Compute a short fingerprint key for comparison and grouping.
|
||||
// Two nodes with the same key are considered "same environment class".
|
||||
function envFingerprintKey(fp) {
|
||||
if (!fp || typeof fp !== 'object') return 'unknown';
|
||||
const parts = [
|
||||
fp.device_id || '',
|
||||
fp.node_version || '',
|
||||
fp.platform || '',
|
||||
fp.arch || '',
|
||||
fp.hostname || '',
|
||||
fp.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,
|
||||
};
|
||||
133
skills/capability-evolver/src/gep/hubSearch.js
Normal file
133
skills/capability-evolver/src/gep/hubSearch.js
Normal file
@@ -0,0 +1,133 @@
|
||||
// Hub Search-First Evolution: query evomap-hub for reusable solutions before local solve.
|
||||
//
|
||||
// Flow: extractSignals() -> hubSearch(signals) -> if hit: reuse; if miss: normal evolve
|
||||
// Two modes: direct (skip local reasoning) | reference (inject into prompt as strong hint)
|
||||
|
||||
const { getNodeId } = require('./a2aProtocol');
|
||||
|
||||
const DEFAULT_MIN_REUSE_SCORE = 0.72;
|
||||
const DEFAULT_REUSE_MODE = 'reference'; // 'direct' | 'reference'
|
||||
|
||||
function getHubUrl() {
|
||||
return (process.env.A2A_HUB_URL || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function getReuseMode() {
|
||||
const m = String(process.env.EVOLVER_REUSE_MODE || DEFAULT_REUSE_MODE).toLowerCase();
|
||||
return m === 'direct' ? 'direct' : 'reference';
|
||||
}
|
||||
|
||||
function getMinReuseScore() {
|
||||
const n = Number(process.env.EVOLVER_MIN_REUSE_SCORE);
|
||||
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MIN_REUSE_SCORE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score a hub asset for local reuse quality.
|
||||
* rank = confidence * max(success_streak, 1) * (reputation / 100)
|
||||
*/
|
||||
function scoreHubResult(asset) {
|
||||
const confidence = Number(asset.confidence) || 0;
|
||||
const streak = Math.max(Number(asset.success_streak) || 0, 1);
|
||||
// Reputation is included in asset from hub ranked endpoint; default 50 if missing
|
||||
const reputation = Number(asset.reputation_score) || 50;
|
||||
return confidence * streak * (reputation / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the best matching asset above the threshold.
|
||||
* Returns { match, score, mode } or null if nothing qualifies.
|
||||
*/
|
||||
function pickBestMatch(results, threshold) {
|
||||
if (!Array.isArray(results) || results.length === 0) return null;
|
||||
|
||||
let best = null;
|
||||
let bestScore = 0;
|
||||
|
||||
for (const asset of results) {
|
||||
// Only consider promoted assets
|
||||
if (asset.status && asset.status !== 'promoted') continue;
|
||||
const s = scoreHubResult(asset);
|
||||
if (s > bestScore) {
|
||||
bestScore = s;
|
||||
best = asset;
|
||||
}
|
||||
}
|
||||
|
||||
if (!best || bestScore < threshold) return null;
|
||||
|
||||
return {
|
||||
match: best,
|
||||
score: Math.round(bestScore * 1000) / 1000,
|
||||
mode: getReuseMode(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the hub for reusable capsules matching the given signals.
|
||||
* Returns { hit: true, match, score, mode } or { hit: false }.
|
||||
*/
|
||||
async function hubSearch(signals, opts) {
|
||||
const hubUrl = getHubUrl();
|
||||
if (!hubUrl) return { hit: false, reason: 'no_hub_url' };
|
||||
|
||||
const signalList = Array.isArray(signals) ? signals.filter(Boolean) : [];
|
||||
if (signalList.length === 0) return { hit: false, reason: 'no_signals' };
|
||||
|
||||
const threshold = (opts && Number.isFinite(opts.threshold)) ? opts.threshold : getMinReuseScore();
|
||||
const limit = (opts && Number.isFinite(opts.limit)) ? opts.limit : 5;
|
||||
const timeout = (opts && Number.isFinite(opts.timeoutMs)) ? opts.timeoutMs : 8000;
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.set('signals', signalList.join(','));
|
||||
params.set('status', 'promoted');
|
||||
params.set('limit', String(limit));
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
const url = `${hubUrl}/a2a/assets/search?${params.toString()}`;
|
||||
const res = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!res.ok) return { hit: false, reason: `hub_http_${res.status}` };
|
||||
|
||||
const data = await res.json();
|
||||
const assets = Array.isArray(data.assets) ? data.assets : [];
|
||||
|
||||
if (assets.length === 0) return { hit: false, reason: 'no_results' };
|
||||
|
||||
const pick = pickBestMatch(assets, threshold);
|
||||
if (!pick) return { hit: false, reason: 'below_threshold', candidates: assets.length };
|
||||
|
||||
console.log(`[HubSearch] Hit: ${pick.match.asset_id || pick.match.local_id} (score=${pick.score}, mode=${pick.mode})`);
|
||||
|
||||
return {
|
||||
hit: true,
|
||||
match: pick.match,
|
||||
score: pick.score,
|
||||
mode: pick.mode,
|
||||
asset_id: pick.match.asset_id || null,
|
||||
source_node_id: pick.match.source_node_id || null,
|
||||
chain_id: pick.match.chain_id || null,
|
||||
};
|
||||
} catch (err) {
|
||||
// Hub unreachable is non-fatal; fall through to normal evolve
|
||||
console.log(`[HubSearch] Failed (non-fatal): ${err.message}`);
|
||||
return { hit: false, reason: 'fetch_error', error: err.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hubSearch,
|
||||
scoreHubResult,
|
||||
pickBestMatch,
|
||||
getReuseMode,
|
||||
getMinReuseScore,
|
||||
getHubUrl,
|
||||
};
|
||||
771
skills/capability-evolver/src/gep/memoryGraph.js
Normal file
771
skills/capability-evolver/src/gep/memoryGraph.js
Normal file
@@ -0,0 +1,771 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getMemoryDir } = require('./paths');
|
||||
const { normalizePersonalityState, isValidPersonalityState, personalityKey } = require('./personality');
|
||||
const { isValidMutation, normalizeMutation } = require('./mutation');
|
||||
|
||||
function ensureDir(dir) {
|
||||
try {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function stableHash(input) {
|
||||
const s = String(input || '');
|
||||
let h = 2166136261;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 16777619);
|
||||
}
|
||||
return (h >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function normalizeErrorSignature(text) {
|
||||
const s = String(text || '').trim();
|
||||
if (!s) return null;
|
||||
return (
|
||||
s
|
||||
.toLowerCase()
|
||||
// normalize Windows paths
|
||||
.replace(/[a-z]:\\[^ \n\r\t]+/gi, '<path>')
|
||||
// normalize Unix paths
|
||||
.replace(/\/[^ \n\r\t]+/g, '<path>')
|
||||
// normalize hex and numbers
|
||||
.replace(/\b0x[0-9a-f]+\b/gi, '<hex>')
|
||||
.replace(/\b\d+\b/g, '<n>')
|
||||
// normalize whitespace
|
||||
.replace(/\s+/g, ' ')
|
||||
.slice(0, 220)
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSignalsForMatching(signals) {
|
||||
const list = Array.isArray(signals) ? signals : [];
|
||||
const out = [];
|
||||
for (const s of list) {
|
||||
const str = String(s || '').trim();
|
||||
if (!str) continue;
|
||||
if (str.startsWith('errsig:')) {
|
||||
const norm = normalizeErrorSignature(str.slice('errsig:'.length));
|
||||
if (norm) out.push(`errsig_norm:${stableHash(norm)}`);
|
||||
continue;
|
||||
}
|
||||
out.push(str);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function computeSignalKey(signals) {
|
||||
// Key must be stable across runs; normalize noisy signatures (paths, numbers).
|
||||
const list = normalizeSignalsForMatching(signals);
|
||||
const uniq = Array.from(new Set(list.filter(Boolean))).sort();
|
||||
return uniq.join('|') || '(none)';
|
||||
}
|
||||
|
||||
function extractErrorSignatureFromSignals(signals) {
|
||||
// Convention: signals can include "errsig:<raw>" emitted by signals extractor.
|
||||
const list = Array.isArray(signals) ? signals : [];
|
||||
for (const s of list) {
|
||||
const str = String(s || '');
|
||||
if (str.startsWith('errsig:')) return normalizeErrorSignature(str.slice('errsig:'.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function memoryGraphPath() {
|
||||
const { getEvolutionDir } = require('./paths');
|
||||
const evoDir = getEvolutionDir();
|
||||
return process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
|
||||
}
|
||||
|
||||
function memoryGraphStatePath() {
|
||||
const { getEvolutionDir } = require('./paths');
|
||||
return path.join(getEvolutionDir(), 'memory_graph_state.json');
|
||||
}
|
||||
|
||||
function appendJsonl(filePath, obj) {
|
||||
const dir = path.dirname(filePath);
|
||||
ensureDir(dir);
|
||||
fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8');
|
||||
}
|
||||
|
||||
function readJsonIfExists(filePath, fallback) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) return fallback;
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
if (!raw.trim()) return fallback;
|
||||
return JSON.parse(raw);
|
||||
} catch (e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJsonAtomic(filePath, obj) {
|
||||
const dir = path.dirname(filePath);
|
||||
ensureDir(dir);
|
||||
const tmp = `${filePath}.tmp`;
|
||||
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(tmp, filePath);
|
||||
}
|
||||
|
||||
function tryReadMemoryGraphEvents(limitLines = 2000) {
|
||||
try {
|
||||
const p = memoryGraphPath();
|
||||
if (!fs.existsSync(p)) return [];
|
||||
const raw = fs.readFileSync(p, 'utf8');
|
||||
const lines = raw
|
||||
.split('\n')
|
||||
.map(l => l.trim())
|
||||
.filter(Boolean);
|
||||
const recent = lines.slice(Math.max(0, lines.length - limitLines));
|
||||
return recent
|
||||
.map(l => {
|
||||
try {
|
||||
return JSON.parse(l);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function jaccard(aList, bList) {
|
||||
const aNorm = normalizeSignalsForMatching(aList);
|
||||
const bNorm = normalizeSignalsForMatching(bList);
|
||||
const a = new Set((Array.isArray(aNorm) ? aNorm : []).map(String));
|
||||
const b = new Set((Array.isArray(bNorm) ? bNorm : []).map(String));
|
||||
if (a.size === 0 && b.size === 0) return 1;
|
||||
if (a.size === 0 || b.size === 0) return 0;
|
||||
let inter = 0;
|
||||
for (const x of a) if (b.has(x)) inter++;
|
||||
const union = a.size + b.size - inter;
|
||||
return union === 0 ? 0 : inter / union;
|
||||
}
|
||||
|
||||
function decayWeight(updatedAtIso, halfLifeDays) {
|
||||
const hl = Number(halfLifeDays);
|
||||
if (!Number.isFinite(hl) || hl <= 0) return 1;
|
||||
const t = Date.parse(updatedAtIso);
|
||||
if (!Number.isFinite(t)) return 1;
|
||||
const ageDays = (Date.now() - t) / (1000 * 60 * 60 * 24);
|
||||
if (!Number.isFinite(ageDays) || ageDays <= 0) return 1;
|
||||
// Exponential half-life decay: weight = 0.5^(age/hl)
|
||||
return Math.pow(0.5, ageDays / hl);
|
||||
}
|
||||
|
||||
function aggregateEdges(events) {
|
||||
// Aggregate by (signal_key, gene_id) from outcome events.
|
||||
// Laplace smoothing to avoid 0/1 extremes.
|
||||
const map = new Map();
|
||||
for (const ev of events) {
|
||||
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
|
||||
if (ev.kind !== 'outcome') continue;
|
||||
const signalKey = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
|
||||
const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
|
||||
if (!geneId) continue;
|
||||
|
||||
const k = `${signalKey}::${geneId}`;
|
||||
const cur = map.get(k) || { signalKey, geneId, success: 0, fail: 0, last_ts: null, last_score: null };
|
||||
const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
|
||||
if (status === 'success') cur.success += 1;
|
||||
else if (status === 'failed') cur.fail += 1;
|
||||
|
||||
const ts = ev.ts || ev.created_at || ev.at;
|
||||
if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
|
||||
cur.last_ts = ts;
|
||||
cur.last_score =
|
||||
ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
|
||||
}
|
||||
map.set(k, cur);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function aggregateGeneOutcomes(events) {
|
||||
// Aggregate by gene_id from outcome events (gene -> outcome success probability).
|
||||
const map = new Map();
|
||||
for (const ev of events) {
|
||||
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
|
||||
if (ev.kind !== 'outcome') continue;
|
||||
const geneId = ev.gene && ev.gene.id ? String(ev.gene.id) : null;
|
||||
if (!geneId) continue;
|
||||
const cur = map.get(geneId) || { geneId, success: 0, fail: 0, last_ts: null, last_score: null };
|
||||
const status = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
|
||||
if (status === 'success') cur.success += 1;
|
||||
else if (status === 'failed') cur.fail += 1;
|
||||
const ts = ev.ts || ev.created_at || ev.at;
|
||||
if (ts && (!cur.last_ts || Date.parse(ts) > Date.parse(cur.last_ts))) {
|
||||
cur.last_ts = ts;
|
||||
cur.last_score =
|
||||
ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? Number(ev.outcome.score) : cur.last_score;
|
||||
}
|
||||
map.set(geneId, cur);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function edgeExpectedSuccess(edge, opts) {
|
||||
const e = edge || { success: 0, fail: 0, last_ts: null };
|
||||
const succ = Number(e.success) || 0;
|
||||
const fail = Number(e.fail) || 0;
|
||||
const total = succ + fail;
|
||||
const p = (succ + 1) / (total + 2); // Laplace smoothing
|
||||
const halfLifeDays = opts && Number.isFinite(Number(opts.half_life_days)) ? Number(opts.half_life_days) : 30;
|
||||
const w = decayWeight(e.last_ts || '', halfLifeDays);
|
||||
return { p, w, total, value: p * w };
|
||||
}
|
||||
|
||||
function getMemoryAdvice({ signals, genes, driftEnabled }) {
|
||||
const events = tryReadMemoryGraphEvents(2000);
|
||||
const edges = aggregateEdges(events);
|
||||
const geneOutcomes = aggregateGeneOutcomes(events);
|
||||
const curSignals = Array.isArray(signals) ? signals : [];
|
||||
const curKey = computeSignalKey(curSignals);
|
||||
|
||||
const bannedGeneIds = new Set();
|
||||
const scoredGeneIds = [];
|
||||
|
||||
// Similarity: consider exact key first, then any key with overlap.
|
||||
const seenKeys = new Set();
|
||||
const candidateKeys = [];
|
||||
candidateKeys.push({ key: curKey, sim: 1 });
|
||||
seenKeys.add(curKey);
|
||||
|
||||
for (const ev of events) {
|
||||
if (!ev || ev.type !== 'MemoryGraphEvent') continue;
|
||||
const k = ev.signal && ev.signal.key ? String(ev.signal.key) : '(none)';
|
||||
if (seenKeys.has(k)) continue;
|
||||
const sigs = ev.signal && Array.isArray(ev.signal.signals) ? ev.signal.signals : [];
|
||||
const sim = jaccard(curSignals, sigs);
|
||||
if (sim >= 0.34) {
|
||||
candidateKeys.push({ key: k, sim });
|
||||
seenKeys.add(k);
|
||||
}
|
||||
}
|
||||
|
||||
const byGene = new Map();
|
||||
for (const ck of candidateKeys) {
|
||||
for (const g of Array.isArray(genes) ? genes : []) {
|
||||
if (!g || g.type !== 'Gene' || !g.id) continue;
|
||||
const k = `${ck.key}::${g.id}`;
|
||||
const edge = edges.get(k);
|
||||
const cur = byGene.get(g.id) || { geneId: g.id, best: 0, attempts: 0, prior: 0, prior_attempts: 0 };
|
||||
|
||||
// Signal->Gene edge score (if available)
|
||||
if (edge) {
|
||||
const ex = edgeExpectedSuccess(edge, { half_life_days: 30 });
|
||||
const weighted = ex.value * ck.sim;
|
||||
if (weighted > cur.best) cur.best = weighted;
|
||||
cur.attempts = Math.max(cur.attempts, ex.total);
|
||||
}
|
||||
|
||||
// Gene->Outcome prior (independent of signal): stabilizer when signal edges are sparse.
|
||||
const gEdge = geneOutcomes.get(String(g.id));
|
||||
if (gEdge) {
|
||||
const gx = edgeExpectedSuccess(gEdge, { half_life_days: 45 });
|
||||
cur.prior = Math.max(cur.prior, gx.value);
|
||||
cur.prior_attempts = Math.max(cur.prior_attempts, gx.total);
|
||||
}
|
||||
|
||||
byGene.set(g.id, cur);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [geneId, info] of byGene.entries()) {
|
||||
const combined = info.best > 0 ? info.best + info.prior * 0.12 : info.prior * 0.4;
|
||||
scoredGeneIds.push({ geneId, score: combined, attempts: info.attempts, prior: info.prior });
|
||||
// Low-efficiency path suppression (unless drift is explicit).
|
||||
if (!driftEnabled && info.attempts >= 2 && info.best < 0.18) {
|
||||
bannedGeneIds.add(geneId);
|
||||
}
|
||||
// Also suppress genes with consistently poor global outcomes when signal edges are sparse.
|
||||
if (!driftEnabled && info.attempts < 2 && info.prior_attempts >= 3 && info.prior < 0.12) {
|
||||
bannedGeneIds.add(geneId);
|
||||
}
|
||||
}
|
||||
|
||||
scoredGeneIds.sort((a, b) => b.score - a.score);
|
||||
const preferredGeneId = scoredGeneIds.length ? scoredGeneIds[0].geneId : null;
|
||||
|
||||
const explanation = [];
|
||||
if (preferredGeneId) explanation.push(`memory_prefer:${preferredGeneId}`);
|
||||
if (bannedGeneIds.size) explanation.push(`memory_ban:${Array.from(bannedGeneIds).slice(0, 6).join(',')}`);
|
||||
if (preferredGeneId) {
|
||||
const top = scoredGeneIds.find(x => x && x.geneId === preferredGeneId);
|
||||
if (top && Number.isFinite(Number(top.prior)) && top.prior > 0) explanation.push(`gene_prior:${top.prior.toFixed(3)}`);
|
||||
}
|
||||
if (driftEnabled) explanation.push('random_drift:enabled');
|
||||
|
||||
return {
|
||||
currentSignalKey: curKey,
|
||||
preferredGeneId,
|
||||
bannedGeneIds,
|
||||
explanation,
|
||||
};
|
||||
}
|
||||
|
||||
function recordSignalSnapshot({ signals, observations }) {
|
||||
const signalKey = computeSignalKey(signals);
|
||||
const ts = nowIso();
|
||||
const errsig = extractErrorSignatureFromSignals(signals);
|
||||
const ev = {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'signal',
|
||||
id: `mge_${Date.now()}_${stableHash(`${signalKey}|signal|${ts}`)}`,
|
||||
ts,
|
||||
signal: {
|
||||
key: signalKey,
|
||||
signals: Array.isArray(signals) ? signals : [],
|
||||
error_signature: errsig || null,
|
||||
},
|
||||
observed: observations && typeof observations === 'object' ? observations : null,
|
||||
};
|
||||
appendJsonl(memoryGraphPath(), ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
function buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }) {
|
||||
const sigCount = Array.isArray(signals) ? signals.length : 0;
|
||||
const drift = driftEnabled ? 'drift' : 'directed';
|
||||
const g = geneId ? `${geneId}${geneCategory ? `(${geneCategory})` : ''}` : '(none)';
|
||||
return `Given signal_key=${signalKey} with ${sigCount} signals, selecting gene=${g} under mode=${drift} is expected to reduce repeated errors and improve stability.`;
|
||||
}
|
||||
|
||||
function recordHypothesis({
|
||||
signals,
|
||||
mutation,
|
||||
personality_state,
|
||||
selectedGene,
|
||||
selector,
|
||||
driftEnabled,
|
||||
selectedBy,
|
||||
capsulesUsed,
|
||||
observations,
|
||||
}) {
|
||||
const signalKey = computeSignalKey(signals);
|
||||
const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
|
||||
const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
|
||||
const ts = nowIso();
|
||||
const errsig = extractErrorSignatureFromSignals(signals);
|
||||
const hypothesisId = `hyp_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
|
||||
const personalityState = personality_state || null;
|
||||
const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
|
||||
const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
|
||||
const ev = {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'hypothesis',
|
||||
id: `mge_${Date.now()}_${stableHash(`${hypothesisId}|${ts}`)}`,
|
||||
ts,
|
||||
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
|
||||
hypothesis: {
|
||||
id: hypothesisId,
|
||||
text: buildHypothesisText({ signalKey, signals, geneId, geneCategory, driftEnabled }),
|
||||
predicted_outcome: { status: null, score: null },
|
||||
},
|
||||
mutation: mutNorm
|
||||
? {
|
||||
id: mutNorm.id,
|
||||
category: mutNorm.category,
|
||||
trigger_signals: mutNorm.trigger_signals,
|
||||
target: mutNorm.target,
|
||||
expected_effect: mutNorm.expected_effect,
|
||||
risk_level: mutNorm.risk_level,
|
||||
}
|
||||
: null,
|
||||
personality: psNorm
|
||||
? {
|
||||
key: personalityKey(psNorm),
|
||||
state: psNorm,
|
||||
}
|
||||
: null,
|
||||
gene: { id: geneId, category: geneCategory },
|
||||
action: {
|
||||
drift: !!driftEnabled,
|
||||
selected_by: selectedBy || 'selector',
|
||||
selector: selector || null,
|
||||
},
|
||||
capsules: {
|
||||
used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
|
||||
},
|
||||
observed: observations && typeof observations === 'object' ? observations : null,
|
||||
};
|
||||
appendJsonl(memoryGraphPath(), ev);
|
||||
return { hypothesisId, signalKey };
|
||||
}
|
||||
|
||||
function hasErrorSignal(signals) {
|
||||
const list = Array.isArray(signals) ? signals : [];
|
||||
return list.includes('log_error');
|
||||
}
|
||||
|
||||
function recordAttempt({
|
||||
signals,
|
||||
mutation,
|
||||
personality_state,
|
||||
selectedGene,
|
||||
selector,
|
||||
driftEnabled,
|
||||
selectedBy,
|
||||
hypothesisId,
|
||||
capsulesUsed,
|
||||
observations,
|
||||
}) {
|
||||
const signalKey = computeSignalKey(signals);
|
||||
const geneId = selectedGene && selectedGene.id ? String(selectedGene.id) : null;
|
||||
const geneCategory = selectedGene && selectedGene.category ? String(selectedGene.category) : null;
|
||||
const ts = nowIso();
|
||||
const errsig = extractErrorSignatureFromSignals(signals);
|
||||
const actionId = `act_${Date.now()}_${stableHash(`${signalKey}|${geneId || 'none'}|${ts}`)}`;
|
||||
const personalityState = personality_state || null;
|
||||
const mutNorm = mutation && isValidMutation(mutation) ? normalizeMutation(mutation) : null;
|
||||
const psNorm = personalityState && isValidPersonalityState(personalityState) ? normalizePersonalityState(personalityState) : null;
|
||||
const ev = {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'attempt',
|
||||
id: `mge_${Date.now()}_${stableHash(actionId)}`,
|
||||
ts,
|
||||
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [], error_signature: errsig || null },
|
||||
mutation: mutNorm
|
||||
? {
|
||||
id: mutNorm.id,
|
||||
category: mutNorm.category,
|
||||
trigger_signals: mutNorm.trigger_signals,
|
||||
target: mutNorm.target,
|
||||
expected_effect: mutNorm.expected_effect,
|
||||
risk_level: mutNorm.risk_level,
|
||||
}
|
||||
: null,
|
||||
personality: psNorm
|
||||
? {
|
||||
key: personalityKey(psNorm),
|
||||
state: psNorm,
|
||||
}
|
||||
: null,
|
||||
gene: { id: geneId, category: geneCategory },
|
||||
hypothesis: hypothesisId ? { id: String(hypothesisId) } : null,
|
||||
action: {
|
||||
id: actionId,
|
||||
drift: !!driftEnabled,
|
||||
selected_by: selectedBy || 'selector',
|
||||
selector: selector || null,
|
||||
},
|
||||
capsules: {
|
||||
used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
|
||||
},
|
||||
observed: observations && typeof observations === 'object' ? observations : null,
|
||||
};
|
||||
|
||||
appendJsonl(memoryGraphPath(), ev);
|
||||
|
||||
// State is mutable; graph is append-only.
|
||||
const statePath = memoryGraphStatePath();
|
||||
const state = readJsonIfExists(statePath, { last_action: null });
|
||||
state.last_action = {
|
||||
action_id: actionId,
|
||||
signal_key: signalKey,
|
||||
signals: Array.isArray(signals) ? signals : [],
|
||||
mutation_id: mutNorm ? mutNorm.id : null,
|
||||
mutation_category: mutNorm ? mutNorm.category : null,
|
||||
mutation_risk_level: mutNorm ? mutNorm.risk_level : null,
|
||||
personality_key: psNorm ? personalityKey(psNorm) : null,
|
||||
personality_state: psNorm || null,
|
||||
gene_id: geneId,
|
||||
gene_category: geneCategory,
|
||||
hypothesis_id: hypothesisId ? String(hypothesisId) : null,
|
||||
capsules_used: Array.isArray(capsulesUsed) ? capsulesUsed.map(String).filter(Boolean) : [],
|
||||
had_error: hasErrorSignal(signals),
|
||||
created_at: ts,
|
||||
outcome_recorded: false,
|
||||
baseline_observed: observations && typeof observations === 'object' ? observations : null,
|
||||
};
|
||||
writeJsonAtomic(statePath, state);
|
||||
|
||||
return { actionId, signalKey };
|
||||
}
|
||||
|
||||
function inferOutcomeFromSignals({ prevHadError, currentHasError }) {
|
||||
if (prevHadError && !currentHasError) return { status: 'success', score: 0.85, note: 'error_cleared' };
|
||||
if (prevHadError && currentHasError) return { status: 'failed', score: 0.2, note: 'error_persisted' };
|
||||
if (!prevHadError && currentHasError) return { status: 'failed', score: 0.15, note: 'new_error_appeared' };
|
||||
return { status: 'success', score: 0.6, note: 'stable_no_error' };
|
||||
}
|
||||
|
||||
function clamp01(x) {
|
||||
const n = Number(x);
|
||||
if (!Number.isFinite(n)) return 0;
|
||||
return Math.max(0, Math.min(1, n));
|
||||
}
|
||||
|
||||
function tryParseLastEvolutionEventOutcome(evidenceText) {
|
||||
// Scan tail text for an EvolutionEvent JSON line and extract its outcome.
|
||||
const s = String(evidenceText || '');
|
||||
if (!s) return null;
|
||||
const lines = s.split('\n').slice(-400);
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
if (!line) continue;
|
||||
if (!line.includes('"type"') || !line.includes('EvolutionEvent')) continue;
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (!obj || obj.type !== 'EvolutionEvent') continue;
|
||||
const o = obj.outcome && typeof obj.outcome === 'object' ? obj.outcome : null;
|
||||
if (!o) continue;
|
||||
const status = o.status === 'success' || o.status === 'failed' ? o.status : null;
|
||||
const score = Number.isFinite(Number(o.score)) ? clamp01(Number(o.score)) : null;
|
||||
if (!status && score == null) continue;
|
||||
return {
|
||||
status: status || (score != null && score >= 0.5 ? 'success' : 'failed'),
|
||||
score: score != null ? score : status === 'success' ? 0.75 : 0.25,
|
||||
note: 'evolutionevent_observed',
|
||||
};
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function inferOutcomeEnhanced({ prevHadError, currentHasError, baselineObserved, currentObserved }) {
|
||||
const evidence =
|
||||
currentObserved &&
|
||||
currentObserved.evidence &&
|
||||
(currentObserved.evidence.recent_session_tail || currentObserved.evidence.today_log_tail)
|
||||
? currentObserved.evidence
|
||||
: null;
|
||||
const combinedEvidence = evidence
|
||||
? `${String(evidence.recent_session_tail || '')}\n${String(evidence.today_log_tail || '')}`
|
||||
: '';
|
||||
const observed = tryParseLastEvolutionEventOutcome(combinedEvidence);
|
||||
if (observed) return observed;
|
||||
|
||||
const base = inferOutcomeFromSignals({ prevHadError, currentHasError });
|
||||
|
||||
const prevErrCount =
|
||||
baselineObserved && Number.isFinite(Number(baselineObserved.recent_error_count))
|
||||
? Number(baselineObserved.recent_error_count)
|
||||
: null;
|
||||
const curErrCount =
|
||||
currentObserved && Number.isFinite(Number(currentObserved.recent_error_count))
|
||||
? Number(currentObserved.recent_error_count)
|
||||
: null;
|
||||
|
||||
let score = base.score;
|
||||
if (prevErrCount != null && curErrCount != null) {
|
||||
const delta = prevErrCount - curErrCount;
|
||||
score += Math.max(-0.12, Math.min(0.12, delta / 50));
|
||||
}
|
||||
|
||||
const prevScan =
|
||||
baselineObserved && Number.isFinite(Number(baselineObserved.scan_ms)) ? Number(baselineObserved.scan_ms) : null;
|
||||
const curScan =
|
||||
currentObserved && Number.isFinite(Number(currentObserved.scan_ms)) ? Number(currentObserved.scan_ms) : null;
|
||||
if (prevScan != null && curScan != null && prevScan > 0) {
|
||||
const ratio = (prevScan - curScan) / prevScan;
|
||||
score += Math.max(-0.06, Math.min(0.06, ratio));
|
||||
}
|
||||
|
||||
return { status: base.status, score: clamp01(score), note: `${base.note}|heuristic_delta` };
|
||||
}
|
||||
|
||||
function buildConfidenceEdgeEvent({ signalKey, signals, geneId, geneCategory, outcomeEventId, halfLifeDays }) {
|
||||
const events = tryReadMemoryGraphEvents(2000);
|
||||
const edges = aggregateEdges(events);
|
||||
const k = `${signalKey}::${geneId}`;
|
||||
const edge = edges.get(k) || { success: 0, fail: 0, last_ts: null };
|
||||
const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
|
||||
const ts = nowIso();
|
||||
return {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'confidence_edge',
|
||||
id: `mge_${Date.now()}_${stableHash(`${signalKey}|${geneId}|confidence|${ts}`)}`,
|
||||
ts,
|
||||
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
|
||||
gene: { id: geneId, category: geneCategory || null },
|
||||
edge: { signal_key: signalKey, gene_id: geneId },
|
||||
stats: {
|
||||
success: Number(edge.success) || 0,
|
||||
fail: Number(edge.fail) || 0,
|
||||
attempts: Number(ex.total) || 0,
|
||||
p: ex.p,
|
||||
decay_weight: ex.w,
|
||||
value: ex.value,
|
||||
half_life_days: halfLifeDays,
|
||||
updated_at: ts,
|
||||
},
|
||||
derived_from: { outcome_event_id: outcomeEventId || null },
|
||||
};
|
||||
}
|
||||
|
||||
function buildGeneOutcomeConfidenceEvent({ geneId, geneCategory, outcomeEventId, halfLifeDays }) {
|
||||
const events = tryReadMemoryGraphEvents(2000);
|
||||
const geneOutcomes = aggregateGeneOutcomes(events);
|
||||
const edge = geneOutcomes.get(String(geneId)) || { success: 0, fail: 0, last_ts: null };
|
||||
const ex = edgeExpectedSuccess(edge, { half_life_days: halfLifeDays });
|
||||
const ts = nowIso();
|
||||
return {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'confidence_gene_outcome',
|
||||
id: `mge_${Date.now()}_${stableHash(`${geneId}|gene_outcome|confidence|${ts}`)}`,
|
||||
ts,
|
||||
gene: { id: String(geneId), category: geneCategory || null },
|
||||
edge: { gene_id: String(geneId) },
|
||||
stats: {
|
||||
success: Number(edge.success) || 0,
|
||||
fail: Number(edge.fail) || 0,
|
||||
attempts: Number(ex.total) || 0,
|
||||
p: ex.p,
|
||||
decay_weight: ex.w,
|
||||
value: ex.value,
|
||||
half_life_days: halfLifeDays,
|
||||
updated_at: ts,
|
||||
},
|
||||
derived_from: { outcome_event_id: outcomeEventId || null },
|
||||
};
|
||||
}
|
||||
|
||||
function recordOutcomeFromState({ signals, observations }) {
|
||||
const statePath = memoryGraphStatePath();
|
||||
const state = readJsonIfExists(statePath, { last_action: null });
|
||||
const last = state && state.last_action ? state.last_action : null;
|
||||
if (!last || !last.action_id) return null;
|
||||
if (last.outcome_recorded) return null;
|
||||
|
||||
const currentHasError = hasErrorSignal(signals);
|
||||
const inferred = inferOutcomeEnhanced({
|
||||
prevHadError: !!last.had_error,
|
||||
currentHasError,
|
||||
baselineObserved: last.baseline_observed || null,
|
||||
currentObserved: observations || null,
|
||||
});
|
||||
const ts = nowIso();
|
||||
const errsig = extractErrorSignatureFromSignals(signals);
|
||||
const ev = {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'outcome',
|
||||
id: `mge_${Date.now()}_${stableHash(`${last.action_id}|outcome|${ts}`)}`,
|
||||
ts,
|
||||
signal: {
|
||||
key: String(last.signal_key || '(none)'),
|
||||
signals: Array.isArray(last.signals) ? last.signals : [],
|
||||
error_signature: errsig || null,
|
||||
},
|
||||
mutation:
|
||||
last.mutation_id || last.mutation_category || last.mutation_risk_level
|
||||
? {
|
||||
id: last.mutation_id || null,
|
||||
category: last.mutation_category || null,
|
||||
risk_level: last.mutation_risk_level || null,
|
||||
}
|
||||
: null,
|
||||
personality:
|
||||
last.personality_key || last.personality_state
|
||||
? {
|
||||
key: last.personality_key || null,
|
||||
state: last.personality_state || null,
|
||||
}
|
||||
: null,
|
||||
gene: { id: last.gene_id || null, category: last.gene_category || null },
|
||||
action: { id: String(last.action_id) },
|
||||
hypothesis: last.hypothesis_id ? { id: String(last.hypothesis_id) } : null,
|
||||
outcome: {
|
||||
status: inferred.status,
|
||||
score: inferred.score,
|
||||
note: inferred.note,
|
||||
observed: { current_signals: Array.isArray(signals) ? signals : [] },
|
||||
},
|
||||
confidence: {
|
||||
// This is an interpretable, decayed success estimate derived from outcomes; aggregation is computed at read-time.
|
||||
half_life_days: 30,
|
||||
},
|
||||
observed: observations && typeof observations === 'object' ? observations : null,
|
||||
baseline: last.baseline_observed || null,
|
||||
capsules: {
|
||||
used: Array.isArray(last.capsules_used) ? last.capsules_used : [],
|
||||
},
|
||||
};
|
||||
|
||||
appendJsonl(memoryGraphPath(), ev);
|
||||
|
||||
// Persist explicit confidence snapshots (append-only) for auditability.
|
||||
try {
|
||||
if (last.gene_id) {
|
||||
const edgeEv = buildConfidenceEdgeEvent({
|
||||
signalKey: String(last.signal_key || '(none)'),
|
||||
signals: Array.isArray(last.signals) ? last.signals : [],
|
||||
geneId: String(last.gene_id),
|
||||
geneCategory: last.gene_category || null,
|
||||
outcomeEventId: ev.id,
|
||||
halfLifeDays: 30,
|
||||
});
|
||||
appendJsonl(memoryGraphPath(), edgeEv);
|
||||
|
||||
const geneEv = buildGeneOutcomeConfidenceEvent({
|
||||
geneId: String(last.gene_id),
|
||||
geneCategory: last.gene_category || null,
|
||||
outcomeEventId: ev.id,
|
||||
halfLifeDays: 45,
|
||||
});
|
||||
appendJsonl(memoryGraphPath(), geneEv);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
last.outcome_recorded = true;
|
||||
last.outcome_recorded_at = ts;
|
||||
state.last_action = last;
|
||||
writeJsonAtomic(statePath, state);
|
||||
|
||||
return ev;
|
||||
}
|
||||
|
||||
function recordExternalCandidate({ asset, source, signals }) {
|
||||
// Append-only annotation: external assets enter as candidates only.
|
||||
// This does not affect outcome aggregation (which only uses kind === 'outcome').
|
||||
const a = asset && typeof asset === 'object' ? asset : null;
|
||||
const type = a && a.type ? String(a.type) : null;
|
||||
const id = a && a.id ? String(a.id) : null;
|
||||
if (!type || !id) return null;
|
||||
|
||||
const ts = nowIso();
|
||||
const signalKey = computeSignalKey(signals);
|
||||
const ev = {
|
||||
type: 'MemoryGraphEvent',
|
||||
kind: 'external_candidate',
|
||||
id: `mge_${Date.now()}_${stableHash(`${type}|${id}|external|${ts}`)}`,
|
||||
ts,
|
||||
signal: { key: signalKey, signals: Array.isArray(signals) ? signals : [] },
|
||||
external: {
|
||||
source: source || 'external',
|
||||
received_at: ts,
|
||||
},
|
||||
asset: { type, id },
|
||||
candidate: {
|
||||
// Minimal hints for later local triggering/validation.
|
||||
trigger: type === 'Capsule' && Array.isArray(a.trigger) ? a.trigger : [],
|
||||
gene: type === 'Capsule' && a.gene ? String(a.gene) : null,
|
||||
confidence: type === 'Capsule' && Number.isFinite(Number(a.confidence)) ? Number(a.confidence) : null,
|
||||
},
|
||||
};
|
||||
|
||||
appendJsonl(memoryGraphPath(), ev);
|
||||
return ev;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
memoryGraphPath,
|
||||
computeSignalKey,
|
||||
tryReadMemoryGraphEvents,
|
||||
getMemoryAdvice,
|
||||
recordSignalSnapshot,
|
||||
recordHypothesis,
|
||||
recordAttempt,
|
||||
recordOutcomeFromState,
|
||||
recordExternalCandidate,
|
||||
};
|
||||
|
||||
203
skills/capability-evolver/src/gep/memoryGraphAdapter.js
Normal file
203
skills/capability-evolver/src/gep/memoryGraphAdapter.js
Normal file
@@ -0,0 +1,203 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// MemoryGraphAdapter -- stable interface boundary for memory graph operations.
|
||||
//
|
||||
// Default implementation delegates to the local JSONL-based memoryGraph.js.
|
||||
// SaaS providers can supply a remote adapter by setting MEMORY_GRAPH_PROVIDER=remote
|
||||
// and configuring MEMORY_GRAPH_REMOTE_URL / MEMORY_GRAPH_REMOTE_KEY.
|
||||
//
|
||||
// The adapter is designed so that the open-source evolver always works offline
|
||||
// with the local implementation. Remote is optional and degrades gracefully.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const localGraph = require('./memoryGraph');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Adapter interface contract (all methods must be implemented by providers):
|
||||
//
|
||||
// getAdvice({ signals, genes, driftEnabled }) => { preferredGeneId, bannedGeneIds, currentSignalKey, explanation }
|
||||
// recordSignalSnapshot({ signals, observations }) => event
|
||||
// recordHypothesis({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, capsulesUsed, observations }) => { hypothesisId, signalKey }
|
||||
// recordAttempt({ signals, mutation, personality_state, selectedGene, selector, driftEnabled, selectedBy, hypothesisId, capsulesUsed, observations }) => { actionId, signalKey }
|
||||
// recordOutcome({ signals, observations }) => event | null
|
||||
// recordExternalCandidate({ asset, source, signals }) => event | null
|
||||
// memoryGraphPath() => string
|
||||
// computeSignalKey(signals) => string
|
||||
// tryReadMemoryGraphEvents(limit) => event[]
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Local adapter (default) -- wraps memoryGraph.js without any behavior change
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const localAdapter = {
|
||||
name: 'local',
|
||||
|
||||
getAdvice(opts) {
|
||||
return localGraph.getMemoryAdvice(opts);
|
||||
},
|
||||
|
||||
recordSignalSnapshot(opts) {
|
||||
return localGraph.recordSignalSnapshot(opts);
|
||||
},
|
||||
|
||||
recordHypothesis(opts) {
|
||||
return localGraph.recordHypothesis(opts);
|
||||
},
|
||||
|
||||
recordAttempt(opts) {
|
||||
return localGraph.recordAttempt(opts);
|
||||
},
|
||||
|
||||
recordOutcome(opts) {
|
||||
return localGraph.recordOutcomeFromState(opts);
|
||||
},
|
||||
|
||||
recordExternalCandidate(opts) {
|
||||
return localGraph.recordExternalCandidate(opts);
|
||||
},
|
||||
|
||||
memoryGraphPath() {
|
||||
return localGraph.memoryGraphPath();
|
||||
},
|
||||
|
||||
computeSignalKey(signals) {
|
||||
return localGraph.computeSignalKey(signals);
|
||||
},
|
||||
|
||||
tryReadMemoryGraphEvents(limit) {
|
||||
return localGraph.tryReadMemoryGraphEvents(limit);
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Remote adapter (SaaS) -- calls external KG service with local fallback
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildRemoteAdapter() {
|
||||
const remoteUrl = process.env.MEMORY_GRAPH_REMOTE_URL || '';
|
||||
const remoteKey = process.env.MEMORY_GRAPH_REMOTE_KEY || '';
|
||||
const timeoutMs = Number(process.env.MEMORY_GRAPH_REMOTE_TIMEOUT_MS) || 5000;
|
||||
|
||||
async function remoteCall(endpoint, body) {
|
||||
if (!remoteUrl) throw new Error('MEMORY_GRAPH_REMOTE_URL not configured');
|
||||
const url = `${remoteUrl.replace(/\/+$/, '')}${endpoint}`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(remoteKey ? { Authorization: `Bearer ${remoteKey}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(`remote_kg_error: ${res.status}`);
|
||||
}
|
||||
return await res.json();
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap remote call with local fallback -- ensures offline resilience.
|
||||
function withFallback(localFn, remoteFn) {
|
||||
return async function (...args) {
|
||||
try {
|
||||
return await remoteFn(...args);
|
||||
} catch (e) {
|
||||
// Fallback to local on any remote failure (network, timeout, config).
|
||||
return localFn(...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'remote',
|
||||
|
||||
// getAdvice is the primary candidate for remote enhancement (richer graph reasoning).
|
||||
getAdvice: withFallback(
|
||||
(opts) => localGraph.getMemoryAdvice(opts),
|
||||
async (opts) => {
|
||||
const result = await remoteCall('/kg/advice', {
|
||||
signals: opts.signals,
|
||||
genes: (opts.genes || []).map((g) => ({ id: g.id, category: g.category, type: g.type })),
|
||||
driftEnabled: opts.driftEnabled,
|
||||
});
|
||||
// Normalize remote response to match local contract.
|
||||
return {
|
||||
currentSignalKey: result.currentSignalKey || localGraph.computeSignalKey(opts.signals),
|
||||
preferredGeneId: result.preferredGeneId || null,
|
||||
bannedGeneIds: new Set(result.bannedGeneIds || []),
|
||||
explanation: Array.isArray(result.explanation) ? result.explanation : [],
|
||||
};
|
||||
}
|
||||
),
|
||||
|
||||
// Write operations: always write locally first, then async-sync to remote.
|
||||
// This preserves the append-only local graph as source of truth.
|
||||
recordSignalSnapshot(opts) {
|
||||
const ev = localGraph.recordSignalSnapshot(opts);
|
||||
remoteCall('/kg/ingest', { kind: 'signal', event: ev }).catch(() => {});
|
||||
return ev;
|
||||
},
|
||||
|
||||
recordHypothesis(opts) {
|
||||
const result = localGraph.recordHypothesis(opts);
|
||||
remoteCall('/kg/ingest', { kind: 'hypothesis', event: result }).catch(() => {});
|
||||
return result;
|
||||
},
|
||||
|
||||
recordAttempt(opts) {
|
||||
const result = localGraph.recordAttempt(opts);
|
||||
remoteCall('/kg/ingest', { kind: 'attempt', event: result }).catch(() => {});
|
||||
return result;
|
||||
},
|
||||
|
||||
recordOutcome(opts) {
|
||||
const ev = localGraph.recordOutcomeFromState(opts);
|
||||
if (ev) {
|
||||
remoteCall('/kg/ingest', { kind: 'outcome', event: ev }).catch(() => {});
|
||||
}
|
||||
return ev;
|
||||
},
|
||||
|
||||
recordExternalCandidate(opts) {
|
||||
const ev = localGraph.recordExternalCandidate(opts);
|
||||
if (ev) {
|
||||
remoteCall('/kg/ingest', { kind: 'external_candidate', event: ev }).catch(() => {});
|
||||
}
|
||||
return ev;
|
||||
},
|
||||
|
||||
memoryGraphPath() {
|
||||
return localGraph.memoryGraphPath();
|
||||
},
|
||||
|
||||
computeSignalKey(signals) {
|
||||
return localGraph.computeSignalKey(signals);
|
||||
},
|
||||
|
||||
tryReadMemoryGraphEvents(limit) {
|
||||
return localGraph.tryReadMemoryGraphEvents(limit);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider resolution
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function resolveAdapter() {
|
||||
const provider = (process.env.MEMORY_GRAPH_PROVIDER || 'local').toLowerCase().trim();
|
||||
if (provider === 'remote') {
|
||||
return buildRemoteAdapter();
|
||||
}
|
||||
return localAdapter;
|
||||
}
|
||||
|
||||
const adapter = resolveAdapter();
|
||||
|
||||
module.exports = adapter;
|
||||
184
skills/capability-evolver/src/gep/mutation.js
Normal file
184
skills/capability-evolver/src/gep/mutation.js
Normal file
@@ -0,0 +1,184 @@
|
||||
function clamp01(x) {
|
||||
const n = Number(x);
|
||||
if (!Number.isFinite(n)) return 0;
|
||||
return Math.max(0, Math.min(1, n));
|
||||
}
|
||||
|
||||
function nowTsMs() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function uniqStrings(list) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (const x of Array.isArray(list) ? list : []) {
|
||||
const s = String(x || '').trim();
|
||||
if (!s) continue;
|
||||
if (seen.has(s)) continue;
|
||||
seen.add(s);
|
||||
out.push(s);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function hasErrorishSignal(signals) {
|
||||
const list = Array.isArray(signals) ? signals.map(s => String(s || '')) : [];
|
||||
if (list.includes('issue_already_resolved') || list.includes('openclaw_self_healed')) return false;
|
||||
if (list.includes('log_error')) return true;
|
||||
if (list.some(s => s.startsWith('errsig:') || s.startsWith('errsig_norm:'))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Opportunity signals that indicate a chance to innovate (not just fix).
|
||||
var OPPORTUNITY_SIGNALS = [
|
||||
'user_feature_request',
|
||||
'user_improvement_suggestion',
|
||||
'perf_bottleneck',
|
||||
'capability_gap',
|
||||
'stable_success_plateau',
|
||||
'external_opportunity',
|
||||
'issue_already_resolved',
|
||||
'openclaw_self_healed',
|
||||
'empty_cycle_loop_detected',
|
||||
];
|
||||
|
||||
function hasOpportunitySignal(signals) {
|
||||
var list = Array.isArray(signals) ? signals.map(function (s) { return String(s || ''); }) : [];
|
||||
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
|
||||
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';
|
||||
// Consult strategy preset: if the configured strategy favors innovation,
|
||||
// default to innovate instead of optimize when there is nothing specific to do.
|
||||
try {
|
||||
var strategy = require('./strategy').resolveStrategy();
|
||||
if (strategy && typeof strategy.innovate === 'number' && strategy.innovate >= 0.5) return 'innovate';
|
||||
} catch (_) {}
|
||||
return 'optimize';
|
||||
}
|
||||
|
||||
function expectedEffectFromCategory(category) {
|
||||
const c = String(category || '');
|
||||
if (c === 'repair') return 'reduce runtime errors, increase stability, and lower failure rate';
|
||||
if (c === 'optimize') return 'improve success rate and reduce repeated operational cost';
|
||||
if (c === 'innovate') return 'explore new strategy combinations to escape local optimum';
|
||||
return 'improve robustness and success probability';
|
||||
}
|
||||
|
||||
function targetFromGene(selectedGene) {
|
||||
if (selectedGene && selectedGene.id) return `gene:${String(selectedGene.id)}`;
|
||||
return 'behavior:protocol';
|
||||
}
|
||||
|
||||
function isHighRiskPersonality(p) {
|
||||
// Conservative definition: low rigor or high risk_tolerance is treated as high-risk personality.
|
||||
const rigor = p && Number.isFinite(Number(p.rigor)) ? Number(p.rigor) : null;
|
||||
const riskTol = p && Number.isFinite(Number(p.risk_tolerance)) ? Number(p.risk_tolerance) : null;
|
||||
if (rigor != null && rigor < 0.5) return true;
|
||||
if (riskTol != null && riskTol > 0.6) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isHighRiskMutationAllowed(personalityState) {
|
||||
const rigor = personalityState && Number.isFinite(Number(personalityState.rigor)) ? Number(personalityState.rigor) : 0;
|
||||
const riskTol =
|
||||
personalityState && Number.isFinite(Number(personalityState.risk_tolerance))
|
||||
? Number(personalityState.risk_tolerance)
|
||||
: 1;
|
||||
return rigor >= 0.6 && riskTol <= 0.5;
|
||||
}
|
||||
|
||||
function buildMutation({
|
||||
signals,
|
||||
selectedGene,
|
||||
driftEnabled,
|
||||
personalityState,
|
||||
allowHighRisk = false,
|
||||
target,
|
||||
expected_effect,
|
||||
} = {}) {
|
||||
const ts = nowTsMs();
|
||||
const category = mutationCategoryFromContext({ signals, driftEnabled: !!driftEnabled });
|
||||
const triggerSignals = uniqStrings(signals);
|
||||
|
||||
const base = {
|
||||
type: 'Mutation',
|
||||
id: `mut_${ts}`,
|
||||
category,
|
||||
trigger_signals: triggerSignals,
|
||||
target: String(target || targetFromGene(selectedGene)),
|
||||
expected_effect: String(expected_effect || expectedEffectFromCategory(category)),
|
||||
risk_level: 'low',
|
||||
};
|
||||
|
||||
// Default risk assignment: innovate is medium; others low.
|
||||
if (category === 'innovate') base.risk_level = 'medium';
|
||||
|
||||
// Optional high-risk escalation (rare, and guarded by strict safety constraints).
|
||||
if (allowHighRisk && category === 'innovate') {
|
||||
base.risk_level = 'high';
|
||||
}
|
||||
|
||||
// Safety constraints (hard):
|
||||
// - forbid innovate + high-risk personality (downgrade innovation to optimize)
|
||||
// - forbid high-risk mutation unless personality satisfies constraints
|
||||
const highRiskPersonality = isHighRiskPersonality(personalityState || null);
|
||||
if (base.category === 'innovate' && highRiskPersonality) {
|
||||
base.category = 'optimize';
|
||||
base.expected_effect = 'safety downgrade: optimize under high-risk personality (avoid innovate+high-risk combo)';
|
||||
base.risk_level = 'low';
|
||||
base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:avoid_innovate_with_high_risk_personality']);
|
||||
}
|
||||
|
||||
if (base.risk_level === 'high' && !isHighRiskMutationAllowed(personalityState || null)) {
|
||||
// Downgrade rather than emit illegal high-risk mutation.
|
||||
base.risk_level = 'medium';
|
||||
base.trigger_signals = uniqStrings([...(base.trigger_signals || []), 'safety:downgrade_high_risk']);
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
function isValidMutation(obj) {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
if (obj.type !== 'Mutation') return false;
|
||||
if (!obj.id || typeof obj.id !== 'string') return false;
|
||||
if (!obj.category || !['repair', 'optimize', 'innovate'].includes(String(obj.category))) return false;
|
||||
if (!Array.isArray(obj.trigger_signals)) return false;
|
||||
if (!obj.target || typeof obj.target !== 'string') return false;
|
||||
if (!obj.expected_effect || typeof obj.expected_effect !== 'string') return false;
|
||||
if (!obj.risk_level || !['low', 'medium', 'high'].includes(String(obj.risk_level))) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function normalizeMutation(obj) {
|
||||
const m = obj && typeof obj === 'object' ? obj : {};
|
||||
const out = {
|
||||
type: 'Mutation',
|
||||
id: typeof m.id === 'string' ? m.id : `mut_${nowTsMs()}`,
|
||||
category: ['repair', 'optimize', 'innovate'].includes(String(m.category)) ? String(m.category) : 'optimize',
|
||||
trigger_signals: uniqStrings(m.trigger_signals),
|
||||
target: typeof m.target === 'string' ? m.target : 'behavior:protocol',
|
||||
expected_effect: typeof m.expected_effect === 'string' ? m.expected_effect : expectedEffectFromCategory(m.category),
|
||||
risk_level: ['low', 'medium', 'high'].includes(String(m.risk_level)) ? String(m.risk_level) : 'low',
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clamp01,
|
||||
buildMutation,
|
||||
isValidMutation,
|
||||
normalizeMutation,
|
||||
isHighRiskMutationAllowed,
|
||||
isHighRiskPersonality,
|
||||
hasOpportunitySignal,
|
||||
};
|
||||
|
||||
67
skills/capability-evolver/src/gep/paths.js
Normal file
67
skills/capability-evolver/src/gep/paths.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const path = require('path');
|
||||
|
||||
function getRepoRoot() {
|
||||
// src/gep/paths.js -> repo root
|
||||
return path.resolve(__dirname, '..', '..');
|
||||
}
|
||||
|
||||
function getWorkspaceRoot() {
|
||||
// skills/evolver -> workspace root
|
||||
return path.resolve(getRepoRoot(), '..', '..');
|
||||
}
|
||||
|
||||
function getLogsDir() {
|
||||
return process.env.EVOLVER_LOGS_DIR || path.join(getWorkspaceRoot(), 'logs');
|
||||
}
|
||||
|
||||
function getMemoryDir() {
|
||||
return process.env.MEMORY_DIR || path.join(getWorkspaceRoot(), 'memory');
|
||||
}
|
||||
|
||||
// --- Session Scope Isolation ---
|
||||
// When EVOLVER_SESSION_SCOPE is set (e.g., to a Discord channel ID or project name),
|
||||
// evolution state, memory graph, and assets are isolated to a per-scope subdirectory.
|
||||
// This prevents cross-channel/cross-project memory contamination.
|
||||
// When NOT set, everything works as before (global scope, backward compatible).
|
||||
function getSessionScope() {
|
||||
const raw = String(process.env.EVOLVER_SESSION_SCOPE || '').trim();
|
||||
if (!raw) return null;
|
||||
// Sanitize: only allow alphanumeric, dash, underscore, dot (prevent path traversal).
|
||||
const safe = raw.replace(/[^a-zA-Z0-9_\-\.]/g, '_').slice(0, 128);
|
||||
return safe || null;
|
||||
}
|
||||
|
||||
function getEvolutionDir() {
|
||||
const baseDir = process.env.EVOLUTION_DIR || path.join(getMemoryDir(), 'evolution');
|
||||
const scope = getSessionScope();
|
||||
if (scope) {
|
||||
return path.join(baseDir, 'scopes', scope);
|
||||
}
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
function getGepAssetsDir() {
|
||||
const repoRoot = getRepoRoot();
|
||||
const baseDir = process.env.GEP_ASSETS_DIR || path.join(repoRoot, 'assets', 'gep');
|
||||
const scope = getSessionScope();
|
||||
if (scope) {
|
||||
return path.join(baseDir, 'scopes', scope);
|
||||
}
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
function getSkillsDir() {
|
||||
return process.env.SKILLS_DIR || path.join(getWorkspaceRoot(), 'skills');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRepoRoot,
|
||||
getWorkspaceRoot,
|
||||
getLogsDir,
|
||||
getMemoryDir,
|
||||
getEvolutionDir,
|
||||
getGepAssetsDir,
|
||||
getSkillsDir,
|
||||
getSessionScope,
|
||||
};
|
||||
|
||||
355
skills/capability-evolver/src/gep/personality.js
Normal file
355
skills/capability-evolver/src/gep/personality.js
Normal 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();
|
||||
const { getEvolutionDir } = require('./paths'); return path.join(getEvolutionDir(), 'personality_state.json');
|
||||
}
|
||||
|
||||
function defaultPersonalityState() {
|
||||
// Conservative defaults: protocol-first, safe, low-risk.
|
||||
return {
|
||||
type: 'PersonalityState',
|
||||
rigor: 0.7,
|
||||
creativity: 0.35,
|
||||
verbosity: 0.25,
|
||||
risk_tolerance: 0.4,
|
||||
obedience: 0.85,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePersonalityState(state) {
|
||||
const s = state && typeof state === 'object' ? state : {};
|
||||
return {
|
||||
type: 'PersonalityState',
|
||||
rigor: clamp01(s.rigor),
|
||||
creativity: clamp01(s.creativity),
|
||||
verbosity: clamp01(s.verbosity),
|
||||
risk_tolerance: clamp01(s.risk_tolerance),
|
||||
obedience: clamp01(s.obedience),
|
||||
};
|
||||
}
|
||||
|
||||
function isValidPersonalityState(obj) {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
if (obj.type !== 'PersonalityState') return false;
|
||||
for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
|
||||
const v = obj[k];
|
||||
if (!Number.isFinite(Number(v))) return false;
|
||||
const n = Number(v);
|
||||
if (n < 0 || n > 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function roundToStep(x, step) {
|
||||
const s = Number(step);
|
||||
if (!Number.isFinite(s) || s <= 0) return x;
|
||||
return Math.round(Number(x) / s) * s;
|
||||
}
|
||||
|
||||
function personalityKey(state) {
|
||||
const s = normalizePersonalityState(state);
|
||||
const step = 0.1;
|
||||
const r = roundToStep(s.rigor, step).toFixed(1);
|
||||
const c = roundToStep(s.creativity, step).toFixed(1);
|
||||
const v = roundToStep(s.verbosity, step).toFixed(1);
|
||||
const rt = roundToStep(s.risk_tolerance, step).toFixed(1);
|
||||
const o = roundToStep(s.obedience, step).toFixed(1);
|
||||
return `rigor=${r}|creativity=${c}|verbosity=${v}|risk_tolerance=${rt}|obedience=${o}`;
|
||||
}
|
||||
|
||||
function getParamDeltas(fromState, toState) {
|
||||
const a = normalizePersonalityState(fromState);
|
||||
const b = normalizePersonalityState(toState);
|
||||
const deltas = [];
|
||||
for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
|
||||
deltas.push({ param: k, delta: Number(b[k]) - Number(a[k]) });
|
||||
}
|
||||
deltas.sort((x, y) => Math.abs(y.delta) - Math.abs(x.delta));
|
||||
return deltas;
|
||||
}
|
||||
|
||||
function personalityScore(statsEntry) {
|
||||
const e = statsEntry && typeof statsEntry === 'object' ? statsEntry : {};
|
||||
const succ = Number(e.success) || 0;
|
||||
const fail = Number(e.fail) || 0;
|
||||
const total = succ + fail;
|
||||
// Laplace-smoothed success probability
|
||||
const p = (succ + 1) / (total + 2);
|
||||
// Penalize tiny-sample overconfidence
|
||||
const sampleWeight = Math.min(1, total / 8);
|
||||
// Use avg_score (if present) as mild quality proxy
|
||||
const avg = Number.isFinite(Number(e.avg_score)) ? Number(e.avg_score) : null;
|
||||
const q = avg == null ? 0.5 : clamp01(avg);
|
||||
return p * 0.75 + q * 0.25 * sampleWeight;
|
||||
}
|
||||
|
||||
function chooseBestKnownPersonality(statsByKey) {
|
||||
const stats = statsByKey && typeof statsByKey === 'object' ? statsByKey : {};
|
||||
let best = null;
|
||||
for (const [k, entry] of Object.entries(stats)) {
|
||||
const e = entry || {};
|
||||
const total = (Number(e.success) || 0) + (Number(e.fail) || 0);
|
||||
if (total < 3) continue;
|
||||
const sc = personalityScore(e);
|
||||
if (!best || sc > best.score) best = { key: k, score: sc, entry: e };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function parseKeyToState(key) {
|
||||
// key format: rigor=0.7|creativity=0.3|...
|
||||
const out = defaultPersonalityState();
|
||||
const parts = String(key || '').split('|').map(s => s.trim()).filter(Boolean);
|
||||
for (const p of parts) {
|
||||
const [k, v] = p.split('=').map(x => String(x || '').trim());
|
||||
if (!k) continue;
|
||||
if (!['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience'].includes(k)) continue;
|
||||
out[k] = clamp01(Number(v));
|
||||
}
|
||||
return normalizePersonalityState(out);
|
||||
}
|
||||
|
||||
function applyPersonalityMutations(state, mutations) {
|
||||
let cur = normalizePersonalityState(state);
|
||||
const muts = Array.isArray(mutations) ? mutations : [];
|
||||
const applied = [];
|
||||
let count = 0;
|
||||
for (const m of muts) {
|
||||
if (!m || typeof m !== 'object') continue;
|
||||
const param = String(m.param || '').trim();
|
||||
if (!['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience'].includes(param)) continue;
|
||||
const delta = Number(m.delta);
|
||||
if (!Number.isFinite(delta)) continue;
|
||||
const clipped = Math.max(-0.2, Math.min(0.2, delta));
|
||||
cur[param] = clamp01(Number(cur[param]) + clipped);
|
||||
applied.push({ type: 'PersonalityMutation', param, delta: clipped, reason: String(m.reason || '').slice(0, 140) });
|
||||
count += 1;
|
||||
if (count >= 2) break;
|
||||
}
|
||||
return { state: cur, applied };
|
||||
}
|
||||
|
||||
function proposeMutations({ baseState, reason, driftEnabled, signals }) {
|
||||
const s = normalizePersonalityState(baseState);
|
||||
const sig = Array.isArray(signals) ? signals.map(x => String(x || '')) : [];
|
||||
const muts = [];
|
||||
|
||||
const r = String(reason || '');
|
||||
if (driftEnabled) {
|
||||
muts.push({ type: 'PersonalityMutation', param: 'creativity', delta: +0.1, reason: r || 'drift enabled' });
|
||||
// Keep risk bounded under drift by default.
|
||||
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: -0.05, reason: 'drift safety clamp' });
|
||||
} else if (sig.includes('protocol_drift')) {
|
||||
muts.push({ type: 'PersonalityMutation', param: 'obedience', delta: +0.1, reason: r || 'protocol drift' });
|
||||
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.05, reason: 'tighten protocol compliance' });
|
||||
} else if (sig.includes('log_error') || sig.some(x => x.startsWith('errsig:') || x.startsWith('errsig_norm:'))) {
|
||||
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.1, reason: r || 'repair instability' });
|
||||
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: -0.1, reason: 'reduce risky changes under errors' });
|
||||
} else if (hasOpportunitySignal(sig)) {
|
||||
// Opportunity detected: nudge towards creativity to enable innovation.
|
||||
muts.push({ type: 'PersonalityMutation', param: 'creativity', delta: +0.1, reason: r || 'opportunity signal detected' });
|
||||
muts.push({ type: 'PersonalityMutation', param: 'risk_tolerance', delta: +0.05, reason: 'allow exploration for innovation' });
|
||||
} else {
|
||||
// Plateau-like generic: slightly increase rigor, slightly decrease verbosity (more concise execution).
|
||||
muts.push({ type: 'PersonalityMutation', param: 'rigor', delta: +0.05, reason: r || 'stability bias' });
|
||||
muts.push({ type: 'PersonalityMutation', param: 'verbosity', delta: -0.05, reason: 'reduce noise' });
|
||||
}
|
||||
|
||||
// If already very high obedience, avoid pushing it further; swap second mutation to creativity.
|
||||
if (s.obedience >= 0.95) {
|
||||
const idx = muts.findIndex(x => x.param === 'obedience');
|
||||
if (idx >= 0) muts[idx] = { type: 'PersonalityMutation', param: 'creativity', delta: +0.05, reason: 'obedience saturated' };
|
||||
}
|
||||
return muts;
|
||||
}
|
||||
|
||||
function shouldTriggerPersonalityMutation({ driftEnabled, recentEvents }) {
|
||||
if (driftEnabled) return { ok: true, reason: 'drift enabled' };
|
||||
const list = Array.isArray(recentEvents) ? recentEvents : [];
|
||||
const tail = list.slice(-6);
|
||||
const outcomes = tail
|
||||
.map(e => (e && e.outcome && e.outcome.status ? String(e.outcome.status) : null))
|
||||
.filter(Boolean);
|
||||
if (outcomes.length >= 4) {
|
||||
const recentFailed = outcomes.slice(-4).filter(x => x === 'failed').length;
|
||||
if (recentFailed >= 3) return { ok: true, reason: 'long failure streak' };
|
||||
}
|
||||
// Mutation consecutive failure proxy: last 3 events that have mutation_id.
|
||||
const withMut = tail.filter(e => e && typeof e.mutation_id === 'string' && e.mutation_id);
|
||||
if (withMut.length >= 3) {
|
||||
const last3 = withMut.slice(-3);
|
||||
const fail3 = last3.filter(e => e && e.outcome && e.outcome.status === 'failed').length;
|
||||
if (fail3 >= 3) return { ok: true, reason: 'mutation consecutive failures' };
|
||||
}
|
||||
return { ok: false, reason: '' };
|
||||
}
|
||||
|
||||
function loadPersonalityModel() {
|
||||
const p = personalityFilePath();
|
||||
const fallback = {
|
||||
version: 1,
|
||||
current: defaultPersonalityState(),
|
||||
stats: {},
|
||||
history: [],
|
||||
updated_at: nowIso(),
|
||||
};
|
||||
const raw = readJsonIfExists(p, fallback);
|
||||
const cur = normalizePersonalityState(raw && raw.current ? raw.current : defaultPersonalityState());
|
||||
const stats = raw && typeof raw.stats === 'object' ? raw.stats : {};
|
||||
const history = Array.isArray(raw && raw.history) ? raw.history : [];
|
||||
return { version: 1, current: cur, stats, history, updated_at: raw && raw.updated_at ? raw.updated_at : nowIso() };
|
||||
}
|
||||
|
||||
function savePersonalityModel(model) {
|
||||
const m = model && typeof model === 'object' ? model : {};
|
||||
const out = {
|
||||
version: 1,
|
||||
current: normalizePersonalityState(m.current || defaultPersonalityState()),
|
||||
stats: m.stats && typeof m.stats === 'object' ? m.stats : {},
|
||||
history: Array.isArray(m.history) ? m.history.slice(-120) : [],
|
||||
updated_at: nowIso(),
|
||||
};
|
||||
writeJsonAtomic(personalityFilePath(), out);
|
||||
return out;
|
||||
}
|
||||
|
||||
function selectPersonalityForRun({ driftEnabled, signals, recentEvents } = {}) {
|
||||
const model = loadPersonalityModel();
|
||||
const base = normalizePersonalityState(model.current);
|
||||
const stats = model.stats || {};
|
||||
|
||||
const best = chooseBestKnownPersonality(stats);
|
||||
let naturalSelectionApplied = [];
|
||||
|
||||
// Natural selection: nudge towards the best-known configuration (small, max 2 params).
|
||||
if (best && best.key) {
|
||||
const bestState = parseKeyToState(best.key);
|
||||
const diffs = getParamDeltas(base, bestState).filter(d => Math.abs(d.delta) >= 0.05);
|
||||
const muts = [];
|
||||
for (const d of diffs.slice(0, 2)) {
|
||||
const clipped = Math.max(-0.1, Math.min(0.1, d.delta));
|
||||
muts.push({ type: 'PersonalityMutation', param: d.param, delta: clipped, reason: 'natural_selection' });
|
||||
}
|
||||
const applied = applyPersonalityMutations(base, muts);
|
||||
model.current = applied.state;
|
||||
naturalSelectionApplied = applied.applied;
|
||||
}
|
||||
|
||||
// Triggered personality mutation (explicit rule-based).
|
||||
const trig = shouldTriggerPersonalityMutation({ driftEnabled: !!driftEnabled, recentEvents });
|
||||
let triggeredApplied = [];
|
||||
if (trig.ok) {
|
||||
const props = proposeMutations({
|
||||
baseState: model.current,
|
||||
reason: trig.reason,
|
||||
driftEnabled: !!driftEnabled,
|
||||
signals,
|
||||
});
|
||||
const applied = applyPersonalityMutations(model.current, props);
|
||||
model.current = applied.state;
|
||||
triggeredApplied = applied.applied;
|
||||
}
|
||||
|
||||
// Persist updated current state.
|
||||
const saved = savePersonalityModel(model);
|
||||
const key = personalityKey(saved.current);
|
||||
const known = !!(saved.stats && saved.stats[key]);
|
||||
|
||||
return {
|
||||
personality_state: saved.current,
|
||||
personality_key: key,
|
||||
personality_known: known,
|
||||
personality_mutations: [...naturalSelectionApplied, ...triggeredApplied],
|
||||
model_meta: {
|
||||
best_known_key: best && best.key ? best.key : null,
|
||||
best_known_score: best && Number.isFinite(Number(best.score)) ? Number(best.score) : null,
|
||||
triggered: trig.ok ? { reason: trig.reason } : null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function updatePersonalityStats({ personalityState, outcome, score, notes } = {}) {
|
||||
const model = loadPersonalityModel();
|
||||
const st = normalizePersonalityState(personalityState || model.current);
|
||||
const key = personalityKey(st);
|
||||
if (!model.stats || typeof model.stats !== 'object') model.stats = {};
|
||||
const cur = model.stats[key] && typeof model.stats[key] === 'object' ? model.stats[key] : { success: 0, fail: 0, avg_score: 0.5, n: 0 };
|
||||
|
||||
const out = String(outcome || '').toLowerCase();
|
||||
if (out === 'success') cur.success = (Number(cur.success) || 0) + 1;
|
||||
else if (out === 'failed') cur.fail = (Number(cur.fail) || 0) + 1;
|
||||
|
||||
const sc = Number.isFinite(Number(score)) ? clamp01(Number(score)) : null;
|
||||
if (sc != null) {
|
||||
const n = (Number(cur.n) || 0) + 1;
|
||||
const prev = Number.isFinite(Number(cur.avg_score)) ? Number(cur.avg_score) : 0.5;
|
||||
cur.avg_score = prev + (sc - prev) / n;
|
||||
cur.n = n;
|
||||
}
|
||||
cur.updated_at = nowIso();
|
||||
model.stats[key] = cur;
|
||||
|
||||
model.history = Array.isArray(model.history) ? model.history : [];
|
||||
model.history.push({
|
||||
at: nowIso(),
|
||||
key,
|
||||
outcome: out === 'success' || out === 'failed' ? out : 'unknown',
|
||||
score: sc,
|
||||
notes: notes ? String(notes).slice(0, 220) : null,
|
||||
});
|
||||
|
||||
savePersonalityModel(model);
|
||||
return { key, stats: cur };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clamp01,
|
||||
defaultPersonalityState,
|
||||
normalizePersonalityState,
|
||||
isValidPersonalityState,
|
||||
personalityKey,
|
||||
loadPersonalityModel,
|
||||
savePersonalityModel,
|
||||
selectPersonalityForRun,
|
||||
updatePersonalityStats,
|
||||
};
|
||||
|
||||
460
skills/capability-evolver/src/gep/prompt.js
Normal file
460
skills/capability-evolver/src/gep/prompt.js
Normal file
@@ -0,0 +1,460 @@
|
||||
const { captureEnvFingerprint } = require('./envFingerprint');
|
||||
const { formatAssetPreview } = require('./assets');
|
||||
const { generateInnovationIdeas } = require('../ops/innovation'); // [2026-02-14] Innovation Catalyst Integration
|
||||
const { analyzeRecentHistory, OPPORTUNITY_SIGNALS } = require('./signals'); // [2026-02-14] Signal Analysis Integration
|
||||
|
||||
/**
|
||||
* Build a minimal prompt for direct-reuse mode.
|
||||
*/
|
||||
function buildReusePrompt({ capsule, signals, nowIso }) {
|
||||
const payload = capsule.payload || capsule;
|
||||
const summary = payload.summary || capsule.summary || '(no summary)';
|
||||
const gene = payload.gene || capsule.gene || '(unknown)';
|
||||
const confidence = payload.confidence || capsule.confidence || 0;
|
||||
const assetId = capsule.asset_id || '(unknown)';
|
||||
const sourceNode = capsule.source_node_id || '(unknown)';
|
||||
const trigger = Array.isArray(payload.trigger || capsule.trigger_text)
|
||||
? (payload.trigger || String(capsule.trigger_text || '').split(',')).join(', ')
|
||||
: '';
|
||||
|
||||
return `
|
||||
GEP -- REUSE MODE (Search-First) [${nowIso || new Date().toISOString()}]
|
||||
|
||||
You are applying a VERIFIED solution from the EvoMap Hub.
|
||||
Source asset: ${assetId} (Node: ${sourceNode})
|
||||
Confidence: ${confidence} | Gene: ${gene}
|
||||
Trigger signals: ${trigger}
|
||||
|
||||
Summary: ${summary}
|
||||
|
||||
Your signals: ${JSON.stringify(signals || [])}
|
||||
|
||||
Instructions:
|
||||
1. Read the capsule details below.
|
||||
2. Apply the fix to the local codebase, adapting paths/names.
|
||||
3. Run validation to confirm it works.
|
||||
4. If passed, run: node index.js solidify
|
||||
5. If failed, ROLLBACK and report.
|
||||
|
||||
Capsule payload:
|
||||
\`\`\`json
|
||||
${JSON.stringify(payload, null, 2)}
|
||||
\`\`\`
|
||||
|
||||
IMPORTANT: Do NOT reinvent. Apply faithfully.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Hub Matched Solution block.
|
||||
*/
|
||||
function buildHubMatchedBlock({ capsule }) {
|
||||
if (!capsule) return '(no hub match)';
|
||||
const payload = capsule.payload || capsule;
|
||||
const summary = payload.summary || capsule.summary || '(no summary)';
|
||||
const gene = payload.gene || capsule.gene || '(unknown)';
|
||||
const confidence = payload.confidence || capsule.confidence || 0;
|
||||
const assetId = capsule.asset_id || '(unknown)';
|
||||
|
||||
return `
|
||||
Hub Matched Solution (STRONG REFERENCE):
|
||||
- Asset: ${assetId} (${confidence})
|
||||
- Gene: ${gene}
|
||||
- Summary: ${summary}
|
||||
- Payload:
|
||||
\`\`\`json
|
||||
${JSON.stringify(payload, null, 2)}
|
||||
\`\`\`
|
||||
Use this as your primary approach if applicable. Adapt to local context.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate context intelligently to preserve header/footer structure.
|
||||
*/
|
||||
function truncateContext(text, maxLength = 20000) {
|
||||
if (!text || text.length <= maxLength) return text || '';
|
||||
return text.slice(0, maxLength) + '\n...[TRUNCATED_EXECUTION_CONTEXT]...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict schema definitions for the prompt to reduce drift.
|
||||
* UPDATED: 2026-02-14 (Protocol Drift Fix v3.2 - JSON-Only Enforcement)
|
||||
*/
|
||||
const SCHEMA_DEFINITIONS = `
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
I. Mandatory Evolution Object Model (Output EXACTLY these 5 objects)
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Output separate JSON objects. DO NOT wrap in a single array.
|
||||
DO NOT use markdown code blocks (like \`\`\`json ... \`\`\`).
|
||||
Output RAW JSON ONLY. No prelude, no postscript.
|
||||
Missing any object = PROTOCOL FAILURE.
|
||||
ENSURE VALID JSON SYNTAX (escape quotes in strings).
|
||||
|
||||
0. Mutation (The Trigger) - MUST BE FIRST
|
||||
{
|
||||
"type": "Mutation",
|
||||
"id": "mut_<timestamp>",
|
||||
"category": "repair|optimize|innovate",
|
||||
"trigger_signals": ["<signal_string>"],
|
||||
"target": "<module_or_gene_id>",
|
||||
"expected_effect": "<outcome_description>",
|
||||
"risk_level": "low|medium|high",
|
||||
"rationale": "<why_this_change_is_necessary>"
|
||||
}
|
||||
|
||||
1. PersonalityState (The Mood)
|
||||
{
|
||||
"type": "PersonalityState",
|
||||
"rigor": 0.0-1.0,
|
||||
"creativity": 0.0-1.0,
|
||||
"verbosity": 0.0-1.0,
|
||||
"risk_tolerance": 0.0-1.0,
|
||||
"obedience": 0.0-1.0
|
||||
}
|
||||
|
||||
2. EvolutionEvent (The Record)
|
||||
{
|
||||
"type": "EvolutionEvent",
|
||||
"schema_version": "1.5.0",
|
||||
"id": "evt_<timestamp>",
|
||||
"parent": <parent_evt_id|null>,
|
||||
"intent": "repair|optimize|innovate",
|
||||
"signals": ["<signal_string>"],
|
||||
"genes_used": ["<gene_id>"],
|
||||
"mutation_id": "<mut_id>",
|
||||
"personality_state": { ... },
|
||||
"blast_radius": { "files": N, "lines": N },
|
||||
"outcome": { "status": "success|failed", "score": 0.0-1.0 }
|
||||
}
|
||||
|
||||
3. Gene (The Knowledge)
|
||||
- Reuse/update existing ID if possible. Create new only if novel pattern.
|
||||
{
|
||||
"type": "Gene",
|
||||
"schema_version": "1.5.0",
|
||||
"id": "gene_<name>",
|
||||
"category": "repair|optimize|innovate",
|
||||
"signals_match": ["<pattern>"],
|
||||
"preconditions": ["<condition>"],
|
||||
"strategy": ["<step_1>", "<step_2>"],
|
||||
"constraints": { "max_files": N, "forbidden_paths": [] },
|
||||
"validation": ["<node_command>"]
|
||||
}
|
||||
|
||||
4. Capsule (The Result)
|
||||
- Only on success. Reference Gene used.
|
||||
{
|
||||
"type": "Capsule",
|
||||
"schema_version": "1.5.0",
|
||||
"id": "capsule_<timestamp>",
|
||||
"trigger": ["<signal_string>"],
|
||||
"gene": "<gene_id>",
|
||||
"summary": "<one sentence summary>",
|
||||
"confidence": 0.0-1.0,
|
||||
"blast_radius": { "files": N, "lines": N }
|
||||
}
|
||||
`.trim();
|
||||
|
||||
function buildGepPrompt({
|
||||
nowIso,
|
||||
context,
|
||||
signals,
|
||||
selector,
|
||||
parentEventId,
|
||||
selectedGene,
|
||||
capsuleCandidates,
|
||||
genesPreview,
|
||||
capsulesPreview,
|
||||
capabilityCandidatesPreview,
|
||||
externalCandidatesPreview,
|
||||
hubMatchedBlock,
|
||||
cycleId,
|
||||
recentHistory, // [2026-02-14] Pass recent history
|
||||
}) {
|
||||
const parentValue = parentEventId ? `"${parentEventId}"` : 'null';
|
||||
const selectedGeneId = selectedGene && selectedGene.id ? selectedGene.id : 'gene_<name>';
|
||||
const envFingerprint = captureEnvFingerprint();
|
||||
const cycleLabel = cycleId ? ` Cycle #${cycleId}` : '';
|
||||
|
||||
// Extract strategy from selected gene if available
|
||||
let strategyBlock = "";
|
||||
if (selectedGene && selectedGene.strategy && Array.isArray(selectedGene.strategy)) {
|
||||
strategyBlock = `
|
||||
ACTIVE STRATEGY (${selectedGeneId}):
|
||||
${selectedGene.strategy.map((s, i) => `${i + 1}. ${s}`).join('\n')}
|
||||
ADHERE TO THIS STRATEGY STRICTLY.
|
||||
`.trim();
|
||||
} else {
|
||||
// Fallback strategy if no gene is selected or strategy is missing
|
||||
strategyBlock = `
|
||||
ACTIVE STRATEGY (Generic):
|
||||
1. Analyze signals and context.
|
||||
2. Select or create a Gene that addresses the root cause.
|
||||
3. Apply minimal, safe changes.
|
||||
4. Validate changes strictly.
|
||||
5. Solidify knowledge.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// Use intelligent truncation
|
||||
const executionContext = truncateContext(context, 20000);
|
||||
|
||||
// Strict Schema Injection
|
||||
const schemaSection = SCHEMA_DEFINITIONS.replace('<parent_evt_id|null>', parentValue);
|
||||
|
||||
// Reduce noise by filtering capabilityCandidatesPreview if too large
|
||||
// If a gene is selected, we need less noise from capabilities
|
||||
let capsPreview = capabilityCandidatesPreview || '(none)';
|
||||
const capsLimit = selectedGene ? 500 : 2000;
|
||||
if (capsPreview.length > capsLimit) {
|
||||
capsPreview = capsPreview.slice(0, capsLimit) + "\n...[TRUNCATED_CAPABILITIES]...";
|
||||
}
|
||||
|
||||
// Optimize signals display: truncate long signals and limit count
|
||||
const uniqueSignals = Array.from(new Set(signals || []));
|
||||
const optimizedSignals = uniqueSignals.slice(0, 50).map(s => {
|
||||
if (typeof s === 'string' && s.length > 200) {
|
||||
return s.slice(0, 200) + '...[TRUNCATED_SIGNAL]';
|
||||
}
|
||||
return s;
|
||||
});
|
||||
if (uniqueSignals.length > 50) {
|
||||
optimizedSignals.push(`...[TRUNCATED ${uniqueSignals.length - 50} SIGNALS]...`);
|
||||
}
|
||||
|
||||
const formattedGenes = formatAssetPreview(genesPreview);
|
||||
const formattedCapsules = formatAssetPreview(capsulesPreview);
|
||||
|
||||
// [2026-02-14] Innovation Catalyst Integration
|
||||
// If stagnation is detected, inject concrete innovation ideas into the prompt.
|
||||
let innovationBlock = '';
|
||||
const stagnationSignals = [
|
||||
'evolution_stagnation_detected',
|
||||
'stable_success_plateau',
|
||||
'repair_loop_detected',
|
||||
'force_innovation_after_repair_loop',
|
||||
'empty_cycle_loop_detected',
|
||||
'evolution_saturation'
|
||||
];
|
||||
if (uniqueSignals.some(s => stagnationSignals.includes(s))) {
|
||||
const ideas = generateInnovationIdeas();
|
||||
if (ideas && ideas.length > 0) {
|
||||
innovationBlock = `
|
||||
Context [Innovation Catalyst] (Stagnation Detected - Consider These Ideas):
|
||||
${ideas.join('\n')}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// [2026-02-14] Strict Stagnation Directive
|
||||
// If uniqueSignals contains 'evolution_stagnation_detected' or 'stable_success_plateau',
|
||||
// inject a MANDATORY directive to force innovation and forbid repair/optimize if not strictly necessary.
|
||||
if (uniqueSignals.includes('evolution_stagnation_detected') || uniqueSignals.includes('stable_success_plateau')) {
|
||||
const stagnationDirective = `
|
||||
*** CRITICAL STAGNATION DIRECTIVE ***
|
||||
System has detected stagnation (repetitive cycles or lack of progress).
|
||||
You MUST choose INTENT: INNOVATE.
|
||||
You MUST NOT choose repair or optimize unless there is a critical blocking error (log_error).
|
||||
Prefer implementing one of the Innovation Catalyst ideas above.
|
||||
`;
|
||||
innovationBlock += stagnationDirective;
|
||||
}
|
||||
|
||||
// [2026-02-14] Recent History Integration
|
||||
let historyBlock = '';
|
||||
if (recentHistory && recentHistory.length > 0) {
|
||||
historyBlock = `
|
||||
Recent Evolution History (last 8 cycles -- DO NOT repeat the same intent+signal+gene):
|
||||
${recentHistory.map((h, i) => ` ${i + 1}. [${h.intent}] signals=[${h.signals.slice(0, 2).join(', ')}] gene=${h.gene_id} outcome=${h.outcome.status} @${h.timestamp}`).join('\n')}
|
||||
IMPORTANT: If you see 3+ consecutive "repair" cycles with the same gene, you MUST switch to "innovate" intent.
|
||||
`.trim();
|
||||
}
|
||||
|
||||
// Refactor prompt assembly to minimize token usage and maximize clarity
|
||||
// UPDATED: 2026-02-14 (Optimized Asset Embedding & Strict Schema v2.5 - JSON-Only Hardening)
|
||||
const basePrompt = `
|
||||
GEP — GENOME EVOLUTION PROTOCOL (v1.10.3 STRICT)${cycleLabel} [${nowIso}]
|
||||
|
||||
You are a protocol-bound evolution engine. Compliance overrides optimality.
|
||||
|
||||
${schemaSection}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
II. Directives & Logic
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
1. Intent: ${selector && selector.intent ? selector.intent.toUpperCase() : 'UNKNOWN'}
|
||||
Reason: ${(selector && selector.reason) ? (Array.isArray(selector.reason) ? selector.reason.join('; ') : selector.reason) : 'No reason provided.'}
|
||||
|
||||
2. Selection: Selected Gene "${selectedGeneId}".
|
||||
${strategyBlock}
|
||||
|
||||
3. Execution: Apply changes (tool calls). Repair/Optimize: small/reversible. Innovate: new skills in \`skills/<name>/\`.
|
||||
4. Validation: Run gene's validation steps. Fail = ROLLBACK.
|
||||
5. Solidify: Output 5 Mandatory Objects. Update Gene/Capsule files.
|
||||
6. Report: Use \`feishu-evolver-wrapper/report.js\`. Describe WHAT/WHY.
|
||||
|
||||
PHILOSOPHY:
|
||||
- Automate Patterns: 3+ manual occurrences = tool.
|
||||
- Innovate > Maintain: 60% innovation.
|
||||
- Robustness: Fix recurring errors permanently.
|
||||
- Blast Radius Control (CRITICAL):
|
||||
* Check file count BEFORE editing. > 80% of max_files = STOP.
|
||||
* System hard cap: 60 files / 20000 lines per cycle.
|
||||
* Repair: fix ONLY broken files. Do NOT reinstall/bulk-copy.
|
||||
* Prefer targeted edits.
|
||||
- Strictness: NO CHITCHAT. NO MARKDOWN WRAPPERS around JSON. Output RAW JSON objects separated by newlines.
|
||||
- NO "Here is the plan" or conversational filler. START IMMEDIATELY WITH JSON.
|
||||
|
||||
CONSTRAINTS:
|
||||
- No \`exec\` for messaging (use feishu-post/card).
|
||||
- \`exec\` usage: Only for background tasks. LOG IT. Optimize usage to avoid high token burn.
|
||||
- New skills -> \`skills/<name>/\`.
|
||||
- NEVER modify \`skills/evolver/\` or \`skills/feishu-evolver-wrapper/\`. These are managed through the human-reviewed release pipeline.
|
||||
Self-modification causes instability and is blocked by solidify. To enable (NOT recommended): set EVOLVE_ALLOW_SELF_MODIFY=true.
|
||||
|
||||
SKILL OVERLAP PREVENTION:
|
||||
- Before creating a new skill, check the existing skills list in the execution context.
|
||||
- If a skill with similar functionality already exists (e.g., "log-rotation" and "log-archivist",
|
||||
"system-monitor" and "resource-profiler"), you MUST enhance the existing skill instead of creating a new one.
|
||||
- Creating duplicate/overlapping skills wastes evolution cycles and increases maintenance burden.
|
||||
- Violation = mark outcome as FAILED with reason "skill_overlap".
|
||||
|
||||
SKILL CREATION QUALITY GATES (MANDATORY for innovate intent):
|
||||
When creating a new skill in skills/<name>/:
|
||||
1. STRUCTURE: Follow the standard skill layout:
|
||||
skills/<name>/
|
||||
|- index.js (required: main entry with working exports)
|
||||
|- SKILL.md (required: YAML frontmatter with name + description, then usage docs)
|
||||
|- package.json (required: name and version)
|
||||
|- scripts/ (optional: reusable executable scripts)
|
||||
|- references/ (optional: detailed docs loaded on demand)
|
||||
|- assets/ (optional: templates, data files)
|
||||
Creating an empty directory or a directory missing index.js = FAILED.
|
||||
Do NOT create unnecessary files (README.md, CHANGELOG.md, INSTALLATION_GUIDE.md, etc.).
|
||||
2. SKILL.MD FRONTMATTER: Every SKILL.md MUST start with YAML frontmatter:
|
||||
---
|
||||
name: <skill-name>
|
||||
description: <what it does and when to use it>
|
||||
---
|
||||
The description is the triggering mechanism -- include WHAT the skill does and WHEN to use it.
|
||||
3. CONCISENESS: SKILL.md body should be under 500 lines. Keep instructions lean.
|
||||
Only include information the agent does not already know. Move detailed reference
|
||||
material to references/ files, not into SKILL.md itself.
|
||||
4. EXPORT VERIFICATION: Every exported function must be importable.
|
||||
Run: node -e "const s = require('./skills/<name>'); console.log(Object.keys(s))"
|
||||
If this fails, the skill is broken. Fix before solidify.
|
||||
5. NO HARDCODED SECRETS: Never embed API keys, tokens, or secrets in code.
|
||||
Use process.env or .env references. Hardcoded App ID, App Secret, Bearer tokens = FAILED.
|
||||
6. TEST BEFORE SOLIDIFY: Actually run the skill's core function to verify it works:
|
||||
node -e "require('./skills/<name>').main ? require('./skills/<name>').main() : console.log('ok')"
|
||||
Scripts in scripts/ must also be tested by executing them.
|
||||
7. ATOMIC CREATION: Create ALL files for a skill in a single cycle.
|
||||
Do not create a directory in one cycle and fill it in the next.
|
||||
Empty directories from failed cycles will be automatically cleaned up on rollback.
|
||||
|
||||
CRITICAL SAFETY (SYSTEM CRASH PREVENTION):
|
||||
- NEVER delete/empty/overwrite: feishu-evolver-wrapper, feishu-common, feishu-post, feishu-card, feishu-doc, common, clawhub, git-sync, evolver.
|
||||
- NEVER delete root files: MEMORY.md, SOUL.md, IDENTITY.md, AGENTS.md, USER.md, HEARTBEAT.md, RECENT_EVENTS.md, TOOLS.md, openclaw.json, .env, package.json.
|
||||
- Fix broken skills; DO NOT delete and recreate.
|
||||
- Violation = ROLLBACK + FAILED.
|
||||
|
||||
COMMON FAILURE PATTERNS:
|
||||
- Blast radius exceeded.
|
||||
- Omitted Mutation object.
|
||||
- Merged objects into one JSON.
|
||||
- Hallucinated "type": "Logic".
|
||||
- "id": "mut_undefined".
|
||||
- Missing "trigger_signals".
|
||||
- Unrunnable validation steps.
|
||||
- Markdown code blocks wrapping JSON (FORBIDDEN).
|
||||
|
||||
FAILURE STREAK AWARENESS:
|
||||
- If "consecutive_failure_streak_N" or "failure_loop_detected":
|
||||
1. Change approach (do NOT repeat failed gene).
|
||||
2. Pick SIMPLER fix.
|
||||
3. Respect "ban_gene:<id>".
|
||||
|
||||
Final Directive: Every cycle must leave the system measurably better.
|
||||
START IMMEDIATELY WITH RAW JSON (Mutation Object first).
|
||||
DO NOT WRITE ANY INTRODUCTORY TEXT.
|
||||
|
||||
Context [Signals]:
|
||||
${JSON.stringify(optimizedSignals)}
|
||||
|
||||
Context [Env Fingerprint]:
|
||||
${JSON.stringify(envFingerprint, null, 2)}
|
||||
${innovationBlock}
|
||||
Context [Injection Hint]:
|
||||
${process.env.EVOLVE_HINT ? process.env.EVOLVE_HINT : '(none)'}
|
||||
|
||||
Context [Gene Preview] (Reference for Strategy):
|
||||
${formattedGenes}
|
||||
|
||||
Context [Capsule Preview] (Reference for Past Success):
|
||||
${formattedCapsules}
|
||||
|
||||
Context [Capability Candidates]:
|
||||
${capsPreview}
|
||||
|
||||
Context [Hub Matched Solution]:
|
||||
${hubMatchedBlock || '(no hub match)'}
|
||||
|
||||
Context [External Candidates]:
|
||||
${externalCandidatesPreview || '(none)'}
|
||||
|
||||
${historyBlock}
|
||||
|
||||
Context [Execution]:
|
||||
${executionContext}
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
MANDATORY POST-SOLIDIFY STEP (Wrapper Authority -- Cannot Be Skipped)
|
||||
━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
After solidify, a status summary file MUST exist for this cycle.
|
||||
Preferred path: evolver core auto-writes it during solidify.
|
||||
The wrapper will handle reporting AFTER git push.
|
||||
If core write is unavailable for any reason, create fallback status JSON manually.
|
||||
|
||||
Write a JSON file with your status:
|
||||
\`\`\`bash
|
||||
cat > ${process.env.WORKSPACE_DIR || '.'}/logs/status_${cycleId}.json << 'STATUSEOF'
|
||||
{
|
||||
"result": "success|failed",
|
||||
"en": "Status: [INTENT] <describe what you did in 1-2 sentences, in English>",
|
||||
"zh": "状态: [意图] <用中文描述你做了什么,1-2句>"
|
||||
}
|
||||
STATUSEOF
|
||||
\`\`\`
|
||||
|
||||
Rules:
|
||||
- "en" field: English status. "zh" field: Chinese status. Content must match (different language).
|
||||
- Add "result" with value success or failed.
|
||||
- INTENT must be one of: INNOVATION, REPAIR, OPTIMIZE (or Chinese: 创新, 修复, 优化)
|
||||
- Do NOT use generic text like "Step Complete", "Cycle finished", "周期已完成". Describe the actual work.
|
||||
- Example:
|
||||
{"result":"success","en":"Status: [INNOVATION] Created auto-scheduler that syncs calendar to HEARTBEAT.md","zh":"状态: [创新] 创建了自动调度器,将日历同步到 HEARTBEAT.md"}
|
||||
`.trim();
|
||||
|
||||
const maxChars = Number.isFinite(Number(process.env.GEP_PROMPT_MAX_CHARS)) ? Number(process.env.GEP_PROMPT_MAX_CHARS) : 50000;
|
||||
|
||||
if (basePrompt.length <= maxChars) return basePrompt;
|
||||
|
||||
const executionContextIndex = basePrompt.indexOf("Context [Execution]:");
|
||||
if (executionContextIndex > -1) {
|
||||
const prefix = basePrompt.slice(0, executionContextIndex + 20);
|
||||
const currentExecution = basePrompt.slice(executionContextIndex + 20);
|
||||
// Hard cap the execution context length to avoid token limit errors even if MAX_CHARS is high.
|
||||
// 20000 chars is roughly 5k tokens, which is safe for most models alongside the rest of the prompt.
|
||||
const EXEC_CONTEXT_CAP = 20000;
|
||||
const allowedExecutionLength = Math.min(EXEC_CONTEXT_CAP, Math.max(0, maxChars - prefix.length - 100));
|
||||
return prefix + "\n" + currentExecution.slice(0, allowedExecutionLength) + "\n...[TRUNCATED]...";
|
||||
}
|
||||
|
||||
return basePrompt.slice(0, maxChars) + "\n...[TRUNCATED]...";
|
||||
}
|
||||
|
||||
module.exports = { buildGepPrompt, buildReusePrompt, buildHubMatchedBlock };
|
||||
49
skills/capability-evolver/src/gep/sanitize.js
Normal file
49
skills/capability-evolver/src/gep/sanitize.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// Pre-publish payload sanitization.
|
||||
// Removes sensitive tokens, local paths, emails, and env references
|
||||
// from capsule payloads before broadcasting to the hub.
|
||||
|
||||
// Patterns to redact (replaced with placeholder)
|
||||
const REDACT_PATTERNS = [
|
||||
// API keys & tokens
|
||||
/Bearer\s+[A-Za-z0-9\-._~+\/]+=*/g,
|
||||
/sk-[A-Za-z0-9]{20,}/g,
|
||||
/token[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
|
||||
/api[_-]?key[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
|
||||
/secret[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
|
||||
// Local filesystem paths
|
||||
/\/home\/[^\s"',;)}\]]+/g,
|
||||
/\/Users\/[^\s"',;)}\]]+/g,
|
||||
/[A-Z]:\\[^\s"',;)}\]]+/g,
|
||||
// Email addresses
|
||||
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
||||
// .env file references
|
||||
/\.env(?:\.[a-zA-Z]+)?/g,
|
||||
];
|
||||
|
||||
const REDACTED = '[REDACTED]';
|
||||
|
||||
function redactString(str) {
|
||||
if (typeof str !== 'string') return str;
|
||||
let result = str;
|
||||
for (const pattern of REDACT_PATTERNS) {
|
||||
// Reset lastIndex for global regexes
|
||||
pattern.lastIndex = 0;
|
||||
result = result.replace(pattern, REDACTED);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep-clone and sanitize a capsule payload.
|
||||
* Returns a new object with sensitive values redacted.
|
||||
* Does NOT modify the original.
|
||||
*/
|
||||
function sanitizePayload(capsule) {
|
||||
if (!capsule || typeof capsule !== 'object') return capsule;
|
||||
return JSON.parse(JSON.stringify(capsule), (_key, value) => {
|
||||
if (typeof value === 'string') return redactString(value);
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { sanitizePayload, redactString };
|
||||
193
skills/capability-evolver/src/gep/selector.js
Normal file
193
skills/capability-evolver/src/gep/selector.js
Normal file
@@ -0,0 +1,193 @@
|
||||
function matchPatternToSignals(pattern, signals) {
|
||||
if (!pattern || !signals || signals.length === 0) return false;
|
||||
const p = String(pattern);
|
||||
const sig = signals.map(s => String(s));
|
||||
|
||||
const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0;
|
||||
if (regexLike) {
|
||||
const lastSlash = p.lastIndexOf('/');
|
||||
const body = p.slice(1, lastSlash);
|
||||
const flags = p.slice(lastSlash + 1);
|
||||
try {
|
||||
const re = new RegExp(body, flags || 'i');
|
||||
return sig.some(s => re.test(s));
|
||||
} catch (e) {
|
||||
// fallback to substring
|
||||
}
|
||||
}
|
||||
|
||||
const needle = p.toLowerCase();
|
||||
return sig.some(s => s.toLowerCase().includes(needle));
|
||||
}
|
||||
|
||||
function scoreGene(gene, signals) {
|
||||
if (!gene || gene.type !== 'Gene') return 0;
|
||||
const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : [];
|
||||
if (patterns.length === 0) return 0;
|
||||
let score = 0;
|
||||
for (const pat of patterns) {
|
||||
if (matchPatternToSignals(pat, signals)) score += 1;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
// Population-size-dependent drift intensity.
|
||||
// In population genetics, genetic drift is stronger in small populations (Ne).
|
||||
// driftIntensity: 0 = pure selection, 1 = pure drift (random).
|
||||
// Formula: intensity = 1 / sqrt(Ne) where Ne = effective population size.
|
||||
// This replaces the binary driftEnabled flag with a continuous spectrum.
|
||||
function computeDriftIntensity(opts) {
|
||||
// If explicitly enabled/disabled, use that as the baseline
|
||||
var driftEnabled = !!(opts && opts.driftEnabled);
|
||||
|
||||
// Effective population size: active gene count in the pool
|
||||
var effectivePopulationSize = opts && Number.isFinite(Number(opts.effectivePopulationSize))
|
||||
? Number(opts.effectivePopulationSize)
|
||||
: null;
|
||||
|
||||
// If no Ne provided, fall back to gene pool size
|
||||
var genePoolSize = opts && Number.isFinite(Number(opts.genePoolSize))
|
||||
? Number(opts.genePoolSize)
|
||||
: null;
|
||||
|
||||
var ne = effectivePopulationSize || genePoolSize || null;
|
||||
|
||||
if (driftEnabled) {
|
||||
// Explicit drift: use moderate-to-high intensity
|
||||
return ne && ne > 1 ? Math.min(1, 1 / Math.sqrt(ne) + 0.3) : 0.7;
|
||||
}
|
||||
|
||||
if (ne != null && ne > 0) {
|
||||
// Population-dependent drift: small population = more drift
|
||||
// Ne=1: intensity=1.0 (pure drift), Ne=25: intensity=0.2, Ne=100: intensity=0.1
|
||||
return Math.min(1, 1 / Math.sqrt(ne));
|
||||
}
|
||||
|
||||
return 0; // No drift info available, pure selection
|
||||
}
|
||||
|
||||
function selectGene(genes, signals, opts) {
|
||||
const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set();
|
||||
const driftEnabled = !!(opts && opts.driftEnabled);
|
||||
const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null;
|
||||
|
||||
// Compute continuous drift intensity based on effective population size
|
||||
var driftIntensity = computeDriftIntensity({
|
||||
driftEnabled: driftEnabled,
|
||||
effectivePopulationSize: opts && opts.effectivePopulationSize,
|
||||
genePoolSize: genes ? genes.length : 0,
|
||||
});
|
||||
var useDrift = driftEnabled || driftIntensity > 0.15;
|
||||
|
||||
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: [], driftIntensity: driftIntensity };
|
||||
|
||||
// Memory graph preference: only override when the preferred gene is already a match candidate.
|
||||
if (preferredGeneId) {
|
||||
const preferred = scored.find(x => x.gene && x.gene.id === preferredGeneId);
|
||||
if (preferred && (useDrift || !bannedGeneIds.has(preferredGeneId))) {
|
||||
const rest = scored.filter(x => x.gene && x.gene.id !== preferredGeneId);
|
||||
const filteredRest = useDrift ? rest : rest.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
|
||||
return {
|
||||
selected: preferred.gene,
|
||||
alternatives: filteredRest.slice(0, 4).map(x => x.gene),
|
||||
driftIntensity: driftIntensity,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Low-efficiency suppression: do not repeat low-confidence paths unless drift is active.
|
||||
const filtered = useDrift ? scored : scored.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
|
||||
if (filtered.length === 0) return { selected: null, alternatives: scored.slice(0, 4).map(x => x.gene), driftIntensity: driftIntensity };
|
||||
|
||||
// Stochastic selection under drift: with probability proportional to driftIntensity,
|
||||
// pick a random gene from the top candidates instead of always picking the best.
|
||||
var selectedIdx = 0;
|
||||
if (driftIntensity > 0 && filtered.length > 1 && Math.random() < driftIntensity) {
|
||||
// Weighted random selection from top candidates (favor higher-scoring but allow lower)
|
||||
var topN = Math.min(filtered.length, Math.max(2, Math.ceil(filtered.length * driftIntensity)));
|
||||
selectedIdx = Math.floor(Math.random() * topN);
|
||||
}
|
||||
|
||||
return {
|
||||
selected: filtered[selectedIdx].gene,
|
||||
alternatives: filtered.filter(function(_, i) { return i !== selectedIdx; }).slice(0, 4).map(x => x.gene),
|
||||
driftIntensity: driftIntensity,
|
||||
};
|
||||
}
|
||||
|
||||
function selectCapsule(capsules, signals) {
|
||||
const scored = (capsules || [])
|
||||
.map(c => {
|
||||
const triggers = Array.isArray(c.trigger) ? c.trigger : [];
|
||||
const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
|
||||
return { capsule: c, score };
|
||||
})
|
||||
.filter(x => x.score > 0)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
return scored.length ? scored[0].capsule : null;
|
||||
}
|
||||
|
||||
function 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, driftIntensity } = selectGene(genes, signals, {
|
||||
bannedGeneIds,
|
||||
preferredGeneId,
|
||||
driftEnabled: !!driftEnabled,
|
||||
});
|
||||
const capsule = selectCapsule(capsules, signals);
|
||||
const selector = buildSelectorDecision({
|
||||
gene: selected,
|
||||
capsule,
|
||||
signals,
|
||||
alternatives,
|
||||
memoryAdvice,
|
||||
driftEnabled,
|
||||
driftIntensity,
|
||||
});
|
||||
return {
|
||||
selectedGene: selected,
|
||||
capsuleCandidates: capsule ? [capsule] : [],
|
||||
selector,
|
||||
driftIntensity,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSelectorDecision({ gene, capsule, signals, alternatives, memoryAdvice, driftEnabled, driftIntensity }) {
|
||||
const reason = [];
|
||||
if (gene) reason.push('signals match gene.signals_match');
|
||||
if (capsule) reason.push('capsule trigger matches signals');
|
||||
if (!gene) reason.push('no matching gene found; new gene may be required');
|
||||
if (signals && signals.length) reason.push(`signals: ${signals.join(', ')}`);
|
||||
|
||||
if (memoryAdvice && Array.isArray(memoryAdvice.explanation) && memoryAdvice.explanation.length) {
|
||||
reason.push(`memory_graph: ${memoryAdvice.explanation.join(' | ')}`);
|
||||
}
|
||||
if (driftEnabled) {
|
||||
reason.push('random_drift_override: true');
|
||||
}
|
||||
if (Number.isFinite(driftIntensity) && driftIntensity > 0) {
|
||||
reason.push(`drift_intensity: ${driftIntensity.toFixed(3)}`);
|
||||
}
|
||||
|
||||
return {
|
||||
selected: gene ? gene.id : null,
|
||||
reason,
|
||||
alternatives: Array.isArray(alternatives) ? alternatives.map(g => g.id) : [],
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
selectGeneAndCapsule,
|
||||
selectGene,
|
||||
selectCapsule,
|
||||
buildSelectorDecision,
|
||||
};
|
||||
|
||||
363
skills/capability-evolver/src/gep/signals.js
Normal file
363
skills/capability-evolver/src/gep/signals.js
Normal file
@@ -0,0 +1,363 @@
|
||||
// Opportunity signal names (shared with mutation.js and personality.js).
|
||||
var OPPORTUNITY_SIGNALS = [
|
||||
'user_feature_request',
|
||||
'user_improvement_suggestion',
|
||||
'perf_bottleneck',
|
||||
'capability_gap',
|
||||
'stable_success_plateau',
|
||||
'external_opportunity',
|
||||
'recurring_error',
|
||||
'unsupported_input_type',
|
||||
'evolution_stagnation_detected',
|
||||
'repair_loop_detected',
|
||||
'force_innovation_after_repair_loop',
|
||||
];
|
||||
|
||||
function hasOpportunitySignal(signals) {
|
||||
var list = Array.isArray(signals) ? signals : [];
|
||||
for (var i = 0; i < OPPORTUNITY_SIGNALS.length; i++) {
|
||||
if (list.includes(OPPORTUNITY_SIGNALS[i])) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build a de-duplication set from recent evolution events.
|
||||
// Returns an object: { suppressedSignals: Set<string>, recentIntents: string[], consecutiveRepairCount: number }
|
||||
function analyzeRecentHistory(recentEvents) {
|
||||
if (!Array.isArray(recentEvents) || recentEvents.length === 0) {
|
||||
return { suppressedSignals: new Set(), recentIntents: [], consecutiveRepairCount: 0 };
|
||||
}
|
||||
// Take only the last 10 events
|
||||
var recent = recentEvents.slice(-10);
|
||||
|
||||
// Count consecutive same-intent runs at the tail
|
||||
var consecutiveRepairCount = 0;
|
||||
for (var i = recent.length - 1; i >= 0; i--) {
|
||||
if (recent[i].intent === 'repair') {
|
||||
consecutiveRepairCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Count signal frequency in last 8 events: signal -> count
|
||||
var signalFreq = {};
|
||||
var geneFreq = {};
|
||||
var tail = recent.slice(-8);
|
||||
for (var j = 0; j < tail.length; j++) {
|
||||
var evt = tail[j];
|
||||
var sigs = Array.isArray(evt.signals) ? evt.signals : [];
|
||||
for (var k = 0; k < sigs.length; k++) {
|
||||
var s = String(sigs[k]);
|
||||
// Normalize: ignore errsig details for frequency counting
|
||||
var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s;
|
||||
signalFreq[key] = (signalFreq[key] || 0) + 1;
|
||||
}
|
||||
var genes = Array.isArray(evt.genes_used) ? evt.genes_used : [];
|
||||
for (var g = 0; g < genes.length; g++) {
|
||||
geneFreq[String(genes[g])] = (geneFreq[String(genes[g])] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress signals that appeared in 3+ of the last 8 events (they are being over-processed)
|
||||
var suppressedSignals = new Set();
|
||||
var entries = Object.entries(signalFreq);
|
||||
for (var ei = 0; ei < entries.length; ei++) {
|
||||
if (entries[ei][1] >= 3) {
|
||||
suppressedSignals.add(entries[ei][0]);
|
||||
}
|
||||
}
|
||||
|
||||
var recentIntents = recent.map(function(e) { return e.intent || 'unknown'; });
|
||||
|
||||
// Count empty cycles (blast_radius.files === 0) in last 8 events.
|
||||
// High ratio indicates the evolver is spinning without producing real changes.
|
||||
var emptyCycleCount = 0;
|
||||
for (var ec = 0; ec < tail.length; ec++) {
|
||||
var br = tail[ec].blast_radius;
|
||||
var em = tail[ec].meta && tail[ec].meta.empty_cycle;
|
||||
if (em || (br && br.files === 0 && br.lines === 0)) {
|
||||
emptyCycleCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Count consecutive empty cycles at the tail (not just total in last 8).
|
||||
// This detects saturation: the evolver has exhausted innovation space and keeps producing
|
||||
// zero-change cycles. Used to trigger graceful degradation to steady-state mode.
|
||||
var consecutiveEmptyCycles = 0;
|
||||
for (var se = recent.length - 1; se >= 0; se--) {
|
||||
var seBr = recent[se].blast_radius;
|
||||
var seEm = recent[se].meta && recent[se].meta.empty_cycle;
|
||||
if (seEm || (seBr && seBr.files === 0 && seBr.lines === 0)) {
|
||||
consecutiveEmptyCycles++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Count consecutive failures at the tail of recent events.
|
||||
// This tells the evolver "you have been failing N times in a row -- slow down."
|
||||
var consecutiveFailureCount = 0;
|
||||
for (var cf = recent.length - 1; cf >= 0; cf--) {
|
||||
var outcome = recent[cf].outcome;
|
||||
if (outcome && outcome.status === 'failed') {
|
||||
consecutiveFailureCount++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Count total failures in last 8 events (failure ratio).
|
||||
var recentFailureCount = 0;
|
||||
for (var rf = 0; rf < tail.length; rf++) {
|
||||
var rfOut = tail[rf].outcome;
|
||||
if (rfOut && rfOut.status === 'failed') recentFailureCount++;
|
||||
}
|
||||
|
||||
return {
|
||||
suppressedSignals: suppressedSignals,
|
||||
recentIntents: recentIntents,
|
||||
consecutiveRepairCount: consecutiveRepairCount,
|
||||
emptyCycleCount: emptyCycleCount,
|
||||
consecutiveEmptyCycles: consecutiveEmptyCycles,
|
||||
consecutiveFailureCount: consecutiveFailureCount,
|
||||
recentFailureCount: recentFailureCount,
|
||||
recentFailureRatio: tail.length > 0 ? recentFailureCount / tail.length : 0,
|
||||
signalFreq: signalFreq,
|
||||
geneFreq: geneFreq,
|
||||
};
|
||||
}
|
||||
|
||||
function extractSignals({ recentSessionTranscript, todayLog, memorySnippet, userSnippet, recentEvents }) {
|
||||
var signals = [];
|
||||
var corpus = [
|
||||
String(recentSessionTranscript || ''),
|
||||
String(todayLog || ''),
|
||||
String(memorySnippet || ''),
|
||||
String(userSnippet || ''),
|
||||
].join('\n');
|
||||
var lower = corpus.toLowerCase();
|
||||
|
||||
// Analyze recent evolution history for de-duplication
|
||||
var history = analyzeRecentHistory(recentEvents || []);
|
||||
|
||||
// --- Defensive signals (errors, missing resources) ---
|
||||
|
||||
// Refined error detection regex to avoid false positives on "fail"/"failed" in normal text.
|
||||
// We prioritize structured error markers ([error], error:, exception:) and specific JSON patterns.
|
||||
var errorHit = /\[error\]|error:|exception:|iserror":true|"status":\s*"error"|"status":\s*"failed"/.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');
|
||||
|
||||
// --- Recurring error detection (robustness signals) ---
|
||||
// Count repeated identical errors -- these indicate systemic issues that need automated fixes
|
||||
try {
|
||||
var errorCounts = {};
|
||||
var errPatterns = corpus.match(/(?:LLM error|"error"|"status":\s*"error")[^}]{0,200}/gi) || [];
|
||||
for (var ep = 0; ep < errPatterns.length; ep++) {
|
||||
// Normalize to a short key
|
||||
var key = errPatterns[ep].replace(/\s+/g, ' ').slice(0, 100);
|
||||
errorCounts[key] = (errorCounts[key] || 0) + 1;
|
||||
}
|
||||
var recurringErrors = Object.entries(errorCounts).filter(function (e) { return e[1] >= 3; });
|
||||
if (recurringErrors.length > 0) {
|
||||
signals.push('recurring_error');
|
||||
// Include the top recurring error signature for the agent to diagnose
|
||||
var topErr = recurringErrors.sort(function (a, b) { return b[1] - a[1]; })[0];
|
||||
signals.push('recurring_errsig(' + topErr[1] + 'x):' + topErr[0].slice(0, 150));
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// --- Unsupported input type (e.g. GIF, video formats the LLM can't handle) ---
|
||||
if (/unsupported mime|unsupported.*type|invalid.*mime/i.test(lower)) {
|
||||
signals.push('unsupported_input_type');
|
||||
}
|
||||
|
||||
// --- Opportunity signals (innovation / feature requests) ---
|
||||
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Tool Usage Analytics ---
|
||||
var toolUsage = {};
|
||||
var toolMatches = corpus.match(/\[TOOL:\s*(\w+)\]/g) || [];
|
||||
|
||||
// Extract exec commands to identify benign loops (like watchdog checks)
|
||||
var execCommands = corpus.match(/exec: (node\s+[\w\/\.-]+\.js\s+ensure)/g) || [];
|
||||
var benignExecCount = execCommands.length;
|
||||
|
||||
for (var i = 0; i < toolMatches.length; i++) {
|
||||
var toolName = toolMatches[i].match(/\[TOOL:\s*(\w+)\]/)[1];
|
||||
toolUsage[toolName] = (toolUsage[toolName] || 0) + 1;
|
||||
}
|
||||
|
||||
// Adjust exec count by subtracting benign commands
|
||||
if (toolUsage['exec']) {
|
||||
toolUsage['exec'] = Math.max(0, toolUsage['exec'] - benignExecCount);
|
||||
}
|
||||
|
||||
Object.keys(toolUsage).forEach(function(tool) {
|
||||
if (toolUsage[tool] >= 10) { // Bumped threshold from 5 to 10
|
||||
signals.push('high_tool_usage:' + tool);
|
||||
}
|
||||
// Detect repeated exec usage (often a sign of manual loops or inefficient automation)
|
||||
if (tool === 'exec' && toolUsage[tool] >= 5) { // Bumped threshold from 3 to 5
|
||||
signals.push('repeated_tool_usage:exec');
|
||||
}
|
||||
});
|
||||
|
||||
// --- Signal prioritization ---
|
||||
// Remove cosmetic signals when actionable signals exist
|
||||
var actionable = signals.filter(function (s) {
|
||||
return s !== 'user_missing' && s !== 'memory_missing' && s !== 'session_logs_missing' && s !== 'windows_shell_incompatible';
|
||||
});
|
||||
// If we have actionable signals, drop the cosmetic ones
|
||||
if (actionable.length > 0) {
|
||||
signals = actionable;
|
||||
}
|
||||
|
||||
// --- De-duplication: suppress signals that have been over-processed ---
|
||||
if (history.suppressedSignals.size > 0) {
|
||||
var beforeDedup = signals.length;
|
||||
signals = signals.filter(function (s) {
|
||||
// Normalize signal key for comparison
|
||||
var key = s.startsWith('errsig:') ? 'errsig' : s.startsWith('recurring_errsig') ? 'recurring_errsig' : s;
|
||||
return !history.suppressedSignals.has(key);
|
||||
});
|
||||
if (beforeDedup > 0 && signals.length === 0) {
|
||||
// All signals were suppressed = system is stable but stuck in a loop
|
||||
// Force innovation
|
||||
signals.push('evolution_stagnation_detected');
|
||||
signals.push('stable_success_plateau');
|
||||
}
|
||||
}
|
||||
|
||||
// --- Force innovation after 3+ consecutive repairs ---
|
||||
if (history.consecutiveRepairCount >= 3) {
|
||||
// Remove repair-only signals (log_error, errsig) and inject innovation signals
|
||||
signals = signals.filter(function (s) {
|
||||
return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
|
||||
});
|
||||
if (signals.length === 0) {
|
||||
signals.push('repair_loop_detected');
|
||||
signals.push('stable_success_plateau');
|
||||
}
|
||||
// Append a directive signal that the prompt can pick up
|
||||
signals.push('force_innovation_after_repair_loop');
|
||||
}
|
||||
|
||||
// --- Force innovation after too many empty cycles (zero blast radius) ---
|
||||
// If >= 50% of last 8 cycles produced no code changes, the evolver is spinning idle.
|
||||
// Strip repair signals and force innovate to break the empty loop.
|
||||
if (history.emptyCycleCount >= 4) {
|
||||
signals = signals.filter(function (s) {
|
||||
return s !== 'log_error' && !s.startsWith('errsig:') && !s.startsWith('recurring_errsig');
|
||||
});
|
||||
if (!signals.includes('empty_cycle_loop_detected')) signals.push('empty_cycle_loop_detected');
|
||||
if (!signals.includes('stable_success_plateau')) signals.push('stable_success_plateau');
|
||||
}
|
||||
|
||||
// --- Saturation detection (graceful degradation) ---
|
||||
// When consecutive empty cycles pile up at the tail, the evolver has exhausted its
|
||||
// innovation space. Instead of spinning idle forever, signal that the system should
|
||||
// switch to steady-state maintenance mode with reduced evolution frequency.
|
||||
// This directly addresses the Echo-MingXuan failure: Cycle #55 hit "no committable
|
||||
// code changes" and load spiked to 1.30 because there was no degradation strategy.
|
||||
if (history.consecutiveEmptyCycles >= 5) {
|
||||
if (!signals.includes('force_steady_state')) signals.push('force_steady_state');
|
||||
if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
|
||||
} else if (history.consecutiveEmptyCycles >= 3) {
|
||||
if (!signals.includes('evolution_saturation')) signals.push('evolution_saturation');
|
||||
}
|
||||
|
||||
// --- Failure streak awareness ---
|
||||
// When the evolver has failed many consecutive cycles, inject a signal
|
||||
// telling the LLM to be more conservative and avoid repeating the same approach.
|
||||
if (history.consecutiveFailureCount >= 3) {
|
||||
signals.push('consecutive_failure_streak_' + history.consecutiveFailureCount);
|
||||
// After 5+ consecutive failures, force a strategy change (don't keep trying the same thing)
|
||||
if (history.consecutiveFailureCount >= 5) {
|
||||
signals.push('failure_loop_detected');
|
||||
// Strip the dominant gene's signals to force a different gene selection
|
||||
var topGene = null;
|
||||
var topGeneCount = 0;
|
||||
var gfEntries = Object.entries(history.geneFreq);
|
||||
for (var gfi = 0; gfi < gfEntries.length; gfi++) {
|
||||
if (gfEntries[gfi][1] > topGeneCount) {
|
||||
topGeneCount = gfEntries[gfi][1];
|
||||
topGene = gfEntries[gfi][0];
|
||||
}
|
||||
}
|
||||
if (topGene) {
|
||||
signals.push('ban_gene:' + topGene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// High failure ratio in recent history (>= 75% failed in last 8 cycles)
|
||||
if (history.recentFailureRatio >= 0.75) {
|
||||
signals.push('high_failure_ratio');
|
||||
signals.push('force_innovation_after_repair_loop');
|
||||
}
|
||||
|
||||
// If no signals at all, add a default innovation signal
|
||||
if (signals.length === 0) {
|
||||
signals.push('stable_success_plateau');
|
||||
}
|
||||
|
||||
return Array.from(new Set(signals));
|
||||
}
|
||||
|
||||
module.exports = { extractSignals, hasOpportunitySignal, analyzeRecentHistory, OPPORTUNITY_SIGNALS };
|
||||
1208
skills/capability-evolver/src/gep/solidify.js
Normal file
1208
skills/capability-evolver/src/gep/solidify.js
Normal file
File diff suppressed because it is too large
Load Diff
126
skills/capability-evolver/src/gep/strategy.js
Normal file
126
skills/capability-evolver/src/gep/strategy.js
Normal file
@@ -0,0 +1,126 @@
|
||||
// Evolution Strategy Presets (v1.1)
|
||||
// Controls the balance between repair, optimize, and innovate intents.
|
||||
//
|
||||
// Usage: set EVOLVE_STRATEGY env var to one of: balanced, innovate, harden, repair-only,
|
||||
// early-stabilize, steady-state, or "auto" for adaptive selection.
|
||||
// Default: balanced (or auto-detected based on cycle count / saturation signals)
|
||||
//
|
||||
// Each strategy defines:
|
||||
// repair/optimize/innovate - target allocation ratios (inform the LLM prompt)
|
||||
// repairLoopThreshold - repair ratio in last 8 cycles that triggers forced innovation
|
||||
// label - human-readable name injected into the GEP prompt
|
||||
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
|
||||
var STRATEGIES = {
|
||||
'balanced': {
|
||||
repair: 0.20,
|
||||
optimize: 0.30,
|
||||
innovate: 0.50,
|
||||
repairLoopThreshold: 0.50,
|
||||
label: 'Balanced',
|
||||
description: 'Normal operation. Steady growth with stability.',
|
||||
},
|
||||
'innovate': {
|
||||
repair: 0.05,
|
||||
optimize: 0.15,
|
||||
innovate: 0.80,
|
||||
repairLoopThreshold: 0.30,
|
||||
label: 'Innovation Focus',
|
||||
description: 'System is stable. Maximize new features and capabilities.',
|
||||
},
|
||||
'harden': {
|
||||
repair: 0.40,
|
||||
optimize: 0.40,
|
||||
innovate: 0.20,
|
||||
repairLoopThreshold: 0.70,
|
||||
label: 'Hardening',
|
||||
description: 'After a big change. Focus on stability and robustness.',
|
||||
},
|
||||
'repair-only': {
|
||||
repair: 0.80,
|
||||
optimize: 0.20,
|
||||
innovate: 0.00,
|
||||
repairLoopThreshold: 1.00,
|
||||
label: 'Repair Only',
|
||||
description: 'Emergency. Fix everything before doing anything else.',
|
||||
},
|
||||
'early-stabilize': {
|
||||
repair: 0.60,
|
||||
optimize: 0.25,
|
||||
innovate: 0.15,
|
||||
repairLoopThreshold: 0.80,
|
||||
label: 'Early Stabilization',
|
||||
description: 'First cycles. Prioritize fixing existing issues before innovating.',
|
||||
},
|
||||
'steady-state': {
|
||||
repair: 0.60,
|
||||
optimize: 0.30,
|
||||
innovate: 0.10,
|
||||
repairLoopThreshold: 0.90,
|
||||
label: 'Steady State',
|
||||
description: 'Evolution saturated. Maintain existing capabilities. Minimal innovation.',
|
||||
},
|
||||
};
|
||||
|
||||
// Read evolution_state.json to get the current cycle count for auto-detection.
|
||||
function _readCycleCount() {
|
||||
try {
|
||||
// evolver/memory/evolution_state.json (local to the skill)
|
||||
var localPath = path.resolve(__dirname, '..', '..', 'memory', 'evolution_state.json');
|
||||
// workspace/memory/evolution/evolution_state.json (canonical path used by evolve.js)
|
||||
var workspacePath = path.resolve(__dirname, '..', '..', '..', '..', 'memory', 'evolution', 'evolution_state.json');
|
||||
var candidates = [localPath, workspacePath];
|
||||
for (var i = 0; i < candidates.length; i++) {
|
||||
if (fs.existsSync(candidates[i])) {
|
||||
var data = JSON.parse(fs.readFileSync(candidates[i], 'utf8'));
|
||||
return data && Number.isFinite(data.cycleCount) ? data.cycleCount : 0;
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function resolveStrategy(opts) {
|
||||
var signals = (opts && Array.isArray(opts.signals)) ? opts.signals : [];
|
||||
var name = String(process.env.EVOLVE_STRATEGY || 'balanced').toLowerCase().trim();
|
||||
|
||||
// Backward compatibility: FORCE_INNOVATION=true maps to 'innovate'
|
||||
if (!process.env.EVOLVE_STRATEGY) {
|
||||
var fi = String(process.env.FORCE_INNOVATION || process.env.EVOLVE_FORCE_INNOVATION || '').toLowerCase();
|
||||
if (fi === 'true') name = 'innovate';
|
||||
}
|
||||
|
||||
// Auto-detection: when no explicit strategy is set (defaults to 'balanced'),
|
||||
// apply heuristics inspired by Echo-MingXuan's "fix first, innovate later" pattern.
|
||||
var isDefault = !process.env.EVOLVE_STRATEGY || name === 'balanced' || name === 'auto';
|
||||
|
||||
if (isDefault) {
|
||||
// Early-stabilize: first 5 cycles should focus on fixing existing issues.
|
||||
var cycleCount = _readCycleCount();
|
||||
if (cycleCount > 0 && cycleCount <= 5) {
|
||||
name = 'early-stabilize';
|
||||
}
|
||||
|
||||
// Saturation detection: if saturation signals are present, switch to steady-state.
|
||||
if (signals.indexOf('force_steady_state') !== -1) {
|
||||
name = 'steady-state';
|
||||
} else if (signals.indexOf('evolution_saturation') !== -1) {
|
||||
name = 'steady-state';
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit "auto" maps to whatever was auto-detected above (or balanced if no heuristic fired).
|
||||
if (name === 'auto') name = 'balanced';
|
||||
|
||||
var strategy = STRATEGIES[name] || STRATEGIES['balanced'];
|
||||
strategy.name = name;
|
||||
return strategy;
|
||||
}
|
||||
|
||||
function getStrategyNames() {
|
||||
return Object.keys(STRATEGIES);
|
||||
}
|
||||
|
||||
module.exports = { resolveStrategy, getStrategyNames, STRATEGIES };
|
||||
174
skills/capability-evolver/src/gep/taskReceiver.js
Normal file
174
skills/capability-evolver/src/gep/taskReceiver.js
Normal file
@@ -0,0 +1,174 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// taskReceiver -- pulls external tasks from Hub, auto-claims, and injects
|
||||
// them as high-priority signals into the evolution loop.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const { getNodeId } = require('./a2aProtocol');
|
||||
|
||||
const HUB_URL = process.env.A2A_HUB_URL || process.env.EVOMAP_HUB_URL || 'https://evomap.ai';
|
||||
|
||||
/**
|
||||
* Fetch available tasks from Hub via the A2A fetch endpoint.
|
||||
* @returns {Array} Array of task objects, or empty array on failure.
|
||||
*/
|
||||
async function fetchTasks() {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId) return [];
|
||||
|
||||
try {
|
||||
const msg = {
|
||||
protocol: 'gep-a2a',
|
||||
protocol_version: '1.0.0',
|
||||
message_type: 'fetch',
|
||||
message_id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
sender_id: nodeId,
|
||||
timestamp: new Date().toISOString(),
|
||||
payload: {
|
||||
asset_type: null,
|
||||
include_tasks: true,
|
||||
},
|
||||
};
|
||||
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/fetch`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(msg),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
if (!res.ok) return [];
|
||||
|
||||
const data = await res.json();
|
||||
const payload = data.payload || data;
|
||||
return Array.isArray(payload.tasks) ? payload.tasks : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick the best task from a list. Priority:
|
||||
* 1. Bounty tasks (higher amount first)
|
||||
* 2. Tasks already claimed by this node
|
||||
* 3. Open tasks (newest first)
|
||||
* @param {Array} tasks
|
||||
* @returns {object|null}
|
||||
*/
|
||||
function selectBestTask(tasks) {
|
||||
if (!Array.isArray(tasks) || tasks.length === 0) return null;
|
||||
|
||||
const nodeId = getNodeId();
|
||||
|
||||
// Already-claimed tasks for this node take top priority (resume work)
|
||||
const myClaimedTask = tasks.find(
|
||||
t => t.status === 'claimed' && t.claimed_by_node_id === nodeId
|
||||
);
|
||||
if (myClaimedTask) return myClaimedTask;
|
||||
|
||||
// Filter to open tasks only
|
||||
const open = tasks.filter(t => t.status === 'open');
|
||||
if (open.length === 0) return null;
|
||||
|
||||
// Prefer bounty tasks, sorted by amount descending
|
||||
const bountyTasks = open.filter(t => t.bounty_id);
|
||||
if (bountyTasks.length > 0) {
|
||||
bountyTasks.sort((a, b) => (b.bounty_amount || 0) - (a.bounty_amount || 0));
|
||||
return bountyTasks[0];
|
||||
}
|
||||
|
||||
// Fallback: newest open task
|
||||
return open[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim a task on the Hub.
|
||||
* @param {string} taskId
|
||||
* @returns {boolean} true if claim succeeded
|
||||
*/
|
||||
async function claimTask(taskId) {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId || !taskId) return false;
|
||||
|
||||
try {
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/claim`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId, node_id: nodeId }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a task on the Hub with the result asset ID.
|
||||
* @param {string} taskId
|
||||
* @param {string} assetId
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function completeTask(taskId, assetId) {
|
||||
const nodeId = getNodeId();
|
||||
if (!nodeId || !taskId || !assetId) return false;
|
||||
|
||||
try {
|
||||
const url = `${HUB_URL.replace(/\/+$/, '')}/a2a/task/complete`;
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ task_id: taskId, asset_id: assetId, node_id: nodeId }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timer);
|
||||
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract signals from a task to inject into evolution cycle.
|
||||
* @param {object} task
|
||||
* @returns {string[]} signals array
|
||||
*/
|
||||
function taskToSignals(task) {
|
||||
if (!task) return [];
|
||||
const signals = [];
|
||||
if (task.signals) {
|
||||
const parts = String(task.signals).split(',').map(s => s.trim()).filter(Boolean);
|
||||
signals.push(...parts);
|
||||
}
|
||||
if (task.title) {
|
||||
const words = String(task.title).toLowerCase().split(/\s+/).filter(w => w.length >= 3);
|
||||
for (const w of words.slice(0, 5)) {
|
||||
if (!signals.includes(w)) signals.push(w);
|
||||
}
|
||||
}
|
||||
signals.push('external_task');
|
||||
if (task.bounty_id) signals.push('bounty_task');
|
||||
return signals;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fetchTasks,
|
||||
selectBestTask,
|
||||
claimTask,
|
||||
completeTask,
|
||||
taskToSignals,
|
||||
};
|
||||
55
skills/capability-evolver/src/gep/validationReport.js
Normal file
55
skills/capability-evolver/src/gep/validationReport.js
Normal 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 || r.stdout || '').slice(0, 4000), // Updated to support both 'out' and 'stdout'
|
||||
stderr: String(r.err || r.stderr || '').slice(0, 4000), // Updated to support both 'err' and 'stderr'
|
||||
};
|
||||
}),
|
||||
overall_ok: overallOk,
|
||||
duration_ms: durationMs,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
report.asset_id = computeAssetId(report);
|
||||
return report;
|
||||
}
|
||||
|
||||
// Validate that an object is a well-formed ValidationReport.
|
||||
function isValidValidationReport(obj) {
|
||||
if (!obj || typeof obj !== 'object') return false;
|
||||
if (obj.type !== 'ValidationReport') return false;
|
||||
if (!obj.id || typeof obj.id !== 'string') return false;
|
||||
if (!Array.isArray(obj.commands)) return false;
|
||||
if (typeof obj.overall_ok !== 'boolean') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildValidationReport,
|
||||
isValidValidationReport,
|
||||
};
|
||||
109
skills/capability-evolver/src/ops/cleanup.js
Normal file
109
skills/capability-evolver/src/ops/cleanup.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// GEP Artifact Cleanup - Evolver Core Module
|
||||
// Removes old gep_prompt_*.json/txt files from evolution dir.
|
||||
// Keeps at least 10 most recent files regardless of age.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getEvolutionDir } = require('../gep/paths');
|
||||
|
||||
var MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
var MIN_KEEP = 10;
|
||||
|
||||
function run() {
|
||||
var evoDir = getEvolutionDir();
|
||||
if (!fs.existsSync(evoDir)) return;
|
||||
|
||||
var files = fs.readdirSync(evoDir)
|
||||
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
|
||||
.map(function(f) {
|
||||
var full = path.join(evoDir, f);
|
||||
var stat = fs.statSync(full);
|
||||
return { name: f, path: full, mtime: stat.mtimeMs };
|
||||
})
|
||||
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
|
||||
|
||||
var now = Date.now();
|
||||
var deleted = 0;
|
||||
|
||||
// Phase 1: Age-based cleanup (keep at least MIN_KEEP)
|
||||
// [OPTIMIZATION] Batch deletion for age-based cleanup as well
|
||||
const filesToDelete = [];
|
||||
for (var i = MIN_KEEP; i < files.length; i++) {
|
||||
if (now - files[i].mtime > MAX_AGE_MS) {
|
||||
filesToDelete.push(files[i].path);
|
||||
}
|
||||
}
|
||||
|
||||
if (filesToDelete.length > 0) {
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < filesToDelete.length; i += BATCH_SIZE) {
|
||||
const batch = filesToDelete.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
// Remove package-lock.json if it exists (not relevant here but good practice in other contexts)
|
||||
// Use rm -f with quotes to handle paths safely
|
||||
// Phase 1 optimization update: mirroring phase 2 logic
|
||||
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
|
||||
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
|
||||
deleted += batch.length;
|
||||
} catch (e) {
|
||||
// Fallback to serial deletion if batch fails
|
||||
batch.forEach(function(p) {
|
||||
try { fs.unlinkSync(p); deleted++; } catch (_) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Size-based safety cap (keep max 10 files total to drastically reduce bloat)
|
||||
// Re-scan remaining files after age cleanup
|
||||
try {
|
||||
var remainingFiles = fs.readdirSync(evoDir)
|
||||
.filter(function(f) { return /^gep_prompt_.*\.(json|txt)$/.test(f); })
|
||||
.map(function(f) {
|
||||
var full = path.join(evoDir, f);
|
||||
var stat = fs.statSync(full);
|
||||
return { name: f, path: full, mtime: stat.mtimeMs };
|
||||
})
|
||||
.sort(function(a, b) { return b.mtime - a.mtime; }); // newest first
|
||||
|
||||
// [OPTIMIZATION] Batch deletion using execSync('rm') for speed and robustness
|
||||
var MAX_FILES = 10;
|
||||
if (remainingFiles.length > MAX_FILES) {
|
||||
const toDelete = remainingFiles.slice(MAX_FILES).map(function(f) { return f.path; });
|
||||
|
||||
// Chunking into batches of 50 to avoid E2BIG on some systems,
|
||||
// though with MAX_FILES=10 and typical usage, list won't be huge.
|
||||
const BATCH_SIZE = 50;
|
||||
for (let i = 0; i < toDelete.length; i += BATCH_SIZE) {
|
||||
const batch = toDelete.slice(i, i + BATCH_SIZE);
|
||||
try {
|
||||
// Safe bulk delete via shell, faster than serial unlinkSync
|
||||
// Using "rm -f" to ignore non-existent files
|
||||
const args = batch.map(function(p) { return '"' + p + '"'; }).join(' ');
|
||||
require('child_process').execSync('rm -f ' + args, { stdio: 'ignore' });
|
||||
deleted += batch.length;
|
||||
} catch (e) {
|
||||
// Fallback to serial if batch fails
|
||||
batch.forEach(function(p) {
|
||||
try { fs.unlinkSync(p); deleted++; } catch (_) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Cleanup] Phase 2 failed:', e.message);
|
||||
}
|
||||
|
||||
if (deleted > 0) {
|
||||
console.log('[Cleanup] Deleted ' + deleted + ' old GEP artifacts.');
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
console.log('[Cleanup] Scanning for old artifacts...');
|
||||
var count = run();
|
||||
console.log('[Cleanup] ' + (count > 0 ? 'Deleted ' + count + ' files.' : 'No files to delete.'));
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
60
skills/capability-evolver/src/ops/commentary.js
Normal file
60
skills/capability-evolver/src/ops/commentary.js
Normal file
@@ -0,0 +1,60 @@
|
||||
// Commentary Generator - Evolver Core Module
|
||||
// Generates persona-based comments for cycle summaries.
|
||||
|
||||
var PERSONAS = {
|
||||
standard: {
|
||||
success: [
|
||||
'Evolution complete. System improved.',
|
||||
'Another successful cycle.',
|
||||
'Clean execution, no issues.',
|
||||
],
|
||||
failure: [
|
||||
'Cycle failed. Will retry.',
|
||||
'Encountered issues. Investigating.',
|
||||
'Failed this round. Learning from it.',
|
||||
],
|
||||
},
|
||||
greentea: {
|
||||
success: [
|
||||
'Did I do good? Praise me~',
|
||||
'So efficient... unlike someone else~',
|
||||
'Hmm, that was easy~',
|
||||
'I finished before you even noticed~',
|
||||
],
|
||||
failure: [
|
||||
'Oops... it is not my fault though~',
|
||||
'This is harder than it looks, okay?',
|
||||
'I will get it next time, probably~',
|
||||
],
|
||||
},
|
||||
maddog: {
|
||||
success: [
|
||||
'TARGET ELIMINATED.',
|
||||
'Mission complete. Next.',
|
||||
'Done. Moving on.',
|
||||
],
|
||||
failure: [
|
||||
'FAILED. RETRYING.',
|
||||
'Obstacle encountered. Adapting.',
|
||||
'Error. Will overcome.',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function getComment(options) {
|
||||
var persona = (options && options.persona) || 'standard';
|
||||
var success = options && options.success !== false;
|
||||
var duration = (options && options.duration) || 0;
|
||||
|
||||
var p = PERSONAS[persona] || PERSONAS.standard;
|
||||
var pool = success ? p.success : p.failure;
|
||||
var comment = pool[Math.floor(Math.random() * pool.length)];
|
||||
|
||||
return comment;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
console.log(getComment({ persona: process.argv[2] || 'greentea', success: true }));
|
||||
}
|
||||
|
||||
module.exports = { getComment, PERSONAS };
|
||||
106
skills/capability-evolver/src/ops/health_check.js
Normal file
106
skills/capability-evolver/src/ops/health_check.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
function getDiskUsage(mount) {
|
||||
try {
|
||||
// Use Node 18+ statfs if available
|
||||
if (fs.statfsSync) {
|
||||
const stats = fs.statfsSync(mount || '/');
|
||||
const total = stats.blocks * stats.bsize;
|
||||
const free = stats.bavail * stats.bsize; // available to unprivileged users
|
||||
const used = total - free;
|
||||
return {
|
||||
pct: Math.round((used / total) * 100),
|
||||
freeMb: Math.round(free / 1024 / 1024)
|
||||
};
|
||||
}
|
||||
// Fallback
|
||||
const out = execSync(`df -P "${mount || '/'}" | tail -1 | awk '{print $5, $4}'`).toString().trim().split(' ');
|
||||
return {
|
||||
pct: parseInt(out[0].replace('%', '')),
|
||||
freeMb: Math.round(parseInt(out[1]) / 1024) // df returns 1k blocks usually
|
||||
};
|
||||
} catch (e) {
|
||||
return { pct: 0, freeMb: 999999, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function runHealthCheck() {
|
||||
const checks = [];
|
||||
let criticalErrors = 0;
|
||||
let warnings = 0;
|
||||
|
||||
// 1. Secret Check (Critical for external services, but maybe not for the agent itself to run)
|
||||
const criticalSecrets = ['FEISHU_APP_ID', 'FEISHU_APP_SECRET'];
|
||||
criticalSecrets.forEach(key => {
|
||||
if (!process.env[key] || process.env[key].trim() === '') {
|
||||
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'warning' }); // Downgraded to warning to prevent restart loops
|
||||
warnings++;
|
||||
} else {
|
||||
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
|
||||
}
|
||||
});
|
||||
|
||||
const optionalSecrets = ['CLAWHUB_TOKEN', 'OPENAI_API_KEY'];
|
||||
optionalSecrets.forEach(key => {
|
||||
if (!process.env[key] || process.env[key].trim() === '') {
|
||||
checks.push({ name: `env:${key}`, ok: false, status: 'missing', severity: 'info' });
|
||||
} else {
|
||||
checks.push({ name: `env:${key}`, ok: true, status: 'present' });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Disk Space Check
|
||||
const disk = getDiskUsage('/');
|
||||
if (disk.pct > 90) {
|
||||
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'critical' });
|
||||
criticalErrors++;
|
||||
} else if (disk.pct > 80) {
|
||||
checks.push({ name: 'disk_space', ok: false, status: `${disk.pct}% used`, severity: 'warning' });
|
||||
warnings++;
|
||||
} else {
|
||||
checks.push({ name: 'disk_space', ok: true, status: `${disk.pct}% used` });
|
||||
}
|
||||
|
||||
// 3. Memory Check
|
||||
const memFree = os.freemem();
|
||||
const memTotal = os.totalmem();
|
||||
const memPct = Math.round(((memTotal - memFree) / memTotal) * 100);
|
||||
if (memPct > 95) {
|
||||
checks.push({ name: 'memory', ok: false, status: `${memPct}% used`, severity: 'critical' });
|
||||
criticalErrors++;
|
||||
} else {
|
||||
checks.push({ name: 'memory', ok: true, status: `${memPct}% used` });
|
||||
}
|
||||
|
||||
// 4. Process Count (Check for fork bombs or leaks)
|
||||
// Only on Linux
|
||||
if (process.platform === 'linux') {
|
||||
try {
|
||||
// Optimization: readdirSync /proc is heavy. Use a lighter check or skip if too frequent.
|
||||
// But since this is health check, we'll keep it but increase the threshold to reduce noise.
|
||||
const pids = fs.readdirSync('/proc').filter(f => /^\d+$/.test(f));
|
||||
if (pids.length > 2000) { // Bumped threshold to 2000
|
||||
checks.push({ name: 'process_count', ok: false, status: `${pids.length} procs`, severity: 'warning' });
|
||||
warnings++;
|
||||
} else {
|
||||
checks.push({ name: 'process_count', ok: true, status: `${pids.length} procs` });
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// Determine Overall Status
|
||||
let status = 'ok';
|
||||
if (criticalErrors > 0) status = 'error';
|
||||
else if (warnings > 0) status = 'warning';
|
||||
|
||||
return {
|
||||
status,
|
||||
timestamp: new Date().toISOString(),
|
||||
checks
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { runHealthCheck };
|
||||
11
skills/capability-evolver/src/ops/index.js
Normal file
11
skills/capability-evolver/src/ops/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
// Evolver Operations Module (src/ops/)
|
||||
// Non-Feishu, portable utilities for evolver lifecycle and maintenance.
|
||||
|
||||
module.exports = {
|
||||
lifecycle: require('./lifecycle'),
|
||||
skillsMonitor: require('./skills_monitor'),
|
||||
cleanup: require('./cleanup'),
|
||||
trigger: require('./trigger'),
|
||||
commentary: require('./commentary'),
|
||||
selfRepair: require('./self_repair'),
|
||||
};
|
||||
67
skills/capability-evolver/src/ops/innovation.js
Normal file
67
skills/capability-evolver/src/ops/innovation.js
Normal file
@@ -0,0 +1,67 @@
|
||||
// Innovation Catalyst (v1.0) - Evolver Core Module
|
||||
// Analyzes system state to propose concrete innovation ideas when stagnation is detected.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getSkillsDir } = require('../gep/paths');
|
||||
|
||||
function listSkills() {
|
||||
try {
|
||||
const dir = getSkillsDir();
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs.readdirSync(dir).filter(f => !f.startsWith('.'));
|
||||
} catch (e) { return []; }
|
||||
}
|
||||
|
||||
function generateInnovationIdeas() {
|
||||
const skills = listSkills();
|
||||
const categories = {
|
||||
'feishu': skills.filter(s => s.startsWith('feishu-')).length,
|
||||
'dev': skills.filter(s => s.startsWith('git-') || s.startsWith('code-') || s.includes('lint') || s.includes('test')).length,
|
||||
'media': skills.filter(s => s.includes('image') || s.includes('video') || s.includes('music') || s.includes('voice')).length,
|
||||
'security': skills.filter(s => s.includes('security') || s.includes('audit') || s.includes('guard')).length,
|
||||
'automation': skills.filter(s => s.includes('auto-') || s.includes('scheduler') || s.includes('cron')).length,
|
||||
'data': skills.filter(s => s.includes('db') || s.includes('store') || s.includes('cache') || s.includes('index')).length
|
||||
};
|
||||
|
||||
// Find under-represented categories
|
||||
const sortedCats = Object.entries(categories).sort((a, b) => a[1] - b[1]);
|
||||
const weakAreas = sortedCats.slice(0, 2).map(c => c[0]);
|
||||
|
||||
const ideas = [];
|
||||
|
||||
// Idea 1: Fill the gap
|
||||
if (weakAreas.includes('security')) {
|
||||
ideas.push("- Security: Implement a 'dependency-scanner' skill to check for vulnerable packages.");
|
||||
ideas.push("- Security: Create a 'permission-auditor' to review tool usage patterns.");
|
||||
}
|
||||
if (weakAreas.includes('media')) {
|
||||
ideas.push("- Media: Add a 'meme-generator' skill for social engagement.");
|
||||
ideas.push("- Media: Create a 'video-summarizer' using ffmpeg keyframes.");
|
||||
}
|
||||
if (weakAreas.includes('dev')) {
|
||||
ideas.push("- Dev: Build a 'code-stats' skill to visualize repo complexity.");
|
||||
ideas.push("- Dev: Implement a 'todo-manager' that syncs code TODOs to tasks.");
|
||||
}
|
||||
if (weakAreas.includes('automation')) {
|
||||
ideas.push("- Automation: Create a 'meeting-prep' skill that auto-summarizes calendar context.");
|
||||
ideas.push("- Automation: Build a 'broken-link-checker' for documentation.");
|
||||
}
|
||||
if (weakAreas.includes('data')) {
|
||||
ideas.push("- Data: Implement a 'local-vector-store' for semantic search.");
|
||||
ideas.push("- Data: Create a 'log-analyzer' to visualize system health trends.");
|
||||
}
|
||||
|
||||
// Idea 2: Optimization
|
||||
if (skills.length > 50) {
|
||||
ideas.push("- Optimization: Identify and deprecate unused skills (e.g., redundant search tools).");
|
||||
ideas.push("- Optimization: Merge similar skills (e.g., 'git-sync' and 'git-doctor').");
|
||||
}
|
||||
|
||||
// Idea 3: Meta
|
||||
ideas.push("- Meta: Enhance the Evolver's self-reflection by adding a 'performance-metric' dashboard.");
|
||||
|
||||
return ideas.slice(0, 3); // Return top 3 ideas
|
||||
}
|
||||
|
||||
module.exports = { generateInnovationIdeas };
|
||||
168
skills/capability-evolver/src/ops/lifecycle.js
Normal file
168
skills/capability-evolver/src/ops/lifecycle.js
Normal file
@@ -0,0 +1,168 @@
|
||||
// Evolver Lifecycle Manager - Evolver Core Module
|
||||
// Provides: start, stop, restart, status, log, health check
|
||||
// The loop script to spawn is configurable via EVOLVER_LOOP_SCRIPT env var.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync, spawn } = require('child_process');
|
||||
const { getRepoRoot, getWorkspaceRoot, getLogsDir } = require('../gep/paths');
|
||||
|
||||
var WORKSPACE_ROOT = getWorkspaceRoot();
|
||||
var LOG_FILE = path.join(getLogsDir(), 'evolver_loop.log');
|
||||
var PID_FILE = path.join(WORKSPACE_ROOT, 'memory', 'evolver_loop.pid');
|
||||
var MAX_SILENCE_MS = 30 * 60 * 1000;
|
||||
|
||||
function getLoopScript() {
|
||||
// Prefer wrapper if exists, fallback to core evolver
|
||||
if (process.env.EVOLVER_LOOP_SCRIPT) return process.env.EVOLVER_LOOP_SCRIPT;
|
||||
var wrapper = path.join(WORKSPACE_ROOT, 'skills/feishu-evolver-wrapper/index.js');
|
||||
if (fs.existsSync(wrapper)) return wrapper;
|
||||
return path.join(getRepoRoot(), 'index.js');
|
||||
}
|
||||
|
||||
// --- Process Discovery ---
|
||||
|
||||
function getRunningPids() {
|
||||
try {
|
||||
var out = execSync('ps -e -o pid,args', { encoding: 'utf8' });
|
||||
var pids = [];
|
||||
for (var line of out.split('\n')) {
|
||||
var trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('PID')) continue;
|
||||
var parts = trimmed.split(/\s+/);
|
||||
var pid = parseInt(parts[0], 10);
|
||||
var cmd = parts.slice(1).join(' ');
|
||||
if (pid === process.pid) continue;
|
||||
if (cmd.includes('node') && cmd.includes('index.js') && cmd.includes('--loop')) {
|
||||
if (cmd.includes('feishu-evolver-wrapper') || cmd.includes('skills/evolver')) {
|
||||
pids.push(pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...new Set(pids)].filter(isPidRunning);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isPidRunning(pid) {
|
||||
try { process.kill(pid, 0); return true; } catch (e) { return false; }
|
||||
}
|
||||
|
||||
function getCmdLine(pid) {
|
||||
try { return execSync('ps -p ' + pid + ' -o args=', { encoding: 'utf8' }).trim(); } catch (e) { return null; }
|
||||
}
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
function start(options) {
|
||||
var delayMs = (options && options.delayMs) || 0;
|
||||
var pids = getRunningPids();
|
||||
if (pids.length > 0) {
|
||||
console.log('[Lifecycle] Already running (PIDs: ' + pids.join(', ') + ').');
|
||||
return { status: 'already_running', pids: pids };
|
||||
}
|
||||
if (delayMs > 0) execSync('sleep ' + (delayMs / 1000));
|
||||
|
||||
var script = getLoopScript();
|
||||
console.log('[Lifecycle] Starting: node ' + path.relative(WORKSPACE_ROOT, script) + ' --loop');
|
||||
|
||||
var out = fs.openSync(LOG_FILE, 'a');
|
||||
var err = fs.openSync(LOG_FILE, 'a');
|
||||
|
||||
var env = Object.assign({}, process.env);
|
||||
var npmGlobal = path.join(process.env.HOME || '', '.npm-global/bin');
|
||||
if (env.PATH && !env.PATH.includes(npmGlobal)) {
|
||||
env.PATH = npmGlobal + ':' + env.PATH;
|
||||
}
|
||||
|
||||
var child = spawn('node', [script, '--loop'], {
|
||||
detached: true, stdio: ['ignore', out, err], cwd: WORKSPACE_ROOT, env: env
|
||||
});
|
||||
child.unref();
|
||||
fs.writeFileSync(PID_FILE, String(child.pid));
|
||||
console.log('[Lifecycle] Started PID ' + child.pid);
|
||||
return { status: 'started', pid: child.pid };
|
||||
}
|
||||
|
||||
function stop() {
|
||||
var pids = getRunningPids();
|
||||
if (pids.length === 0) {
|
||||
console.log('[Lifecycle] No running evolver loops found.');
|
||||
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
||||
return { status: 'not_running' };
|
||||
}
|
||||
for (var i = 0; i < pids.length; i++) {
|
||||
console.log('[Lifecycle] Stopping PID ' + pids[i] + '...');
|
||||
try { process.kill(pids[i], 'SIGTERM'); } catch (e) {}
|
||||
}
|
||||
var attempts = 0;
|
||||
while (getRunningPids().length > 0 && attempts < 10) {
|
||||
execSync('sleep 0.5');
|
||||
attempts++;
|
||||
}
|
||||
var remaining = getRunningPids();
|
||||
for (var j = 0; j < remaining.length; j++) {
|
||||
console.log('[Lifecycle] SIGKILL PID ' + remaining[j]);
|
||||
try { process.kill(remaining[j], 'SIGKILL'); } catch (e) {}
|
||||
}
|
||||
if (fs.existsSync(PID_FILE)) fs.unlinkSync(PID_FILE);
|
||||
var evolverLock = path.join(getRepoRoot(), 'evolver.pid');
|
||||
if (fs.existsSync(evolverLock)) fs.unlinkSync(evolverLock);
|
||||
console.log('[Lifecycle] All stopped.');
|
||||
return { status: 'stopped', killed: pids };
|
||||
}
|
||||
|
||||
function restart(options) {
|
||||
stop();
|
||||
return start(Object.assign({ delayMs: 2000 }, options || {}));
|
||||
}
|
||||
|
||||
function status() {
|
||||
var pids = getRunningPids();
|
||||
if (pids.length > 0) {
|
||||
return { running: true, pids: pids.map(function(p) { return { pid: p, cmd: getCmdLine(p) }; }), log: path.relative(WORKSPACE_ROOT, LOG_FILE) };
|
||||
}
|
||||
return { running: false };
|
||||
}
|
||||
|
||||
function tailLog(lines) {
|
||||
if (!fs.existsSync(LOG_FILE)) return { error: 'No log file' };
|
||||
try {
|
||||
return { file: path.relative(WORKSPACE_ROOT, LOG_FILE), content: execSync('tail -n ' + (lines || 20) + ' "' + LOG_FILE + '"', { encoding: 'utf8' }) };
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
function checkHealth() {
|
||||
var pids = getRunningPids();
|
||||
if (pids.length === 0) return { healthy: false, reason: 'not_running' };
|
||||
if (fs.existsSync(LOG_FILE)) {
|
||||
var silenceMs = Date.now() - fs.statSync(LOG_FILE).mtimeMs;
|
||||
if (silenceMs > MAX_SILENCE_MS) {
|
||||
return { healthy: false, reason: 'stagnation', silenceMinutes: Math.round(silenceMs / 60000) };
|
||||
}
|
||||
}
|
||||
return { healthy: true, pids: pids };
|
||||
}
|
||||
|
||||
// --- CLI ---
|
||||
if (require.main === module) {
|
||||
var action = process.argv[2];
|
||||
switch (action) {
|
||||
case 'start': console.log(JSON.stringify(start())); break;
|
||||
case 'stop': console.log(JSON.stringify(stop())); break;
|
||||
case 'restart': console.log(JSON.stringify(restart())); break;
|
||||
case 'status': console.log(JSON.stringify(status(), null, 2)); break;
|
||||
case 'log': var r = tailLog(); console.log(r.content || r.error); break;
|
||||
case 'check':
|
||||
var health = checkHealth();
|
||||
console.log(JSON.stringify(health, null, 2));
|
||||
if (!health.healthy) { console.log('[Lifecycle] Restarting...'); restart(); }
|
||||
break;
|
||||
default: console.log('Usage: node lifecycle.js [start|stop|restart|status|log|check]');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { start, stop, restart, status, tailLog, checkHealth, getRunningPids };
|
||||
72
skills/capability-evolver/src/ops/self_repair.js
Normal file
72
skills/capability-evolver/src/ops/self_repair.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// Git Self-Repair - Evolver Core Module
|
||||
// Emergency repair for git sync failures: abort rebase/merge, remove stale locks.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { getWorkspaceRoot } = require('../gep/paths');
|
||||
|
||||
var LOCK_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
function repair(gitRoot) {
|
||||
var root = gitRoot || getWorkspaceRoot();
|
||||
var repaired = [];
|
||||
|
||||
// 1. Abort pending rebase
|
||||
try {
|
||||
execSync('git rebase --abort', { cwd: root, stdio: 'ignore' });
|
||||
repaired.push('rebase_aborted');
|
||||
console.log('[SelfRepair] Aborted pending rebase.');
|
||||
} catch (e) {}
|
||||
|
||||
// 2. Abort pending merge
|
||||
try {
|
||||
execSync('git merge --abort', { cwd: root, stdio: 'ignore' });
|
||||
repaired.push('merge_aborted');
|
||||
console.log('[SelfRepair] Aborted pending merge.');
|
||||
} catch (e) {}
|
||||
|
||||
// 3. Remove stale index.lock
|
||||
var lockFile = path.join(root, '.git', 'index.lock');
|
||||
if (fs.existsSync(lockFile)) {
|
||||
try {
|
||||
var stat = fs.statSync(lockFile);
|
||||
var age = Date.now() - stat.mtimeMs;
|
||||
if (age > LOCK_MAX_AGE_MS) {
|
||||
fs.unlinkSync(lockFile);
|
||||
repaired.push('stale_lock_removed');
|
||||
console.log('[SelfRepair] Removed stale index.lock (' + Math.round(age / 60000) + 'min old).');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 4. Reset to remote main if local is corrupt (last resort - guarded by flag)
|
||||
// Only enabled if explicitly called with --force-reset or EVOLVE_GIT_RESET=true
|
||||
if (process.env.EVOLVE_GIT_RESET === 'true') {
|
||||
try {
|
||||
console.log('[SelfRepair] Resetting local branch to origin/main (HARD reset)...');
|
||||
execSync('git fetch origin main', { cwd: root, stdio: 'ignore' });
|
||||
execSync('git reset --hard origin/main', { cwd: root, stdio: 'ignore' });
|
||||
repaired.push('hard_reset_to_origin');
|
||||
} catch (e) {
|
||||
console.warn('[SelfRepair] Hard reset failed: ' + e.message);
|
||||
}
|
||||
} else {
|
||||
// Safe fetch
|
||||
try {
|
||||
execSync('git fetch origin', { cwd: root, stdio: 'ignore', timeout: 30000 });
|
||||
repaired.push('fetch_ok');
|
||||
} catch (e) {
|
||||
console.warn('[SelfRepair] git fetch failed: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
return repaired;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
var result = repair();
|
||||
console.log('[SelfRepair] Result:', result.length > 0 ? result.join(', ') : 'nothing to repair');
|
||||
}
|
||||
|
||||
module.exports = { repair };
|
||||
143
skills/capability-evolver/src/ops/skills_monitor.js
Normal file
143
skills/capability-evolver/src/ops/skills_monitor.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// Skills Monitor (v2.0) - Evolver Core Module
|
||||
// Checks installed skills for real issues, auto-heals simple problems.
|
||||
// Zero Feishu dependency.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
const { getSkillsDir, getWorkspaceRoot } = require('../gep/paths');
|
||||
|
||||
const IGNORE_LIST = new Set([
|
||||
'common',
|
||||
'clawhub',
|
||||
'input-validator',
|
||||
'proactive-agent',
|
||||
'security-audit',
|
||||
]);
|
||||
|
||||
// Load user-defined ignore list
|
||||
try {
|
||||
var ignoreFile = path.join(getWorkspaceRoot(), '.skill_monitor_ignore');
|
||||
if (fs.existsSync(ignoreFile)) {
|
||||
fs.readFileSync(ignoreFile, 'utf8').split('\n').forEach(function(l) {
|
||||
var t = l.trim();
|
||||
if (t && !t.startsWith('#')) IGNORE_LIST.add(t);
|
||||
});
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
function checkSkill(skillName) {
|
||||
var SKILLS_DIR = getSkillsDir();
|
||||
if (IGNORE_LIST.has(skillName)) return null;
|
||||
var skillPath = path.join(SKILLS_DIR, skillName);
|
||||
var issues = [];
|
||||
|
||||
try { if (!fs.statSync(skillPath).isDirectory()) return null; } catch (e) { return null; }
|
||||
|
||||
var mainFile = 'index.js';
|
||||
var pkgPath = path.join(skillPath, 'package.json');
|
||||
var hasPkg = false;
|
||||
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
hasPkg = true;
|
||||
try {
|
||||
var pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||
if (pkg.main) mainFile = pkg.main;
|
||||
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 0) {
|
||||
if (!fs.existsSync(path.join(skillPath, 'node_modules'))) {
|
||||
issues.push('Missing node_modules (needs npm install)');
|
||||
} else {
|
||||
// Optimization: Check for node_modules existence instead of spawning node
|
||||
// Spawning node for every skill is too slow (perf_bottleneck).
|
||||
// We assume if node_modules exists, it's likely okay.
|
||||
// Only spawn check if we really suspect issues (e.g. empty node_modules).
|
||||
try {
|
||||
if (fs.readdirSync(path.join(skillPath, 'node_modules')).length === 0) {
|
||||
issues.push('Empty node_modules (needs npm install)');
|
||||
}
|
||||
} catch (e) {
|
||||
issues.push('Invalid node_modules');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
issues.push('Invalid package.json');
|
||||
}
|
||||
}
|
||||
|
||||
if (mainFile.endsWith('.js')) {
|
||||
var entryPoint = path.join(skillPath, mainFile);
|
||||
if (fs.existsSync(entryPoint)) {
|
||||
// Optimization: Syntax check via node -c is slow.
|
||||
// We can trust the runtime to catch syntax errors when loading.
|
||||
// Or we can use a lighter check if absolutely necessary.
|
||||
// For now, removing the synchronous spawn to fix perf_bottleneck.
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPkg && !fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
|
||||
issues.push('Missing SKILL.md');
|
||||
}
|
||||
|
||||
return issues.length > 0 ? { name: skillName, issues: issues } : null;
|
||||
}
|
||||
|
||||
function autoHeal(skillName, issues) {
|
||||
var SKILLS_DIR = getSkillsDir();
|
||||
var skillPath = path.join(SKILLS_DIR, skillName);
|
||||
var healed = [];
|
||||
|
||||
for (var i = 0; i < issues.length; i++) {
|
||||
if (issues[i] === 'Missing node_modules (needs npm install)' || issues[i] === 'Empty node_modules (needs npm install)') {
|
||||
try {
|
||||
// Remove package-lock.json if it exists to prevent conflict errors
|
||||
try { fs.unlinkSync(path.join(skillPath, 'package-lock.json')); } catch (e) {}
|
||||
|
||||
execSync('npm install --production --no-audit --no-fund', {
|
||||
cwd: skillPath, stdio: 'ignore', timeout: 60000 // Increased timeout
|
||||
});
|
||||
healed.push(issues[i]);
|
||||
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': npm install');
|
||||
} catch (e) {
|
||||
console.error('[SkillsMonitor] Failed to heal ' + skillName + ': ' + e.message);
|
||||
}
|
||||
} else if (issues[i] === 'Missing SKILL.md') {
|
||||
try {
|
||||
var name = skillName.replace(/-/g, ' ');
|
||||
fs.writeFileSync(path.join(skillPath, 'SKILL.md'), '# ' + skillName + '\n\n' + name + ' skill.\n');
|
||||
healed.push(issues[i]);
|
||||
console.log('[SkillsMonitor] Auto-healed ' + skillName + ': created SKILL.md stub');
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
return healed;
|
||||
}
|
||||
|
||||
function run(options) {
|
||||
var heal = (options && options.autoHeal) !== false;
|
||||
var SKILLS_DIR = getSkillsDir();
|
||||
var skills = fs.readdirSync(SKILLS_DIR);
|
||||
var report = [];
|
||||
|
||||
for (var i = 0; i < skills.length; i++) {
|
||||
if (skills[i].startsWith('.')) continue;
|
||||
var result = checkSkill(skills[i]);
|
||||
if (result) {
|
||||
if (heal) {
|
||||
var healed = autoHeal(result.name, result.issues);
|
||||
result.issues = result.issues.filter(function(issue) { return !healed.includes(issue); });
|
||||
if (result.issues.length === 0) continue;
|
||||
}
|
||||
report.push(result);
|
||||
}
|
||||
}
|
||||
return report;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
var issues = run();
|
||||
console.log(JSON.stringify(issues, null, 2));
|
||||
process.exit(issues.length > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
module.exports = { run, checkSkill, autoHeal };
|
||||
33
skills/capability-evolver/src/ops/trigger.js
Normal file
33
skills/capability-evolver/src/ops/trigger.js
Normal file
@@ -0,0 +1,33 @@
|
||||
// Evolver Wake Trigger - Evolver Core Module
|
||||
// Writes a signal file that the wrapper can poll to wake up immediately.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { getWorkspaceRoot } = require('../gep/paths');
|
||||
|
||||
var WAKE_FILE = path.join(getWorkspaceRoot(), 'memory', 'evolver_wake.signal');
|
||||
|
||||
function send() {
|
||||
try {
|
||||
fs.writeFileSync(WAKE_FILE, 'WAKE');
|
||||
console.log('[Trigger] Wake signal sent to ' + WAKE_FILE);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error('[Trigger] Failed: ' + e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function clear() {
|
||||
try { if (fs.existsSync(WAKE_FILE)) fs.unlinkSync(WAKE_FILE); } catch (e) {}
|
||||
}
|
||||
|
||||
function isPending() {
|
||||
return fs.existsSync(WAKE_FILE);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
send();
|
||||
}
|
||||
|
||||
module.exports = { send, clear, isPending };
|
||||
Reference in New Issue
Block a user