Skill: besty0121/experiment-notes (version 1.0.0)

--- name: experiment-notes description: Track, search, and learn from experiments. Automatic logging of trial-and-error, success/failure patterns, and distilled lessons. Prevents repeating mistakes. tags: - memory - learning - agent - productivity requires: bins: - python env: [] --- # 实验笔记系统 / Experiment Notes 让 Agent 从经验中学习，不再重复犯错。 Every agent fails. Smart agents remember why. ## 核心理念 普通 Agent： ``` 试了 → 失败了 → 忘了 → 下次又试 → 又失败了 ``` 用了实验笔记的 Agent： ``` 试了 → 失败了 → 记下来了 → 下次查一下 → 直接用对的方案 ``` ## 安装 skill 安装后，数据存储在 `~/.openclaw/memory/experiments/`： - `experiments.jsonl` — 所有实验记录 - `lessons.jsonl` — 提炼的最佳实践 首次使用会自动创建目录。 ## 命令参考 所有命令通过 `python <skill_dir>/scripts/expnote.py` 调用。 ### 记录实验 ```bash python expnote.py log \ --task "从 Docker Hub 拉取镜像" \ --outcome failure \ --tags docker,network \ --cmd "docker pull nginx" \ --error "Error response from daemon: Get https://registry-1.docker.io/v2/: net/http: request canceled" \ --fix "配置 Docker 镜像加速器" \ --lesson "国内环境拉 Docker 镜像大概率超时，先配加速器" ``` **outcome 取值：** - `success` — 成功了 ✅ - `failure` — 失败了 ❌ - `partial` — 部分成功 ⚠️ ### 搜索历史 ```bash # 全文搜索 python expnote.py search "docker" # 限制结果数 python expnote.py search "clawhub publish" --limit 5 ``` ### 查找类似任务 在开始新任务前，先查查之前有没有做过类似的： ```bash python expnote.py similar "发布 skill 到 ClawHub" ``` ### 查看经验教训 ```bash # 所有教训 python expnote.py lessons # 按标签过滤 python expnote.py lessons --tag docker ``` ### 统计概览 ```bash python expnote.py stats ``` 显示：总实验数、成功率/失败率、热门标签、最近活动。 ### 提炼最佳实践 从多次实验中总结通用经验： ```bash python expnote.py distill \ --tags clawhub,publish \ --lesson "clawhub publish 必须显式指定 --version，不支持自动递增" ``` ## 给 Agent 的使用指南 在你的 `AGENTS.md` 中添加以下规则： ```markdown ## 实验笔记 每次尝试新操作时： 1. **之前**：运行 `expnote.py similar "任务描述"`，看看有没有历史经验 2. **之后**：无论成功失败，运行 `expnote.py log` 记录结果 3. **定期**：review 实验记录，用 `expnote.py distill` 提炼通用教训 记录格式： - task: 简短描述（动词开头） - outcome: success / failure / partial - tags: 相关技术/工具（逗号分隔） - cmd: 实际执行的命令 - error: 报错信息（失败时） - fix: 怎么修复的 - lesson: 一句话总结（最重要！） ``` ## 数据格式 每条实验记录（experiments.jsonl 中的一行）： ```json { "id": "exp_a1b2c3d4e5f6", "timestamp": "2026-04-02T08:58:00+08:00", "task": "发布 ClawHub skill", "outcome": "partial", "attempt": { "command": "clawhub publish ./skill --no-input", "description": "发布小红书浏览器 skill" }, "result": { "error": "Error: --version must be valid semver", "fix": "加上 --version 1.1.0" }, "tags": ["clawhub", "publish", "skill"], "lesson": "clawhub publish 必须显式指定 --version" } ``` ## 目录结构 ``` experiment-notes/ ├── SKILL.md # 本文件 ├── scripts/ │ └── expnote.py # CLI 工具 └── templates/ └── agents-snippet.md # AGENTS.md 集成片段 ``` ## 设计理念 - **JSONL 格式**：追加写入，不怕并发，方便 grep - **纯文件存储**：不需要数据库，不需要服务端 - **标签系统**：多维分类，支持按领域过滤 - **提炼机制**：不只是记录，还要总结成可复用的知识 ## 局限性 - 搜索是关键词匹配，不是语义搜索（够用但不完美） - 跨 agent 共享需要手动同步文件 - 没有自动清理机制（可以手动删除旧记录）

path: scripts/expnote.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
expnote - Experiment Notes System for AI Agents

Track, search, and learn from your experiments.

Usage:
  python expnote.py log --task "..." --outcome success|failure|partial --tags tag1,tag2 [--cmd "..." --error "..." --fix "..." --lesson "..."]
  python expnote.py search <query> [--limit N]
  python expnote.py similar <task_description> [--limit N]
  python expnote.py lessons [--tag TAG] [--limit N]
  python expnote.py stats
  python expnote.py distill --tag TAG --lesson "..."
"""

import json, sys, time, argparse, os, hashlib
from pathlib import Path
from datetime import datetime, timezone, timedelta

# Fix Windows console encoding for Unicode output
if sys.platform == "win32":
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
    sys.stderr.reconfigure(encoding="utf-8", errors="replace")


def get_data_dir():
    """Get experiment notes data directory."""
    d = Path.home() / ".openclaw" / "memory" / "experiments"
    d.mkdir(parents=True, exist_ok=True)
    return d


def get_experiments_file():
    return get_data_dir() / "experiments.jsonl"


def get_lessons_file():
    return get_data_dir() / "lessons.jsonl"


def gen_id():
    """Generate a short unique ID."""
    ts = str(time.time()).encode()
    return f"exp_{hashlib.sha256(ts).hexdigest()[:12]}"


def load_experiments():
    """Load all experiments from JSONL file."""
    f = get_experiments_file()
    if not f.exists():
        return []
    experiments = []
    for line in f.read_text(encoding="utf-8").strip().split("\n"):
        if line.strip():
            try:
                experiments.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return experiments


def save_experiment(exp):
    """Append an experiment to the JSONL file."""
    f = get_experiments_file()
    with open(f, "a", encoding="utf-8") as fh:
        fh.write(json.dumps(exp, ensure_ascii=False) + "\n")


def load_lessons():
    """Load distilled lessons."""
    f = get_lessons_file()
    if not f.exists():
        return []
    lessons = []
    for line in f.read_text(encoding="utf-8").strip().split("\n"):
        if line.strip():
            try:
                lessons.append(json.loads(line))
            except json.JSONDecodeError:
                continue
    return lessons


def save_lesson(lesson):
    """Append a lesson to the JSONL file."""
    f = get_lessons_file()
    with open(f, "a", encoding="utf-8") as fh:
        fh.write(json.dumps(lesson, ensure_ascii=False) + "\n")


# --- Commands ---

def cmd_log(args):
    """Log a new experiment."""
    now = datetime.now(timezone(timedelta(hours=8)))
    exp = {
        "id": gen_id(),
        "timestamp": now.isoformat(),
        "task": args.task,
        "outcome": args.outcome,
        "attempt": {
            "command": args.cmd or "",
            "description": args.description or args.task,
        },
        "result": {
            "error": args.error or "",
            "fix": args.fix or "",
        },
        "tags": [t.strip() for t in args.tags.split(",")] if args.tags else [],
        "lesson": args.lesson or "",
    }
    save_experiment(exp)
    print(f"✅ Logged: [{exp['id']}] {args.task} → {args.outcome}")
    if exp["lesson"]:
        print(f"   💡 Lesson: {exp['lesson']}")


def cmd_search(args):
    """Full-text search experiments."""
    experiments = load_experiments()
    query = args.query.lower()
    results = []
    for exp in experiments:
        searchable = " ".join([
            exp.get("task", ""),
            exp.get("attempt", {}).get("description", ""),
            exp.get("attempt", {}).get("command", ""),
            exp.get("result", {}).get("error", ""),
            exp.get("result", {}).get("fix", ""),
            exp.get("lesson", ""),
            " ".join(exp.get("tags", [])),
        ]).lower()
        if query in searchable:
            results.append(exp)
    
    results = results[-args.limit:]  # most recent first
    results.reverse()
    
    if not results:
        print(f"🔍 No results for: {args.query}")
        return
    
    print(f"🔍 Found {len(results)} experiment(s):\n")
    for exp in results:
        icon = {"success": "✅", "failure": "❌", "partial": "⚠️"}.get(exp["outcome"], "❓")
        print(f"{icon} [{exp['id']}] {exp['task']}")
        print(f"   Time: {exp['timestamp'][:16]}")
        if exp["result"].get("error"):
            print(f"   Error: {exp['result']['error'][:100]}")
        if exp["result"].get("fix"):
            print(f"   Fix: {exp['result']['fix'][:100]}")
        if exp.get("lesson"):
            print(f"   💡 {exp['lesson'][:100]}")
        print()


def cmd_similar(args):
    """Find experiments with similar tasks."""
    experiments = load_experiments()
    task_words = set(args.task.lower().split())
    
    scored = []
    for exp in experiments:
        exp_words = set(exp.get("task", "").lower().split())
        tag_words = set(" ".join(exp.get("tags", [])).lower().split())
        overlap = len(task_words & (exp_words | tag_words))
        if overlap > 0:
            scored.append((overlap, exp))
    
    scored.sort(key=lambda x: x[0], reverse=True)
    results = [s[1] for s in scored[:args.limit]]
    
    if not results:
        print(f"🔍 No similar experiments for: {args.task}")
        return
    
    print(f"🔍 Found {len(results)} similar experiment(s):\n")
    for exp in results:
        icon = {"success": "✅", "failure": "❌", "partial": "⚠️"}.get(exp["outcome"], "❓")
        print(f"{icon} [{exp['id']}] {exp['task']}")
        if exp["result"].get("error"):
            print(f"   ⚠️ Error: {exp['result']['error'][:100]}")
        if exp["result"].get("fix"):
            print(f"   🔧 Fix: {exp['result']['fix'][:100]}")
        if exp.get("lesson"):
            print(f"   💡 {exp['lesson'][:100]}")
        print()


def cmd_lessons(args):
    """Show distilled lessons, optionally filtered by tag."""
    experiments = load_experiments()
    tag_filter = args.tag.lower() if args.tag else None
    
    # Collect experiments with lessons
    learned = []
    for exp in experiments:
        if not exp.get("lesson"):
            continue
        if tag_filter and tag_filter not in [t.lower() for t in exp.get("tags", [])]:
            continue
        learned.append(exp)
    
    # Also load distilled lessons
    distilled = load_lessons()
    if tag_filter:
        distilled = [l for l in distilled if tag_filter in [t.lower() for t in l.get("tags", [])]]
    
    all_lessons = learned[-args.limit:] + distilled[-args.limit:]
    
    if not all_lessons:
        print("📚 No lessons found.")
        return
    
    print(f"📚 {len(all_lessons)} lesson(s):\n")
    for item in all_lessons:
        tags_str = ", ".join(item.get("tags", []))
        print(f"💡 [{tags_str}] {item['lesson']}")
        if item.get("task"):
            print(f"   From: {item['task']}")
        print()


def cmd_stats(args):
    """Show experiment statistics."""
    experiments = load_experiments()
    if not experiments:
        print("📊 No experiments recorded yet.")
        return
    
    total = len(experiments)
    success = sum(1 for e in experiments if e["outcome"] == "success")
    failure = sum(1 for e in experiments if e["outcome"] == "failure")
    partial = sum(1 for e in experiments if e["outcome"] == "partial")
    
    # Tag frequency
    tag_counts = {}
    for exp in experiments:
        for tag in exp.get("tags", []):
            tag_counts[tag] = tag_counts.get(tag, 0) + 1
    
    top_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)[:10]
    
    # Recent activity
    recent = experiments[-5:]
    
    print(f"📊 Experiment Statistics\n")
    print(f"Total:    {total}")
    print(f"✅ Success:  {success} ({success/total*100:.0f}%)")
    print(f"❌ Failure:  {failure} ({failure/total*100:.0f}%)")
    print(f"⚠️ Partial:  {partial} ({partial/total*100:.0f}%)")
    
    if top_tags:
        print(f"\nTop Tags:")
        for tag, count in top_tags:
            print(f"  #{tag}: {count}")
    
    lessons_count = sum(1 for e in experiments if e.get("lesson"))
    print(f"\nLessons captured: {lessons_count}")
    
    print(f"\nRecent:")
    for exp in recent:
        icon = {"success": "✅", "failure": "❌", "partial": "⚠️"}.get(exp["outcome"], "❓")
        print(f"  {icon} {exp['timestamp'][:10]} {exp['task'][:50]}")


def cmd_distill(args):
    """Save a distilled lesson from multiple experiments."""
    now = datetime.now(timezone(timedelta(hours=8)))
    lesson = {
        "id": gen_id(),
        "timestamp": now.isoformat(),
        "lesson": args.lesson,
        "tags": [t.strip() for t in args.tags.split(",")] if args.tags else [],
        "source": "distilled",
    }
    save_lesson(lesson)
    print(f"📚 Lesson saved: {args.lesson}")


def main():
    parser = argparse.ArgumentParser(
        prog="expnote",
        description="Experiment Notes System for AI Agents",
    )
    sub = parser.add_subparsers(dest="command")
    
    # log
    p_log = sub.add_parser("log", help="Log an experiment")
    p_log.add_argument("--task", required=True, help="What were you trying to do?")
    p_log.add_argument("--outcome", required=True, choices=["success", "failure", "partial"])
    p_log.add_argument("--tags", default="", help="Comma-separated tags")
    p_log.add_argument("--cmd", default="", help="Command or action attempted")
    p_log.add_argument("--description", default="", help="Detailed description")
    p_log.add_argument("--error", default="", help="Error encountered (if any)")
    p_log.add_argument("--fix", default="", help="How it was fixed")
    p_log.add_argument("--lesson", default="", help="Key takeaway")
    
    # search
    p_search = sub.add_parser("search", help="Search experiments")
    p_search.add_argument("query", help="Search query")
    p_search.add_argument("--limit", type=int, default=10)
    
    # similar
    p_similar = sub.add_parser("similar", help="Find similar experiments")
    p_similar.add_argument("task", help="Task description to match")
    p_similar.add_argument("--limit", type=int, default=5)
    
    # lessons
    p_lessons = sub.add_parser("lessons", help="Show lessons learned")
    p_lessons.add_argument("--tag", default=None)
    p_lessons.add_argument("--limit", type=int, default=20)
    
    # stats
    sub.add_parser("stats", help="Show statistics")
    
    # distill
    p_distill = sub.add_parser("distill", help="Save a distilled lesson")
    p_distill.add_argument("--tags", required=True, help="Comma-separated tags")
    p_distill.add_argument("--lesson", required=True, help="The lesson")
    
    args = parser.parse_args()
    
    if not args.command:
        parser.print_help()
        return
    
    cmds = {
        "log": cmd_log,
        "search": cmd_search,
        "similar": cmd_similar,
        "lessons": cmd_lessons,
        "stats": cmd_stats,
        "distill": cmd_distill,
    }
    cmds[args.command](args)


if __name__ == "__main__":
    main()


path: templates/agents-snippet.md
content: # 实验笔记集成片段
# 将以下内容添加到你的 AGENTS.md 中

## Experiment Notes / 实验笔记

使用 experiment-notes skill 记录和查询实验经验。

EXPNOTE = python <skill_dir>/scripts/expnote.py

### 何时记录

**必须记录：**
- 遇到错误并成功修复 → `outcome: partial` 或 `success`
- 尝试了多种方案才搞定 → 每种方案分别记录
- 踩了一个坑（API 限制、环境问题等）→ `outcome: failure` + `lesson`

**可选记录：**
- 顺利完成的标准操作 → `outcome: success`（低优先级，可不记）

### 记录模板

```bash
$EXPNOTE log \
  --task "简短描述你要做什么" \
  --outcome success|failure|partial \
  --tags tag1,tag2,tag3 \
  --cmd "实际执行的命令" \
  --error "报错信息（如有）" \
  --fix "怎么解决的（如有）" \
  --lesson "一句话总结关键教训"
```

### 查询模板

```bash
# 开始任务前，先查历史
$EXPNOTE similar "任务描述"

# 找不到？试全文搜索
$EXPNOTE search "关键词"

# 查看某个领域的所有教训
$EXPNOTE lessons --tag docker
```

### 定期维护

每周或每 20 条记录后：
1. 运行 `$EXPNOTE stats` 看看模式
2. 把重复出现的教训用 `$EXPNOTE distill` 提炼
3. 更新 AGENTS.md 中的最佳实践


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)