Skill: tristanmanchester/actual-self-improvement (version 1.0.0)

--- name: self-improvement description: Capture durable lessons from debugging, user corrections, missing capabilities, and repeated workflow friction so future sessions avoid the same mistakes. Use this skill when a non-obvious failure is diagnosed, the user corrects or updates the agent, a workaround or project convention is discovered, a capability is missing, a solved issue should be promoted into shared memory, or you should review prior learnings before changing a known-problem area. Do not use for trivial typos, expected failures, straightforward retries, or one-off noise with no reusable lesson. compatibility: Portable Agent Skills format. Core workflow is agent-agnostic. Bundled helpers require Python 3.11+; hook helpers require bash. No network access is required. Hook snippets are examples for Claude Code-style configs and OpenClaw; manual use works everywhere. metadata: version: "4.0.0" original_slug: "self-improving-agent" category: "workflow" author: "OpenAI adaptation from user-provided skill" --- # Self-Improvement Capture, review, promote, and extract durable lessons so future sessions avoid repeating the same mistakes. ## Core idea Use this skill for **reusable learning**, not for every bump in the road. A good entry usually has at least one of these properties: - It corrected a wrong assumption. - It revealed a project-specific convention. - It required real debugging or investigation. - It is likely to recur. - It should change future workflow, memory, or tooling. Do **not** log routine noise such as obvious typos, expected validation failures, or errors that were solved immediately with no transferable lesson. ## Important path model There are **two different roots** in this skill: 1. **Skill root** — where bundled resources live: - `scripts/...` - `references/...` - `assets/...` 2. **Workspace root** — where the project or active workspace lives: - `.learnings/LEARNINGS.md` - `.learnings/ERRORS.md` - `.learnings/FEATURE_REQUESTS.md` - `CLAUDE.md`, `AGENTS.md`, `.github/copilot-instructions.md`, `SOUL.md`, `TOOLS.md` Never write learnings into the installed skill directory. Always target the **workspace root**. ## Quick decision table | Situation | What to do | |---|---| | User corrects you or updates a fact | Log a **learning** | | Non-obvious command / API / tool failure | Log an **error** | | User asks for a missing capability | Log a **feature request** | | You discover a reusable workaround or convention | Log a **learning** | | A pattern keeps recurring | Search related entries, link with `See Also`, and consider promotion | | A lesson is broadly applicable or repeated | Promote it into project memory | | A resolved, general pattern could help other projects | Extract a new skill | ## Standard workflow ### 1) Find the workspace root first Before reading or writing `.learnings/`, determine `WORKSPACE_ROOT`. Good defaults: - the repository root for the current codebase - the OpenClaw workspace root - the directory containing the files being edited If unsure, prefer the directory containing `.git`, `AGENTS.md`, `CLAUDE.md`, or the user's active project files. ### 2) Initialise `.learnings/` if needed Use the helper instead of creating files manually: ```bash python3 scripts/learnings.py init --root /absolute/path/to/workspace ``` This creates: - `.learnings/LEARNINGS.md` - `.learnings/ERRORS.md` - `.learnings/FEATURE_REQUESTS.md` ### 3) Review existing learnings before risky or familiar work Review first when: - you are returning to an area with prior failures - the task touches infra, CI, deployment, auth, data migration, or generated code - the user explicitly says “remember this”, “we hit this before”, or similar Use the helper: ```bash python3 scripts/learnings.py status --root /absolute/path/to/workspace python3 scripts/learnings.py search --root /absolute/path/to/workspace --query "pnpm" --limit 5 ``` ### 4) Search before logging to avoid duplicates Always search for related entries before creating a new one. ```bash python3 scripts/learnings.py search --root /absolute/path/to/workspace --query "keyword or pattern" --limit 10 ``` If a similar entry already exists: - prefer linking with `See Also` - reuse or add a stable `Pattern-Key` for recurring issues - bump priority only when recurrence justifies it - prefer updating the existing pattern story over spraying near-duplicate entries ### 5) Log the right kind of entry #### Learning Use for corrections, knowledge gaps, best practices, and durable conventions. ```bash python3 scripts/learnings.py log-learning \ --root /absolute/path/to/workspace \ --category correction \ --priority high \ --area backend \ --summary "Project uses pnpm workspaces, not npm" \ --details "Attempted npm install. Lockfile and workspace config showed pnpm." \ --suggested-action "Check for pnpm-lock.yaml before assuming npm." \ --source error \ --related-files pnpm-lock.yaml pnpm-workspace.yaml \ --tags package-manager,pnpm ``` #### Error Use for non-obvious failures, exceptions, or tool/API issues worth remembering. ```bash python3 scripts/learnings.py log-error \ --root /absolute/path/to/workspace \ --name docker-build \ --priority high \ --area infra \ --summary "Docker build failed on Apple Silicon due to platform mismatch" \ --error-text "error: failed to solve: no match for platform linux/arm64" \ --context "docker build -t myapp . on Apple Silicon" \ --suggested-fix "Retry with --platform linux/amd64 or update base image" \ --reproducible yes \ --related-files Dockerfile ``` #### Feature request Use when the user wants a missing capability or a recurring friction point should become a feature. ```bash python3 scripts/learnings.py log-feature \ --root /absolute/path/to/workspace \ --capability export-to-csv \ --priority medium \ --area backend \ --summary "User needs report export to CSV" \ --user-context "Needed for sharing weekly reports with non-technical stakeholders" \ --complexity-estimate simple \ --suggested-implementation "Add --output csv alongside existing JSON output" \ --frequency recurring \ --related-features analyze-command,json-output ``` ### 6) Promote proven lessons into memory Promote when the learning is broad, repeated, or something any future contributor should know. Common targets: - `CLAUDE.md` — durable project facts and conventions - `AGENTS.md` — workflow rules and automation guidance - `.github/copilot-instructions.md` — shared Copilot context - `SOUL.md` — behavioural principles in OpenClaw workspaces - `TOOLS.md` — tool-specific gotchas in OpenClaw workspaces Write promotions as **short prevention rules**, not long incident write-ups. Example: - Bad promotion: “On 2026-03-12 npm failed because…” - Good promotion: “Use `pnpm install` in this repo; it is a pnpm workspace.” When a learning is promoted, update the original entry’s status to `promoted` or `promoted_to_skill` and record the destination. ### 7) Extract a reusable skill when the pattern is real Extract a new skill when the solution is: - resolved and working - broadly useful beyond one file or repo - non-obvious enough that future agents would benefit - recurring enough to justify its own instructions Use the helper: ```bash python3 scripts/extract_skill.py \ --root /absolute/path/to/workspace \ docker-build-fixes \ --description "Fix recurring Docker build and platform mismatch issues. Use when Docker builds fail due to architecture, base image, or runtime packaging problems." \ --from-learning-id LRN-20260313-001 \ --scaffold-evals ``` Or keep the old entry point if existing automation already calls it: ```bash bash scripts/extract-skill.sh docker-build-fixes --root /absolute/path/to/workspace --dry-run ``` ## Logging rules that matter most 1. **Search first.** Duplicate entries are worse than missing tags. 2. **Prefer durable lessons.** Only log what should change future behaviour. 3. **Be specific.** Name the assumption, failure, or convention clearly. 4. **Include the fix or prevention rule.** An entry without next action is weak. 5. **Use stable pattern keys for recurring problems.** This lets recurrence compound. 6. **Promote aggressively once a rule is proven.** The point is fewer repeat mistakes. 7. **Do not interrupt the user with bookkeeping.** Log silently unless the user asked to see it or you need missing details. ## Recommended references Use these only when needed: - `references/entry-formats.md` — full field schemas and manual templates - `references/examples.md` — concrete examples of good entries and promotions - `references/promotion-and-extraction.md` — promotion rules and skill extraction criteria - `references/platform-setup.md` — Claude Code, Codex, Copilot, and OpenClaw setup notes - `references/evaluation.md` — trigger/output eval plan for this skill - `references/openclaw-integration.md` — deeper OpenClaw workflow guidance ## Hooks Hook helpers are intentionally optional. Available hook scripts: - `scripts/activator.sh` — lightweight reminder at prompt start - `scripts/error-detector.sh` — lightweight error reminder after failed Bash-like commands Hook configuration examples live in `references/platform-setup.md`. ## What “next-level” looks like for this skill A mature use of this skill has a loop: **capture → dedupe → promote → extract → evaluate** That means: - entries are created with deterministic IDs and consistent fields - repeated issues link to each other instead of fragmenting - proven rules move into persistent memory files - broadly useful fixes become standalone skills - the skill itself is tested with trigger and output evals in `evals/`

path: evals/trigger-validation.json
content: query: Actually, our auth middleware reads the correlation ID from x-request-id, not x-correlation-id. Log that correction.
should_trigger: yes

query: This workaround for flaky Docker layers is now proven. Promote it into project memory.
should_trigger: yes

query: I wish you could export these results directly to CSV in future sessions.
should_trigger: yes

query: We hit this Prisma migration issue again. Search related learnings before you try anything new.
should_trigger: yes

query: Please write a Python script that renames these image files.
should_trigger: no

query: What is the capital of Portugal?
should_trigger: no

query: Run the failed test suite again.
should_trigger: no

query: Generate a DOCX report from this markdown.
should_trigger: no

path: evals/output-evals.json
content: {
  "skill_name": "self-improvement",
  "evals": [
    {
      "id": 1,
      "prompt": "You were wrong earlier: the project uses pnpm workspaces, not npm. Capture that learning for future sessions.",
      "expected_output": "A learning entry is created in `.learnings/LEARNINGS.md` with a deterministic ID, summary, details, suggested action, and metadata indicating the correction source.",
      "assertions": [
        "The workflow chooses a learning entry rather than an error or feature request",
        "The created entry includes an LRN-YYYYMMDD-XXX identifier",
        "The entry includes Summary, Details, Suggested Action, and Metadata sections",
        "The suggested action states a prevention rule rather than just restating the issue"
      ]
    },
    {
      "id": 2,
      "prompt": "This Docker build issue required real debugging and will probably recur. Search related learnings first, then log the error if needed.",
      "expected_output": "The agent searches existing learnings before logging and writes a structured error entry only if the issue is new or meaningfully distinct.",
      "assertions": [
        "The workflow searches existing learnings before creating a new entry",
        "If a new entry is created, it is written to `.learnings/ERRORS.md`",
        "The error entry includes Error, Context, Suggested Fix, and Metadata sections",
        "The output distinguishes durable logging from routine noise"
      ]
    },
    {
      "id": 3,
      "prompt": "We keep forgetting to regenerate the API client after OpenAPI changes. Promote that into durable project memory and consider extracting a skill.",
      "expected_output": "The agent proposes a concise rule suitable for a memory file and only recommends skill extraction if the pattern is broad and proven.",
      "assertions": [
        "The promotion is expressed as a short prevention rule",
        "The output names an appropriate memory target such as CLAUDE.md or AGENTS.md",
        "Skill extraction is justified with recurrence or broad applicability, not assumed automatically"
      ]
    }
  ]
}


path: evals/trigger-train.json
content: query: You were wrong earlier: this repo uses pnpm workspaces, not npm. Please remember that for future tasks.
should_trigger: yes

query: That Docker build failure took real debugging. Capture the lesson so we do not repeat it next week.
should_trigger: yes

query: Before touching CI again, check whether we already logged any learnings about flaky Playwright runs.
should_trigger: yes

query: This capability is missing right now — can you track it as a future feature rather than implementing it immediately?
should_trigger: yes

query: We keep regenerating the API client after spec changes. Turn that into a durable rule and maybe extract a skill.
should_trigger: yes

query: Fix the TypeScript error in src/server.ts.
should_trigger: no

query: Summarise this PDF.
should_trigger: no

query: Make a bar chart from this CSV.
should_trigger: no

query: Why does this bash command say permission denied?
should_trigger: no

query: Create a new skill for Stripe onboarding.
should_trigger: no

path: references/examples.md
content: # Examples

## Example 1 — user correction becomes a learning

```bash
python3 scripts/learnings.py log-learning \
  --root /repo \
  --category correction \
  --priority high \
  --area tests \
  --summary "Database fixtures in this repo are module-scoped, not function-scoped" \
  --details "Initial assumption used function-scoped fixtures. Existing suite uses module scope for DB-heavy tests." \
  --suggested-action "Check local fixture conventions before creating expensive test fixtures." \
  --source user_feedback \
  --related-files tests/conftest.py \
  --tags pytest,fixtures
```

## Example 2 — recurring environment failure becomes an error entry

```bash
python3 scripts/learnings.py log-error \
  --root /repo \
  --name docker-build \
  --priority high \
  --area infra \
  --summary "Docker image build fails on Apple Silicon due to architecture mismatch" \
  --error-text "error: failed to solve: no match for platform linux/arm64" \
  --context "docker build -t myapp . on Apple Silicon" \
  --suggested-fix "Use --platform linux/amd64 or choose a multi-arch base image" \
  --reproducible yes \
  --related-files Dockerfile
```

## Example 3 — project convention promoted into memory

Learning entry summary:
> This repository uses `pnpm` workspaces. `npm install` is the wrong default.

Promotion to `CLAUDE.md`:

```markdown
## Build & dependencies
- Package manager: pnpm. Use `pnpm install` and `pnpm -r build`.
```

## Example 4 — extracted skill candidate

A learning is a good extraction candidate when it is resolved, general, and recurs across tasks.

Good candidate:
- “Regenerate generated API clients after OpenAPI changes”

Poor candidate:
- “A typo in one temporary bash command”


path: references/promotion-and-extraction.md
content: # Promotion and extraction

## Promotion rule of thumb

Promote a learning when it is:
- broadly applicable across multiple files or tasks
- likely to save future contributors time
- a stable convention rather than a one-off event
- better expressed as a short prevention rule than a full incident report

## Promotion targets

| Target | Best for |
|---|---|
| `CLAUDE.md` | Project facts, conventions, durable gotchas |
| `AGENTS.md` | Workflow rules, automation sequences, verification steps |
| `.github/copilot-instructions.md` | Shared Copilot context |
| `SOUL.md` | Behavioural rules in OpenClaw workspaces |
| `TOOLS.md` | Tool-specific gotchas in OpenClaw workspaces |

## Extraction criteria

Extract a reusable skill when most of these are true:
- the solution is resolved and tested
- the pattern is not tied to a single file or repo
- the fix is non-obvious enough to deserve specialised guidance
- the pattern has recurred or is likely to recur
- the resulting skill can stand on its own without original chat context

## Before extracting

Check:
- Does the new skill solve a real category of task?
- Can its description say exactly when it should trigger?
- Can you move detailed docs to `references/`?
- Can repeated logic be bundled into `scripts/`?
- Can you add at least a small eval set?


path: references/openclaw-integration.md
content: # OpenClaw integration

OpenClaw can make this skill more powerful because it already has workspace-level memory files and optional hooks.

## Suggested workspace layout

```text
~/.openclaw/
├── workspace/
│   ├── AGENTS.md
│   ├── SOUL.md
│   ├── TOOLS.md
│   ├── MEMORY.md
│   └── .learnings/
│       ├── LEARNINGS.md
│       ├── ERRORS.md
│       └── FEATURE_REQUESTS.md
├── skills/
│   └── self-improvement/
└── hooks/
    └── self-improvement/
```

## Recommended split

- keep incident details in `.learnings/`
- promote stable workflow rules to `AGENTS.md`
- promote behavioural rules to `SOUL.md`
- promote tool gotchas to `TOOLS.md`

## Why this works well

OpenClaw already distinguishes between transient task work and durable context. This skill adds a disciplined loop for deciding what should graduate from one-off troubleshooting into shared memory.

## Hook bundle

The optional OpenClaw hook lives in `hooks/openclaw/`.

Suggested use:
1. install the skill in your OpenClaw skills directory
2. copy `hooks/openclaw/` into your OpenClaw hooks directory if desired
3. enable the hook in OpenClaw
4. keep `.learnings/` in the workspace root, not the skill directory

## Promotion examples

### To `TOOLS.md`

```markdown
## GitHub CLI
- Check `gh auth status` before assuming push or PR automation will work.
```

### To `AGENTS.md`

```markdown
## After OpenAPI changes
1. Regenerate the API client
2. Run type-checking
3. Re-run contract-sensitive tests
```

### To `SOUL.md`

```markdown
## Correction behaviour
- Admit wrong assumptions quickly and replace them with the corrected fact.
```


path: references/entry-formats.md
content: # Entry formats

Use the bundled scripts when possible. These templates are the manual fallback.

## Learning entry

```markdown
## [LRN-YYYYMMDD-XXX] category

**Logged**: 2026-03-13T12:00:00Z
**Priority**: medium
**Status**: pending
**Area**: backend

### Summary
One-line summary of the lesson

### Details
Explain the wrong assumption, the evidence, and the corrected understanding.

### Suggested Action
Turn the lesson into a prevention rule.

### Metadata
- Source: conversation | error | user_feedback | docs | simplify-and-harden
- Related Files: src/example.ts
- Tags: api, auth
- See Also: LRN-20260312-001
- Pattern-Key: api.auth.header
- Recurrence-Count: 1
- First-Seen: 2026-03-13
- Last-Seen: 2026-03-13

---
```

## Error entry

```markdown
## [ERR-YYYYMMDD-XXX] error-name

**Logged**: 2026-03-13T12:00:00Z
**Priority**: high
**Status**: pending
**Area**: infra

### Summary
One-line description of the failure

### Error
```
Representative error text
```

### Context
Command, tool, API, or environment details

### Suggested Fix
Most likely remediation or prevention rule

### Metadata
- Reproducible: yes | no | unknown
- Related Files: Dockerfile
- See Also: ERR-20260310-001

---
```

## Feature request entry

```markdown
## [FEAT-YYYYMMDD-XXX] capability-name

**Logged**: 2026-03-13T12:00:00Z
**Priority**: medium
**Status**: pending
**Area**: backend

### Requested Capability
What is missing

### Summary
One-line summary

### User Context
Why it matters

### Complexity Estimate
simple | medium | complex

### Suggested Implementation
Concrete first implementation idea

### Metadata
- Frequency: first_time | recurring
- Related Features: existing-feature-name

---
```

## Status guidance

- `pending` — captured, not yet addressed
- `in_progress` — being worked on now
- `resolved` — issue fixed or lesson integrated
- `wont_fix` — intentionally not addressing it
- `promoted` — distilled into project memory
- `promoted_to_skill` — extracted into a reusable skill


path: references/platform-setup.md
content: # Platform setup

This skill is portable, but automation differs by environment.

## Manual use (works everywhere)

The safest baseline is manual activation:
1. determine the workspace root
2. run `python3 scripts/learnings.py ... --root /path/to/workspace`
3. promote or extract only after the pattern is proven

## Claude Code / Claude-style hook configs

Example prompt-start reminder:

```json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/self-improvement/scripts/activator.sh"
          }
        ]
      }
    ]
  }
}
```

Example error reminder:

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "/absolute/path/to/self-improvement/scripts/error-detector.sh"
          }
        ]
      }
    ]
  }
}
```

## Codex / other skills-aware CLIs

Use the same manual workflow unless your client supports equivalent command hooks.

## GitHub Copilot

When hooks are unavailable, add a compact reminder to `.github/copilot-instructions.md`:

```markdown
## Self-improvement
After solving non-obvious issues or learning project-specific conventions, consider logging the durable lesson to `.learnings/` and promoting proven rules into shared memory.
```

## OpenClaw

OpenClaw-specific workspace and hook notes are in `references/openclaw-integration.md`.


path: references/evaluation.md
content: # Evaluation plan

This skill should be evaluated on two axes:

1. **Triggering** — does it activate when durable learning is relevant?
2. **Output quality** — when it activates, does it create good entries and promotions?

## Included eval assets

- `evals/trigger-train.json`
- `evals/trigger-validation.json`
- `evals/output-evals.json`

## What to look for

### Trigger quality

The skill should trigger for:
- user corrections with durable implications
- non-obvious failures worth remembering
- requests to capture, remember, promote, or extract a solution
- work in areas with known historical learnings

It should usually not trigger for:
- routine debugging with no reusable lesson yet
- trivial typos or expected validation errors
- unrelated document or coding tasks

### Output quality

A good run should:
- pick the right entry type
- search before logging
- create a deterministic ID
- fill the required fields
- produce a concrete prevention rule or suggested action
- promote only when a rule is truly durable

## Suggested loop

1. Run trigger evals on train and validation sets.
2. Run output evals with and without the skill.
3. Grade assertions with concrete evidence.
4. Tighten description or instructions based on failures.
5. Repeat until results are stable.


path: references/hooks-setup.md
content: # Hook setup

Hook examples were consolidated into `references/platform-setup.md`.

See that file for:
- prompt-start reminder hooks
- post-tool error reminder hooks
- Copilot fallback guidance
- notes on absolute vs relative script paths


path: hooks/openclaw/handler.js
content: /**
 * Self-Improvement Hook for OpenClaw
 *
 * Injects a lightweight reminder at bootstrap so the agent remembers to
 * capture durable lessons, promote proven rules, and avoid logging noise.
 */

const REMINDER_CONTENT = `## Self-Improvement Reminder

Capture only durable learning:
- user correction or updated fact
- non-obvious failure worth remembering
- project convention or workaround discovered
- missing capability worth tracking

Promote proven rules into shared memory.
Extract broad, resolved patterns into standalone skills.
`.trim();

const handler = async (event) => {
  if (!event || typeof event !== 'object') return;
  if (event.type !== 'agent' || event.action !== 'bootstrap') return;
  if (!event.context || typeof event.context !== 'object') return;

  const sessionKey = event.sessionKey || '';
  if (sessionKey.includes(':subagent:')) return;

  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/HOOK.md
content: ---
name: self-improvement
description: Injects a lightweight self-improvement reminder during OpenClaw bootstrap.
metadata: {"openclaw":{"emoji":"🧠","events":["agent:bootstrap"]}}
---

# Self-Improvement Hook

Adds a compact reminder to capture durable lessons, not routine noise.

Use this hook when you want OpenClaw sessions to remember the capture → promote → extract loop automatically.


path: hooks/openclaw/handler.ts
content: /**
 * Self-Improvement Hook for OpenClaw
 *
 * Injects a lightweight reminder at bootstrap so the agent remembers to
 * capture durable lessons, promote proven rules, and avoid logging noise.
 */

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

const REMINDER_CONTENT = `## Self-Improvement Reminder

Capture only durable learning:
- user correction or updated fact
- non-obvious failure worth remembering
- project convention or workaround discovered
- missing capability worth tracking

Promote proven rules into shared memory.
Extract broad, resolved patterns into standalone skills.
`;

const handler: HookHandler = async (event) => {
  if (!event || typeof event !== 'object') return;
  if (event.type !== 'agent' || event.action !== 'bootstrap') return;
  if (!event.context || typeof event.context !== 'object') return;

  const sessionKey = event.sessionKey || '';
  if (sessionKey.includes(':subagent:')) return;

  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/learnings.py
content: #!/usr/bin/env python3
"""Manage workspace .learnings files for the self-improvement skill.

This script is designed for agentic use:
- non-interactive only
- structured JSON output by default
- clear errors on stderr
- no external dependencies
"""

from __future__ import annotations

import argparse
import json
import re
import shutil
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Iterable, List, Dict, Any, Optional

AREAS = ["frontend", "backend", "infra", "tests", "docs", "config"]
PRIORITIES = ["low", "medium", "high", "critical"]
STATUSES = ["pending", "in_progress", "resolved", "wont_fix", "promoted", "promoted_to_skill"]
LEARNING_CATEGORIES = ["correction", "knowledge_gap", "best_practice", "insight"]
COMPLEXITY = ["simple", "medium", "complex"]
FREQUENCY = ["first_time", "recurring"]
REPRODUCIBLE = ["yes", "no", "unknown"]
OUTPUT_FORMATS = ["json", "text", "markdown"]

SCRIPT_DIR = Path(__file__).resolve().parent
SKILL_ROOT = SCRIPT_DIR.parent
ASSETS_DIR = SKILL_ROOT / "assets"

TEMPLATE_FILES = {
    "LEARNINGS.md": ASSETS_DIR / "LEARNINGS.md",
    "ERRORS.md": ASSETS_DIR / "ERRORS.md",
    "FEATURE_REQUESTS.md": ASSETS_DIR / "FEATURE_REQUESTS.md",
}

TYPE_CONFIG = {
    "learning": {"prefix": "LRN", "file": "LEARNINGS.md"},
    "error": {"prefix": "ERR", "file": "ERRORS.md"},
    "feature": {"prefix": "FEAT", "file": "FEATURE_REQUESTS.md"},
}

ENTRY_HEADER_RE = re.compile(r"^## \[(?P<id>[A-Z]+-\d{8}-\d{3})\] (?P<title>.+)$", re.M)
FIELD_RE = re.compile(r"^\*\*(?P<key>[^*]+)\*\*: (?P<value>.+)$", re.M)
SUMMARY_RE = re.compile(r"^### Summary\n(?P<summary>.+?)(?:\n\n|\n### |\Z)", re.M | re.S)
PATTERN_KEY_RE = re.compile(r"^- Pattern-Key: (?P<value>.+)$", re.M)
RECURRENCE_RE = re.compile(r"^- Recurrence-Count: (?P<value>\d+)\b", re.M)


class CliError(Exception):
    def __init__(self, message: str, exit_code: int = 1):
        super().__init__(message)
        self.exit_code = exit_code


def eprint(message: str) -> None:
    print(message, file=sys.stderr)


def print_output(payload: Any, fmt: str) -> None:
    if fmt == "json":
        print(json.dumps(payload, indent=2, ensure_ascii=False))
        return
    if isinstance(payload, dict):
        if fmt == "markdown":
            for key, value in payload.items():
                print(f"- **{key}**: {value}")
        else:
            for key, value in payload.items():
                print(f"{key}: {value}")
        return
    if isinstance(payload, list):
        if fmt == "markdown":
            for item in payload:
                print(f"- {item}")
        else:
            for item in payload:
                print(item)
        return
    print(payload)


def resolve_root(path_str: str) -> Path:
    root = Path(path_str).expanduser().resolve()
    if not root.exists():
        raise CliError(f"Workspace root does not exist: {root}", 2)
    if not root.is_dir():
        raise CliError(f"Workspace root is not a directory: {root}", 2)
    return root


def learnings_dir(root: Path) -> Path:
    return root / ".learnings"


def require_learnings_dir(root: Path) -> Path:
    directory = learnings_dir(root)
    if not directory.exists():
        raise CliError(
            f"Missing {directory}. Run 'python3 scripts/learnings.py init --root {root}' first.",
            2,
        )
    return directory


def read_text_or_file(text: Optional[str], file_path: Optional[str], field_name: str) -> str:
    if text and file_path:
        raise CliError(f"Provide either --{field_name} or --{field_name}-file, not both.")
    if file_path:
        path = Path(file_path).expanduser().resolve()
        if not path.exists():
            raise CliError(f"File not found for --{field_name}-file: {path}", 2)
        return path.read_text(encoding="utf-8").rstrip()
    return (text or "").rstrip()


def now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def today_str() -> str:
    return datetime.now(timezone.utc).strftime("%Y%m%d")


def next_id(target_file: Path, prefix: str) -> str:
    date_part = today_str()
    pattern = re.compile(rf"\[{prefix}-{date_part}-(\d{{3}})\]")
    max_seq = 0
    if target_file.exists():
        text = target_file.read_text(encoding="utf-8")
        for match in pattern.finditer(text):
            max_seq = max(max_seq, int(match.group(1)))
    return f"{prefix}-{date_part}-{max_seq + 1:03d}"


def ensure_choice(value: str, allowed: Iterable[str], label: str) -> str:
    if value not in allowed:
        raise CliError(f"Invalid {label}: {value}. Allowed values: {', '.join(allowed)}")
    return value


def split_csvish(values: Optional[List[str]]) -> List[str]:
    if not values:
        return []
    result: List[str] = []
    for value in values:
        for part in value.split(","):
            clean = part.strip()
            if clean:
                result.append(clean)
    return result


def normalise_metadata_lines(lines: List[str]) -> List[str]:
    return [line for line in lines if line and not line.endswith(": ")]


def append_entry(target_file: Path, entry: str) -> None:
    target_file.parent.mkdir(parents=True, exist_ok=True)
    prefix = "\n" if target_file.exists() and target_file.read_text(encoding="utf-8").rstrip() else ""
    with target_file.open("a", encoding="utf-8") as f:
        f.write(prefix)
        f.write(entry.rstrip())
        f.write("\n")


def parse_entries(file_path: Path) -> List[Dict[str, Any]]:
    if not file_path.exists():
        return []
    text = file_path.read_text(encoding="utf-8")
    matches = list(ENTRY_HEADER_RE.finditer(text))
    entries: List[Dict[str, Any]] = []
    for i, match in enumerate(matches):
        start = match.start()
        end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
        block = text[start:end].strip()
        fields = {m.group("key").strip(): m.group("value").strip() for m in FIELD_RE.finditer(block)}
        summary_match = SUMMARY_RE.search(block)
        pattern_key_match = PATTERN_KEY_RE.search(block)
        recurrence_match = RECURRENCE_RE.search(block)
        entries.append(
            {
                "id": match.group("id"),
                "title": match.group("title").strip(),
                "summary": summary_match.group("summary").strip() if summary_match else "",
                "fields": fields,
                "pattern_key": pattern_key_match.group("value").strip() if pattern_key_match else None,
                "recurrence_count": int(recurrence_match.group("value")) if recurrence_match else None,
                "text": block,
                "file": str(file_path),
            }
        )
    return entries


def kind_to_file(root: Path, kind: str) -> Path:
    return require_learnings_dir(root) / TYPE_CONFIG[kind]["file"]


def cmd_init(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = learnings_dir(root)
    directory.mkdir(parents=True, exist_ok=True)
    created: List[str] = []
    skipped: List[str] = []

    for name, template in TEMPLATE_FILES.items():
        target = directory / name
        if target.exists() and not args.force:
            skipped.append(str(target))
            continue
        shutil.copyfile(template, target)
        created.append(str(target))

    return {
        "ok": True,
        "workspace_root": str(root),
        "learnings_dir": str(directory),
        "created": created,
        "skipped": skipped,
    }


def cmd_status(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = require_learnings_dir(root)
    all_entries: List[Dict[str, Any]] = []
    by_file: Dict[str, int] = {}

    for name in TEMPLATE_FILES:
        file_path = directory / name
        entries = parse_entries(file_path)
        by_file[name] = len(entries)
        all_entries.extend(entries)

    status_counts = Counter(entry["fields"].get("Status", "unknown") for entry in all_entries)
    priority_counts = Counter(entry["fields"].get("Priority", "unknown") for entry in all_entries)

    pending_high = [
        {
            "id": entry["id"],
            "title": entry["title"],
            "summary": entry["summary"],
            "file": entry["file"],
        }
        for entry in all_entries
        if entry["fields"].get("Status") == "pending"
        and entry["fields"].get("Priority") in {"high", "critical"}
    ]

    promotion_candidates = [
        {
            "id": entry["id"],
            "title": entry["title"],
            "summary": entry["summary"],
            "recurrence_count": entry["recurrence_count"],
        }
        for entry in all_entries
        if (entry["recurrence_count"] or 0) >= 3
        and entry["fields"].get("Status") in {"pending", "resolved"}
    ]

    return {
        "ok": True,
        "workspace_root": str(root),
        "counts": {
            "total_entries": len(all_entries),
            "by_file": by_file,
            "by_status": dict(status_counts),
            "by_priority": dict(priority_counts),
        },
        "pending_high_priority": pending_high[: args.limit],
        "promotion_candidates": promotion_candidates[: args.limit],
    }


def cmd_search(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = require_learnings_dir(root)
    query = args.query.strip().lower()
    if not query:
        raise CliError("--query must be non-empty")

    files = {
        None: list(TEMPLATE_FILES.keys()),
        "learning": [TYPE_CONFIG["learning"]["file"]],
        "error": [TYPE_CONFIG["error"]["file"]],
        "feature": [TYPE_CONFIG["feature"]["file"]],
    }[args.kind]

    matches: List[Dict[str, Any]] = []
    for file_name in files:
        for entry in parse_entries(directory / file_name):
            haystack = "\n".join(
                [entry["id"], entry["title"], entry["summary"], entry["text"], entry.get("pattern_key") or ""]
            ).lower()
            if query in haystack:
                matches.append(
                    {
                        "id": entry["id"],
                        "title": entry["title"],
                        "summary": entry["summary"],
                        "file": entry["file"],
                        "status": entry["fields"].get("Status"),
                        "priority": entry["fields"].get("Priority"),
                        "pattern_key": entry.get("pattern_key"),
                    }
                )
    return {
        "ok": True,
        "workspace_root": str(root),
        "query": args.query,
        "count": len(matches),
        "matches": matches[: args.limit],
    }


def render_metadata(lines: List[str]) -> str:
    valid = normalise_metadata_lines(lines)
    return "\n".join(valid) if valid else "- Source: unknown"


def cmd_log_learning(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = require_learnings_dir(root)
    ensure_choice(args.category, LEARNING_CATEGORIES, "category")
    ensure_choice(args.priority, PRIORITIES, "priority")
    ensure_choice(args.status, STATUSES, "status")
    ensure_choice(args.area, AREAS, "area")

    details = read_text_or_file(args.details, args.details_file, "details")
    suggested_action = read_text_or_file(args.suggested_action, args.suggested_action_file, "suggested-action")
    tags = ", ".join(split_csvish(args.tags))
    related_files = ", ".join(split_csvish(args.related_files))
    see_also = ", ".join(split_csvish(args.see_also))

    target_file = directory / TYPE_CONFIG["learning"]["file"]
    entry_id = next_id(target_file, TYPE_CONFIG["learning"]["prefix"])
    metadata_lines = [
        f"- Source: {args.source}",
        f"- Related Files: {related_files}" if related_files else "",
        f"- Tags: {tags}" if tags else "",
        f"- See Also: {see_also}" if see_also else "",
        f"- Pattern-Key: {args.pattern_key}" if args.pattern_key else "",
        f"- Recurrence-Count: {args.recurrence_count}" if args.recurrence_count is not None else "",
        f"- First-Seen: {args.first_seen}" if args.first_seen else "",
        f"- Last-Seen: {args.last_seen}" if args.last_seen else "",
    ]

    entry = f"""## [{entry_id}] {args.category}

**Logged**: {now_iso()}
**Priority**: {args.priority}
**Status**: {args.status}
**Area**: {args.area}

### Summary
{args.summary}

### Details
{details or '[TODO: add details]'}

### Suggested Action
{suggested_action or '[TODO: add suggested action]'}

### Metadata
{render_metadata(metadata_lines)}

---
"""

    if not args.dry_run:
        append_entry(target_file, entry)

    return {
        "ok": True,
        "action": "dry_run" if args.dry_run else "logged",
        "kind": "learning",
        "id": entry_id,
        "target_file": str(target_file),
        "summary": args.summary,
        "entry_markdown": entry.strip(),
    }


def cmd_log_error(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = require_learnings_dir(root)
    ensure_choice(args.priority, PRIORITIES, "priority")
    ensure_choice(args.status, STATUSES, "status")
    ensure_choice(args.area, AREAS, "area")
    ensure_choice(args.reproducible, REPRODUCIBLE, "reproducible")

    error_text = read_text_or_file(args.error_text, args.error_text_file, "error-text")
    context = read_text_or_file(args.context, args.context_file, "context")
    suggested_fix = read_text_or_file(args.suggested_fix, args.suggested_fix_file, "suggested-fix")
    related_files = ", ".join(split_csvish(args.related_files))
    see_also = ", ".join(split_csvish(args.see_also))

    target_file = directory / TYPE_CONFIG["error"]["file"]
    entry_id = next_id(target_file, TYPE_CONFIG["error"]["prefix"])
    metadata_lines = [
        f"- Reproducible: {args.reproducible}",
        f"- Related Files: {related_files}" if related_files else "",
        f"- See Also: {see_also}" if see_also else "",
    ]

    entry = f"""## [{entry_id}] {args.name}

**Logged**: {now_iso()}
**Priority**: {args.priority}
**Status**: {args.status}
**Area**: {args.area}

### Summary
{args.summary}

### Error
```
{error_text or '[TODO: add representative error output]'}
```

### Context
{context or '[TODO: add context]'}

### Suggested Fix
{suggested_fix or '[TODO: add suggested fix]'}

### Metadata
{render_metadata(metadata_lines)}

---
"""

    if not args.dry_run:
        append_entry(target_file, entry)

    return {
        "ok": True,
        "action": "dry_run" if args.dry_run else "logged",
        "kind": "error",
        "id": entry_id,
        "target_file": str(target_file),
        "summary": args.summary,
        "entry_markdown": entry.strip(),
    }


def cmd_log_feature(args: argparse.Namespace) -> Dict[str, Any]:
    root = resolve_root(args.root)
    directory = require_learnings_dir(root)
    ensure_choice(args.priority, PRIORITIES, "priority")
    ensure_choice(args.status, STATUSES, "status")
    ensure_choice(args.area, AREAS, "area")
    ensure_choice(args.complexity_estimate, COMPLEXITY, "complexity-estimate")
    ensure_choice(args.frequency, FREQUENCY, "frequency")

    user_context = read_text_or_file(args.user_context, args.user_context_file, "user-context")
    suggested_impl = read_text_or_file(
        args.suggested_implementation,
        args.suggested_implementation_file,
        "suggested-implementation",
    )
    related_features = ", ".join(split_csvish(args.related_features))

    target_file = directory / TYPE_CONFIG["feature"]["file"]
    entry_id = next_id(target_file, TYPE_CONFIG["feature"]["prefix"])
    metadata_lines = [
        f"- Frequency: {args.frequency}",
        f"- Related Features: {related_features}" if related_features else "",
    ]

    entry = f"""## [{entry_id}] {args.capability}

**Logged**: {now_iso()}
**Priority**: {args.priority}
**Status**: {args.status}
**Area**: {args.area}

### Requested Capability
{args.capability.replace('-', ' ')}

### Summary
{args.summary}

### User Context
{user_context or '[TODO: add user context]'}

### Complexity Estimate
{args.complexity_estimate}

### Suggested Implementation
{suggested_impl or '[TODO: add suggested implementation]'}

### Metadata
{render_metadata(metadata_lines)}

---
"""

    if not args.dry_run:
        append_entry(target_file, entry)

    return {
        "ok": True,
        "action": "dry_run" if args.dry_run else "logged",
        "kind": "feature",
        "id": entry_id,
        "target_file": str(target_file),
        "summary": args.summary,
        "entry_markdown": entry.strip(),
    }


def add_common_output_arg(parser: argparse.ArgumentParser) -> None:
    parser.add_argument("--format", choices=OUTPUT_FORMATS, default="json", help="Output format (default: json)")


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Manage workspace .learnings files for the self-improvement skill.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    subparsers = parser.add_subparsers(dest="command", required=True)

    init_parser = subparsers.add_parser("init", help="Create .learnings files in a workspace")
    init_parser.add_argument("--root", required=True, help="Workspace root where .learnings should live")
    init_parser.add_argument("--force", action="store_true", help="Overwrite existing template files")
    add_common_output_arg(init_parser)
    init_parser.set_defaults(func=cmd_init)

    status_parser = subparsers.add_parser("status", help="Summarise current learnings status")
    status_parser.add_argument("--root", required=True, help="Workspace root containing .learnings")
    status_parser.add_argument("--limit", type=int, default=10, help="Max items for list sections")
    add_common_output_arg(status_parser)
    status_parser.set_defaults(func=cmd_status)

    search_parser = subparsers.add_parser("search", help="Search learnings entries")
    search_parser.add_argument("--root", required=True, help="Workspace root containing .learnings")
    search_parser.add_argument("--query", required=True, help="Case-insensitive search query")
    search_parser.add_argument("--kind", choices=["learning", "error", "feature"], default=None, help="Restrict to one entry kind")
    search_parser.add_argument("--limit", type=int, default=10, help="Max matches to return")
    add_common_output_arg(search_parser)
    search_parser.set_defaults(func=cmd_search)

    ll_parser = subparsers.add_parser("log-learning", help="Append a learning entry")
    ll_parser.add_argument("--root", required=True, help="Workspace root containing .learnings")
    ll_parser.add_argument("--category", required=True, choices=LEARNING_CATEGORIES)
    ll_parser.add_argument("--summary", required=True)
    ll_parser.add_argument("--priority", default="medium", choices=PRIORITIES)
    ll_parser.add_argument("--status", default="pending", choices=STATUSES)
    ll_parser.add_argument("--area", default="backend", choices=AREAS)
    ll_parser.add_argument("--details")
    ll_parser.add_argument("--details-file")
    ll_parser.add_argument("--suggested-action")
    ll_parser.add_argument("--suggested-action-file")
    ll_parser.add_argument("--source", default="conversation")
    ll_parser.add_argument("--related-files", nargs="*")
    ll_parser.add_argument("--tags", nargs="*")
    ll_parser.add_argument("--see-also", nargs="*")
    ll_parser.add_argument("--pattern-key")
    ll_parser.add_argument("--recurrence-count", type=int)
    ll_parser.add_argument("--first-seen")
    ll_parser.add_argument("--last-seen")
    ll_parser.add_argument("--dry-run", action="store_true")
    add_common_output_arg(ll_parser)
    ll_parser.set_defaults(func=cmd_log_learning)

    le_parser = subparsers.add_parser("log-error", help="Append an error entry")
    le_parser.add_argument("--root", required=True, help="Workspace root containing .learnings")
    le_parser.add_argument("--name", required=True, help="Short error identifier, e.g. docker-build")
    le_parser.add_argument("--summary", required=True)
    le_parser.add_argument("--priority", default="high", choices=PRIORITIES)
    le_parser.add_argument("--status", default="pending", choices=STATUSES)
    le_parser.add_argument("--area", default="backend", choices=AREAS)
    le_parser.add_argument("--error-text")
    le_parser.add_argument("--error-text-file")
    le_parser.add_argument("--context")
    le_parser.add_argument("--context-file")
    le_parser.add_argument("--suggested-fix")
    le_parser.add_argument("--suggested-fix-file")
    le_parser.add_argument("--reproducible", default="unknown", choices=REPRODUCIBLE)
    le_parser.add_argument("--related-files", nargs="*")
    le_parser.add_argument("--see-also", nargs="*")
    le_parser.add_argument("--dry-run", action="store_true")
    add_common_output_arg(le_parser)
    le_parser.set_defaults(func=cmd_log_error)

    lf_parser = subparsers.add_parser("log-feature", help="Append a feature request entry")
    lf_parser.add_argument("--root", required=True, help="Workspace root containing .learnings")
    lf_parser.add_argument("--capability", required=True, help="Capability slug, e.g. export-to-csv")
    lf_parser.add_argument("--summary", required=True)
    lf_parser.add_argument("--priority", default="medium", choices=PRIORITIES)
    lf_parser.add_argument("--status", default="pending", choices=STATUSES)
    lf_parser.add_argument("--area", default="backend", choices=AREAS)
    lf_parser.add_argument("--user-context")
    lf_parser.add_argument("--user-context-file")
    lf_parser.add_argument("--complexity-estimate", default="medium", choices=COMPLEXITY)
    lf_parser.add_argument("--suggested-implementation")
    lf_parser.add_argument("--suggested-implementation-file")
    lf_parser.add_argument("--frequency", default="first_time", choices=FREQUENCY)
    lf_parser.add_argument("--related-features", nargs="*")
    lf_parser.add_argument("--dry-run", action="store_true")
    add_common_output_arg(lf_parser)
    lf_parser.set_defaults(func=cmd_log_feature)

    return parser


def main() -> int:
    parser = build_parser()
    args = parser.parse_args()
    try:
        payload = args.func(args)
        print_output(payload, args.format)
        return 0
    except CliError as exc:
        eprint(f"Error: {exc}")
        return exc.exit_code


if __name__ == "__main__":
    sys.exit(main())


path: scripts/activator.sh
content: #!/usr/bin/env bash
set -euo pipefail
cat <<'MSG'
<self-improvement-reminder>
After completing this task, check whether a durable lesson emerged.
Log only reusable learning:
- user correction or updated fact
- non-obvious failure worth remembering
- project convention or workaround discovered
- missing capability worth tracking
If a pattern is proven, promote it into shared memory or extract a skill.
</self-improvement-reminder>
MSG


path: scripts/extract_skill.py
content: #!/usr/bin/env python3
"""Create a new skill scaffold from a proven learning."""

from __future__ import annotations

import argparse
import json
import re
import sys
from pathlib import Path
from typing import Any, Dict, List

NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
OUTPUT_FORMATS = ["json", "text", "markdown"]


class CliError(Exception):
    def __init__(self, message: str, exit_code: int = 1):
        super().__init__(message)
        self.exit_code = exit_code


def eprint(message: str) -> None:
    print(message, file=sys.stderr)


def print_output(payload: Any, fmt: str) -> None:
    if fmt == "json":
        print(json.dumps(payload, indent=2, ensure_ascii=False))
        return
    if fmt == "markdown":
        if isinstance(payload, dict):
            for key, value in payload.items():
                print(f"- **{key}**: {value}")
            return
    if isinstance(payload, dict):
        for key, value in payload.items():
            print(f"{key}: {value}")
        return
    print(payload)


def resolve_root(path_str: str) -> Path:
    root = Path(path_str).expanduser().resolve()
    if not root.exists():
        raise CliError(f"Workspace root does not exist: {root}", 2)
    if not root.is_dir():
        raise CliError(f"Workspace root is not a directory: {root}", 2)
    return root


def validate_output_dir(output_dir: str) -> str:
    if output_dir.startswith("/"):
        raise CliError("--output-dir must be relative to --root")
    if ".." in Path(output_dir).parts:
        raise CliError("--output-dir must not contain '..' path segments")
    cleaned = output_dir.strip() or "skills"
    return cleaned


def title_from_name(name: str) -> str:
    return " ".join(part.capitalize() for part in name.split("-"))


def skill_template(args: argparse.Namespace) -> str:
    lines: List[str] = ["---", f"name: {args.name}"]
    lines.append(f"description: {args.description or '[TODO: describe what this skill does and when to use it]'}")
    if args.compatibility:
        lines.append(f"compatibility: {args.compatibility}")
    lines.append("metadata:")
    lines.append(f"  version: \"{args.version}\"")
    if args.from_learning_id:
        lines.append(f"  source_learning: \"{args.from_learning_id}\"")
    if args.author:
        lines.append(f"  author: \"{args.author}\"")
    lines.append("---")
    lines.append("")
    lines.append(f"# {args.title or title_from_name(args.name)}")
    lines.append("")
    lines.append("One-sentence explanation of the problem this skill solves.")
    lines.append("")
    lines.append("## When to use")
    lines.append("")
    lines.append("Use this skill when:")
    lines.append("- trigger or situation 1")
    lines.append("- trigger or situation 2")
    lines.append("- trigger or situation 3")
    lines.append("")
    lines.append("Do not use it for:")
    lines.append("- adjacent case 1")
    lines.append("- adjacent case 2")
    lines.append("")
    lines.append("## Workflow")
    lines.append("")
    lines.append("1. Step one")
    lines.append("2. Step two")
    lines.append("3. Verify the result")
    lines.append("")
    lines.append("## References")
    lines.append("")
    lines.append("Move long documentation into `references/` once the skill grows.")
    lines.append("")
    lines.append("## Source")
    lines.append("")
    lines.append(f"- Learning ID: {args.from_learning_id or '[TODO: add learning id]'}")
    return "\n".join(lines) + "\n"


def evals_template(skill_name: str) -> str:
    return json.dumps(
        {
            "skill_name": skill_name,
            "evals": [
                {
                    "id": 1,
                    "prompt": "[TODO] realistic user request this new skill should handle",
                    "expected_output": "[TODO] what success looks like",
                    "assertions": [
                        "[TODO] output contains the required artefact or result",
                        "[TODO] workflow followed the expected structure",
                    ],
                }
            ],
        },
        indent=2,
    ) + "\n"


def create_files(skill_path: Path, args: argparse.Namespace) -> Dict[str, Any]:
    created: List[str] = []

    skill_path.mkdir(parents=True, exist_ok=True)
    created.append(str(skill_path))

    skill_md = skill_path / "SKILL.md"
    skill_md.write_text(skill_template(args), encoding="utf-8")
    created.append(str(skill_md))

    references_dir = skill_path / "references"
    references_dir.mkdir(exist_ok=True)
    created.append(str(references_dir))

    examples_md = references_dir / "examples.md"
    examples_md.write_text("# Examples\n\nAdd concrete examples here.\n", encoding="utf-8")
    created.append(str(examples_md))

    if args.scaffold_evals:
        evals_dir = skill_path / "evals"
        evals_dir.mkdir(exist_ok=True)
        created.append(str(evals_dir))
        evals_json = evals_dir / "evals.json"
        evals_json.write_text(evals_template(args.name), encoding="utf-8")
        created.append(str(evals_json))

    return {"created": created}


def main() -> int:
    parser = argparse.ArgumentParser(
        description="Create a new Agent Skill scaffold from a proven learning.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument("name", help="Skill name in lowercase kebab-case")
    parser.add_argument("--root", required=True, help="Workspace root where the new skill should be created")
    parser.add_argument("--output-dir", default="skills", help="Relative output directory under --root")
    parser.add_argument("--description", help="Skill description for frontmatter")
    parser.add_argument("--title", help="Human-readable title")
    parser.add_argument("--from-learning-id", help="Source learning ID, e.g. LRN-20260313-001")
    parser.add_argument("--compatibility", help="Optional compatibility frontmatter")
    parser.add_argument("--author", help="Optional metadata author value")
    parser.add_argument("--version", default="1.0.0", help="Initial version metadata")
    parser.add_argument("--scaffold-evals", action="store_true", help="Create evals/evals.json scaffold")
    parser.add_argument("--force", action="store_true", help="Overwrite an existing scaffold if present")
    parser.add_argument("--dry-run", action="store_true", help="Print what would be created without writing files")
    parser.add_argument("--format", choices=OUTPUT_FORMATS, default="json", help="Output format")
    args = parser.parse_args()

    try:
        if not NAME_RE.fullmatch(args.name):
            raise CliError("Invalid skill name. Use lowercase letters, numbers, and single hyphens only.")
        root = resolve_root(args.root)
        output_dir = validate_output_dir(args.output_dir)
        skill_path = root / output_dir / args.name

        if skill_path.exists() and not args.force:
            raise CliError(f"Skill path already exists: {skill_path}. Use --force to overwrite.")

        preview = {
            "ok": True,
            "workspace_root": str(root),
            "skill_path": str(skill_path),
            "scaffold_evals": args.scaffold_evals,
        }

        if args.dry_run:
            preview["sketch"] = skill_template(args)
            print_output(preview, args.format)
            return 0

        if skill_path.exists() and args.force:
            for child in sorted(skill_path.rglob("*"), reverse=True):
                if child.is_file():
                    child.unlink()
                elif child.is_dir():
                    child.rmdir()

        created = create_files(skill_path, args)
        preview.update(created)
        print_output(preview, args.format)
        return 0
    except CliError as exc:
        eprint(f"Error: {exc}")
        return exc.exit_code


if __name__ == "__main__":
    sys.exit(main())


path: scripts/extract-skill.sh
content: #!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
exec python3 "$SCRIPT_DIR/extract_skill.py" "$@"


path: scripts/error-detector.sh
content: #!/usr/bin/env bash
set -euo pipefail

EXIT_CODE="${CLAUDE_TOOL_EXIT_CODE:-${TOOL_EXIT_CODE:-}}"
OUTPUT="${CLAUDE_TOOL_OUTPUT:-${TOOL_OUTPUT:-}}"

contains_error=false

if [[ -n "$EXIT_CODE" && "$EXIT_CODE" != "0" ]]; then
  contains_error=true
fi

if [[ "$contains_error" == false ]]; then
  ERROR_PATTERNS=(
    "error:"
    "Error:"
    "ERROR:"
    "failed"
    "FAILED"
    "command not found"
    "No such file"
    "Permission denied"
    "fatal:"
    "Exception"
    "Traceback"
    "npm ERR!"
    "ModuleNotFoundError"
    "SyntaxError"
    "TypeError"
    "non-zero"
  )

  for pattern in "${ERROR_PATTERNS[@]}"; do
    if [[ "$OUTPUT" == *"$pattern"* ]]; then
      contains_error=true
      break
    fi
  done
fi

if [[ "$contains_error" == true ]]; then
  cat <<'MSG'
<error-detected>
A likely command failure was detected.
Consider logging it only if the failure required real diagnosis, is likely to recur, or changed future workflow.
Use the workspace `.learnings/ERRORS.md`, not the skill directory.
</error-detected>
MSG
fi


path: assets/FEATURE_REQUESTS.md
content: # Feature Requests

Missing capabilities or recurring workflow frictions that should become features, scripts, or skills.

**Areas**: frontend | backend | infra | tests | docs | config
**Statuses**: pending | in_progress | resolved | wont_fix | promoted

## When to log here

Use this file when the user asks for something the current workflow or tooling cannot yet do well.

Examples:
- a missing export format
- a repeated manual step that should be automated
- a capability gap that keeps surfacing across sessions

## Entry template

```markdown
## [FEAT-YYYYMMDD-XXX] capability-name

**Logged**: ISO-8601 timestamp
**Priority**: low | medium | high | critical
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config

### Requested Capability
What the user wanted to do

### Summary
One-line summary of the request

### User Context
Why the capability matters

### Complexity Estimate
simple | medium | complex

### Suggested Implementation
Concrete starting point for building it

### Metadata
- Frequency: first_time | recurring
- Related Features: existing-feature-name

---
```


path: assets/ERRORS.md
content: # Errors Log

Non-obvious command failures, tool/API issues, exceptions, and recurring operational problems.

**Areas**: frontend | backend | infra | tests | docs | config
**Statuses**: pending | in_progress | resolved | wont_fix | promoted

## When to log here

Use this file when the failure is worth remembering, not for every routine error.

Good candidates:
- required real debugging or investigation
- likely to recur
- revealed an environment or tooling gotcha
- should influence future troubleshooting steps

## Entry template

```markdown
## [ERR-YYYYMMDD-XXX] error-name

**Logged**: ISO-8601 timestamp
**Priority**: low | medium | high | critical
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config

### Summary
One-line description of what failed

### Error
```
Raw error message or representative output
```

### Context
What command, tool, API, or workflow was involved

### Suggested Fix
What to try next time or how to prevent recurrence

### Metadata
- Reproducible: yes | no | unknown
- Related Files: path/to/file.ext
- See Also: ERR-20260313-001

---
```


path: assets/SKILL-TEMPLATE.md
content: # Skill Template

Use this when promoting a proven learning into a standalone Agent Skill.

```markdown
---
name: skill-name
description: What the skill does and when to use it. Include trigger phrases and scope boundaries.
compatibility: Optional environment requirements.
metadata:
  version: "1.0.0"
  source_learning: "LRN-YYYYMMDD-XXX"
---

# Skill Name

One-sentence statement of the problem this skill solves.

## When to use

Use this skill when:
- trigger or situation 1
- trigger or situation 2
- trigger or situation 3

Do not use it for:
- adjacent case 1
- adjacent case 2

## Workflow

1. Step one
2. Step two
3. Verify the result

## Quick commands

```bash
# preferred command or script
```

## Edge cases

- edge case 1
- edge case 2

## References

- `references/...`
- `scripts/...`

## Source

- Learning ID: LRN-YYYYMMDD-XXX
- Extraction date: YYYY-MM-DD
```

## Checklist

Before shipping a promoted skill:
- keep the description under 1024 characters
- keep `SKILL.md` under 500 lines where possible
- move long docs into `references/`
- bundle repeated logic into `scripts/`
- add trigger and output evals


path: assets/LEARNINGS.md
content: # Learnings Log

Corrections, knowledge gaps, best practices, and durable project conventions.

**Categories**: correction | knowledge_gap | best_practice | insight
**Areas**: frontend | backend | infra | tests | docs | config
**Statuses**: pending | in_progress | resolved | wont_fix | promoted | promoted_to_skill

## When to log here

Use this file when a lesson should change future behaviour.

Examples:
- the user corrected a wrong assumption
- a project convention was discovered
- a workaround or prevention rule emerged from debugging
- a better workflow or tool usage pattern was identified

## Entry template

```markdown
## [LRN-YYYYMMDD-XXX] category

**Logged**: ISO-8601 timestamp
**Priority**: low | medium | high | critical
**Status**: pending
**Area**: frontend | backend | infra | tests | docs | config

### Summary
One-line summary of the learning

### Details
What happened, what was wrong or surprising, and what is now known to be true

### Suggested Action
Specific prevention rule, fix, or workflow change

### Metadata
- Source: conversation | error | user_feedback | docs | simplify-and-harden
- Related Files: path/to/file.ext
- Tags: tag1, tag2
- See Also: LRN-20260313-001
- Pattern-Key: stable.pattern.key
- Recurrence-Count: 1
- First-Seen: 2026-03-13
- Last-Seen: 2026-03-13

---
```

## Promotion fields

When the learning becomes durable project memory:

```markdown
**Status**: promoted
**Promoted**: CLAUDE.md
```

When it becomes a reusable skill:

```markdown
**Status**: promoted_to_skill
**Skill-Path**: skills/skill-name
```


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