Skill: omaression/auto-improving-agent (version 1.0.0-alpha)

--- name: self-improving-agent description: Automatically capture corrections, failures, and reusable discoveries into `.learnings/` files using signal-based filtering. Triggers when the user corrects the agent, a tool/command fails in a reusable way, or a better approach is discovered. Also handles periodic retention scoring and promotion of proven patterns into SOUL.md, AGENTS.md, or TOOLS.md. --- # Self-Improving Agent Capture what matters. Ignore noise. Promote proven patterns. Automate all of it. ## Source of truth - `.learnings/LEARNINGS.md` — corrections, env configs, reusable fixes, architecture decisions - `.learnings/ERRORS.md` — tool/command failures with fixes - `.learnings/FEATURE_REQUESTS.md` — missing capabilities worth tracking - `.learnings/ARCHIVE.md` — entries scored out during retention sweeps (never injected into context, but searchable) ## Write gate Before logging anything, the candidate must pass at least ONE filter: | Filter | Weight | Description | |---|---|---| | **Correction** | ALWAYS | Omar explicitly corrected the agent | | **Recurrence** | HIGH | Same issue hit 2+ times (check existing entries) | | **Cost-to-rediscover** | HIGH | Would take >2 tool calls to figure out again | | **Blast radius** | MEDIUM | Affects multiple skills, projects, or workflows | | **Decay risk** | MEDIUM | Non-obvious env/config detail that changes rarely | If NONE match → do not log. This replaces any arbitrary line-count threshold. Never log: - routine successes - facts obvious from docs or code - one-off tasks with no recurrence potential - anything already in MEMORY.md, SOUL.md, USER.md, or AGENTS.md ## Entry format LEARNINGS.md: ```markdown - [YYYY-MM-DD] [Category]: [Actionable takeaway] ``` Categories: `Correction`, `Env`, `Workflow`, `Testing`, `Skills`, `Git`, `Architecture` ERRORS.md: ```markdown - [YYYY-MM-DD] [Tool]: [What failed] → [Fix] ``` Mark fixed items with `[fixed]`. Delete stale entries during retention sweeps. ## Retention gate Instead of a hard line cap, score each entry periodically: | Signal | Score | |---|---| | Referenced or applied in last 30 days | +3 | | Matches active project context | +2 | | Direct correction from Omar | +2 | | Has prevented a repeat error | +3 | | Env/config still valid | +1 | | Superseded by newer entry | −5 | | >90 days old, never referenced | −3 | Action: - score ≥ 2 → keep - 0 ≤ score < 2 → archive to `.learnings/ARCHIVE.md` - score < 0 → delete Run this sweep during heartbeat maintenance (every ~3 days) or when LEARNINGS.md feels noisy. ## Automated triggers These fire without user prompting: 1. **Post-task scan**: After multi-step tasks, check for retried commands, error→workaround sequences, or avoidable file reads. If found, evaluate against write gate and log if it passes. 2. **Session-start sweep**: On `.learnings/LEARNINGS.md` read, flag entries >90 days old for retention scoring. 3. **Promotion detector**: After logging, scan for entries with the same `[Category]` tag appearing 3+ times. If found, auto-suggest a one-liner promotion to: - behavior/style → `SOUL.md` - workflow/process → `AGENTS.md` - tool/env gotcha → `TOOLS.md` 4. **Cross-session pattern detection**: When `memory_search` returns a daily note describing a workaround, check if `.learnings/` already has it. If not and it passes the write gate, log it. ## Dedup Before logging, scan existing entries for near-duplicates. If the lesson already exists, only update it if the new version is sharper or more general. ## Quality bar Every entry must help a future session avoid wasted work in under one glance.

path: hooks/openclaw/HOOK.md
content: ---
name: self-improvement
description: "Injects self-improvement reminder during agent bootstrap"
metadata: {"openclaw":{"emoji":"🧠","events":["agent:bootstrap"]}}
---

# Self-Improvement Hook

Injects a reminder to evaluate learnings during agent bootstrap.

## What It Does

- Fires on `agent:bootstrap` (before workspace files are injected)
- Adds a reminder block to check `.learnings/` for relevant entries
- Prompts the agent to log corrections, errors, and discoveries

## Configuration

No configuration needed. Enable with:

```bash
openclaw hooks enable self-improvement
```


path: hooks/openclaw/handler.js
content: /**
 * Self-Improvement Hook for OpenClaw
 * 
 * Injects a reminder to evaluate learnings during agent bootstrap.
 * Fires on agent:bootstrap event before workspace files are injected.
 */

const REMINDER_CONTENT = `
## Self-Improvement Reminder

After completing tasks, evaluate if any learnings should be captured:

**Log when:**
- User corrects you → \`.learnings/LEARNINGS.md\`
- Command/operation fails → \`.learnings/ERRORS.md\`
- User wants missing capability → \`.learnings/FEATURE_REQUESTS.md\`
- You discover your knowledge was wrong → \`.learnings/LEARNINGS.md\`
- You find a better approach → \`.learnings/LEARNINGS.md\`

**Promote when pattern is proven:**
- Behavioral patterns → \`SOUL.md\`
- Workflow improvements → \`AGENTS.md\`
- Tool gotchas → \`TOOLS.md\`

Keep entries simple: date, title, what happened, what to do differently.
`.trim();

const handler = async (event) => {
  // Safety checks for event structure
  if (!event || typeof event !== 'object') {
    return;
  }

  // Only handle agent:bootstrap events
  if (event.type !== 'agent' || event.action !== 'bootstrap') {
    return;
  }

  // Safety check for context
  if (!event.context || typeof event.context !== 'object') {
    return;
  }

  // Inject the reminder as a virtual bootstrap file
  // Check that bootstrapFiles is an array before pushing
  if (Array.isArray(event.context.bootstrapFiles)) {
    event.context.bootstrapFiles.push({
      path: 'SELF_IMPROVEMENT_REMINDER.md',
      content: REMINDER_CONTENT,
      virtual: true,
    });
  }
};

module.exports = handler;
module.exports.default = handler;


path: hooks/openclaw/handler.ts
content: /**
 * Self-Improvement Hook for OpenClaw
 * 
 * Injects a reminder to evaluate learnings during agent bootstrap.
 * Fires on agent:bootstrap event before workspace files are injected.
 */

import type { HookHandler } from 'openclaw/hooks';

const REMINDER_CONTENT = `## Self-Improvement Reminder

After completing tasks, evaluate if any learnings should be captured:

**Log when:**
- User corrects you → \`.learnings/LEARNINGS.md\`
- Command/operation fails → \`.learnings/ERRORS.md\`
- User wants missing capability → \`.learnings/FEATURE_REQUESTS.md\`
- You discover your knowledge was wrong → \`.learnings/LEARNINGS.md\`
- You find a better approach → \`.learnings/LEARNINGS.md\`

**Promote when pattern is proven:**
- Behavioral patterns → \`SOUL.md\`
- Workflow improvements → \`AGENTS.md\`
- Tool gotchas → \`TOOLS.md\`

Keep entries simple: date, title, what happened, what to do differently.`;

const handler: HookHandler = async (event) => {
  // Safety checks for event structure
  if (!event || typeof event !== 'object') {
    return;
  }

  // Only handle agent:bootstrap events
  if (event.type !== 'agent' || event.action !== 'bootstrap') {
    return;
  }

  // Safety check for context
  if (!event.context || typeof event.context !== 'object') {
    return;
  }

  // Skip sub-agent sessions to avoid bootstrap issues
  // Sub-agents have sessionKey patterns like "agent:main:subagent:..."
  const sessionKey = event.sessionKey || '';
  if (sessionKey.includes(':subagent:')) {
    return;
  }

  // Inject the reminder as a virtual bootstrap file
  // Check that bootstrapFiles is an array before pushing
  if (Array.isArray(event.context.bootstrapFiles)) {
    event.context.bootstrapFiles.push({
      path: 'SELF_IMPROVEMENT_REMINDER.md',
      content: REMINDER_CONTENT,
      virtual: true,
    });
  }
};

export default handler;


path: scripts/retention_scorer.py
content: #!/usr/bin/env python3
"""Score .learnings/LEARNINGS.md entries for retention, archival, or deletion.

Usage:
  python3 scripts/retention_scorer.py [--learnings PATH] [--dry-run]

Reads LEARNINGS.md, scores each entry, and outputs keep/archive/delete decisions.
With --dry-run, prints decisions without modifying files.
"""

from __future__ import annotations

import argparse
import re
from dataclasses import dataclass
from datetime import date, timedelta
from pathlib import Path


ENTRY_RE = re.compile(r"^- \[(\d{4}-\d{2}-\d{2})\] \[([^\]]+)\]: (.+)$")
DEFAULT_LEARNINGS = Path(".learnings/LEARNINGS.md")
DEFAULT_ARCHIVE = Path(".learnings/ARCHIVE.md")


@dataclass
class ScoredEntry:
    line: str
    entry_date: date
    category: str
    takeaway: str
    score: int
    action: str  # "keep", "archive", "delete"


def parse_date(s: str) -> date:
    y, m, d = s.split("-")
    return date(int(y), int(m), int(d))


def score_entry(
    entry_date: date,
    category: str,
    takeaway: str,
    today: date | None = None,
    active_categories: set[str] | None = None,
    category_counts: dict[str, int] | None = None,
) -> int:
    today = today or date.today()
    active_categories = active_categories or set()
    category_counts = category_counts or {}
    score = 0

    age_days = (today - entry_date).days

    # Referenced or applied in last 30 days (heuristic: recent entries)
    if age_days <= 30:
        score += 3

    # Matches active project context
    if category in active_categories:
        score += 2

    # Direct correction from Omar
    if category.lower() == "correction":
        score += 2

    # Has prevented a repeat error (heuristic: category appears 2+ times)
    if category_counts.get(category, 0) >= 2:
        score += 3

    # Env/config still valid (heuristic: env entries get +1)
    if category.lower() == "env":
        score += 1

    # Superseded check would need content comparison — skip for now

    # Old and likely stale
    if age_days > 90:
        score -= 3

    return score


def classify(score: int) -> str:
    if score >= 2:
        return "keep"
    if score >= 0:
        return "archive"
    return "delete"


def score_file(
    learnings_path: Path,
    today: date | None = None,
    active_categories: set[str] | None = None,
) -> list[ScoredEntry]:
    today = today or date.today()
    lines = learnings_path.read_text().splitlines()

    # First pass: count categories
    category_counts: dict[str, int] = {}
    entries_raw: list[tuple[str, date, str, str]] = []
    for line in lines:
        m = ENTRY_RE.match(line.strip())
        if m:
            d = parse_date(m.group(1))
            cat = m.group(2)
            take = m.group(3)
            category_counts[cat] = category_counts.get(cat, 0) + 1
            entries_raw.append((line, d, cat, take))

    # Second pass: score
    results: list[ScoredEntry] = []
    for raw_line, entry_date, category, takeaway in entries_raw:
        s = score_entry(
            entry_date,
            category,
            takeaway,
            today=today,
            active_categories=active_categories,
            category_counts=category_counts,
        )
        results.append(ScoredEntry(
            line=raw_line,
            entry_date=entry_date,
            category=category,
            takeaway=takeaway,
            score=s,
            action=classify(s),
        ))

    return results


def apply_decisions(
    learnings_path: Path,
    archive_path: Path,
    scored: list[ScoredEntry],
    dry_run: bool = False,
) -> dict[str, int]:
    keep_lines: list[str] = []
    archive_lines: list[str] = []
    counts = {"keep": 0, "archive": 0, "delete": 0}

    # Preserve header lines from learnings file
    original = learnings_path.read_text().splitlines()
    header_lines = [line for line in original if not ENTRY_RE.match(line.strip())]

    for entry in scored:
        counts[entry.action] += 1
        if entry.action == "keep":
            keep_lines.append(entry.line)
        elif entry.action == "archive":
            archive_lines.append(entry.line)
        # "delete" entries are simply dropped

    if dry_run:
        return counts

    # Write back learnings
    learnings_path.write_text("\n".join(header_lines + keep_lines) + "\n")

    # Append to archive
    if archive_lines:
        existing_archive = archive_path.read_text() if archive_path.exists() else "# ARCHIVE.md — Scored-out learnings\n"
        archive_path.write_text(existing_archive.rstrip() + "\n" + "\n".join(archive_lines) + "\n")

    return counts


def main() -> None:
    parser = argparse.ArgumentParser(description="Score learnings for retention")
    parser.add_argument("--learnings", type=Path, default=DEFAULT_LEARNINGS)
    parser.add_argument("--archive", type=Path, default=DEFAULT_ARCHIVE)
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args()

    if not args.learnings.exists():
        print(f"No file at {args.learnings}")
        return

    scored = score_file(args.learnings)
    for entry in scored:
        print(f"[{entry.action:>7}] (score={entry.score:>3}) {entry.line.strip()[:80]}")

    counts = apply_decisions(args.learnings, args.archive, scored, dry_run=args.dry_run)
    print(f"\nResult: keep={counts['keep']} archive={counts['archive']} delete={counts['delete']}")
    if args.dry_run:
        print("(dry run — no files changed)")


if __name__ == "__main__":
    main()


path: tests/test_retention_scorer.py
content: import sys
from datetime import date
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))

from retention_scorer import ScoredEntry, classify, score_entry, score_file


def test_recent_correction_scores_high() -> None:
    score = score_entry(
        entry_date=date(2026, 3, 10),
        category="Correction",
        takeaway="Omar said X",
        today=date(2026, 3, 15),
        category_counts={"Correction": 1},
    )
    assert score >= 5  # recent (+3) + correction (+2)
    assert classify(score) == "keep"


def test_old_unreferenced_entry_gets_archived_or_deleted() -> None:
    score = score_entry(
        entry_date=date(2025, 11, 1),
        category="Testing",
        takeaway="some old test tip",
        today=date(2026, 3, 15),
        category_counts={"Testing": 1},
    )
    assert score < 2
    action = classify(score)
    assert action in ("archive", "delete")


def test_env_entry_with_recurrence_keeps() -> None:
    score = score_entry(
        entry_date=date(2026, 2, 20),
        category="Env",
        takeaway="use python3 not python",
        today=date(2026, 3, 15),
        category_counts={"Env": 3},
        active_categories={"Env"},
    )
    assert score >= 2
    assert classify(score) == "keep"


def test_score_file_with_sample(tmp_path: Path) -> None:
    learnings = tmp_path / "LEARNINGS.md"
    learnings.write_text(
        "# LEARNINGS\n"
        "- [2026-03-10] [Correction]: Omar said use python3\n"
        "- [2025-06-01] [Testing]: old stale entry\n"
        "- [2026-03-14] [Git]: use short-lived branches\n"
    )
    scored = score_file(learnings, today=date(2026, 3, 15))
    assert len(scored) == 3
    actions = {e.category: e.action for e in scored}
    assert actions["Correction"] == "keep"
    assert actions["Testing"] in ("archive", "delete")


def test_classify_boundaries() -> None:
    assert classify(10) == "keep"
    assert classify(2) == "keep"
    assert classify(1) == "archive"
    assert classify(0) == "archive"
    assert classify(-1) == "delete"
    assert classify(-5) == "delete"


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