auto: sync OpenClaw config 2026-09-16 16:13
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"self-improving-agent": {
|
||||
"version": "4.0.2",
|
||||
"registry": "https://clawhub.ai",
|
||||
"ownerHandle": "pskoett",
|
||||
"installedAt": 1789545691162,
|
||||
"artifact": {
|
||||
"kind": "archive",
|
||||
"sha256": "89f2a239f9d675c4c5787cf61c17f709cd38cb9c7d395f9dc50f487a59a47291",
|
||||
"integrity": "sha256-ifKiOfnWdcTFeHz2HBf3Cc04y5x9OV+dxQ9IelmkcpE="
|
||||
},
|
||||
"skillFile": {
|
||||
"path": "SKILL.md",
|
||||
"sha256": "eff889950f23bb8a3de90cd617481bbf2557f9495f884bd5b1e9b7604a20817c"
|
||||
},
|
||||
"fileTreeSha256": "sha256:cd72f5c1910da8f21f4e326cc4f3b39de8b5bd371411f6a026f1f72b6747de8d",
|
||||
"verification": {
|
||||
"schema": "clawhub.skill.verify.v1",
|
||||
"ok": false,
|
||||
"decision": "fail",
|
||||
"reasons": [
|
||||
"security.status_not_clean"
|
||||
],
|
||||
"card": {
|
||||
"available": true,
|
||||
"path": "skill-card.md",
|
||||
"url": "https://wry-manatee-359.convex.site/api/v1/skills/self-improving-agent/card?ownerHandle=pskoett&version=4.0.2",
|
||||
"sha256": "6f9beece207f7b423e9a20c1b804b664741ee8d76cc77ade7240766b32d4916d",
|
||||
"size": 2633,
|
||||
"contentType": "text/markdown; charset=utf-8"
|
||||
},
|
||||
"artifact": {
|
||||
"sourceFingerprint": "c75aa847057f2210ef7136d8456b1f91b7d7cd0352962c9f920679cce045adf2",
|
||||
"bundleFingerprints": [
|
||||
"a0b59e786c352370b6031b7f2b956eaade2dec1467933d0f97e104aa28220214",
|
||||
"3bb2a005cd20f56524be2d5e379f14b0f4cadc5ae2a6801179d46a4530385612",
|
||||
"ca25cab698e926de89a5b4548f7c090dcf5f1ca70558c537b3f998f1e95ad346"
|
||||
],
|
||||
"files": [
|
||||
{
|
||||
"path": "CHANGELOG.md",
|
||||
"size": 7598,
|
||||
"sha256": "8d8e946a3e1482470068773fa62482af3440431b3aa400df23b8c781906ba934",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "references/uninstall.md",
|
||||
"size": 1891,
|
||||
"sha256": "c569a88473ac0187e0d9dde1686eb24efe4666d84af2a149b5fb8e99b7a99828",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "references/examples.md",
|
||||
"size": 8287,
|
||||
"sha256": "ba521451abf0f1cc774b061894c94689390dafa3ce3905f63d5fa6f860a5e3b8",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "references/openclaw-integration.md",
|
||||
"size": 9814,
|
||||
"sha256": "517a40ddec886f07cf66cb7c98e61ea464af953b505610722ed46a297741708a",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "hooks/openclaw/handler.test.js",
|
||||
"size": 8921,
|
||||
"sha256": "3047f4239bc3eadcb8f12cc3515f946e86b16cb482de82ae900e5b994c403e69",
|
||||
"contentType": "text/javascript"
|
||||
},
|
||||
{
|
||||
"path": "hooks/openclaw/handler.js",
|
||||
"size": 13741,
|
||||
"sha256": "b94261dce2de54ba6bd1d6aca48d9ef11236cbe7396bd37354daa218e0ceecf3",
|
||||
"contentType": "text/javascript"
|
||||
},
|
||||
{
|
||||
"path": "hooks/openclaw/HOOK.md",
|
||||
"size": 2465,
|
||||
"sha256": "2ae0820cc5cad75d9ecce8cfc6a9c4c8baafcb4c96f820bf85f3a0e7ff2364f5",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "hooks/openclaw/handler.ts",
|
||||
"size": 14615,
|
||||
"sha256": "35f8ccd18d6979b4bacfbf1841f4f0965193ed694e46fa6cee67994f3ac838ba",
|
||||
"contentType": "application/octet-stream"
|
||||
},
|
||||
{
|
||||
"path": "scripts/extract-skill.sh",
|
||||
"size": 5293,
|
||||
"sha256": "d9af3d6f2cde0eeb3c0a85b898828656c1c6d9d51f61ba38f9801030424f5147",
|
||||
"contentType": "text/x-sh"
|
||||
},
|
||||
{
|
||||
"path": "SKILL.md",
|
||||
"size": 21361,
|
||||
"sha256": "eff889950f23bb8a3de90cd617481bbf2557f9495f884bd5b1e9b7604a20817c",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "assets/FEATURE_REQUESTS.md",
|
||||
"size": 84,
|
||||
"sha256": "a6291573936034efaad57b43b6f57bdb35ed627f01ce9309b2c781da63563c37",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "assets/ERRORS.md",
|
||||
"size": 75,
|
||||
"sha256": "731f4a26fee7074d11e999920e5c3ca63b0e025b75a4d9966429ee88a2cb5988",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "assets/SKILL-TEMPLATE.md",
|
||||
"size": 3407,
|
||||
"sha256": "3ee008f80d53e0e576f7e747ac4c4e040ff659f02d3f5689a95524a3696e4881",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"path": "assets/LEARNINGS.md",
|
||||
"size": 1135,
|
||||
"sha256": "58b31d4cda7d1f2a6fdaf1aff06f45288e6cb0161dd1ed1bf2152c7f8c3c88da",
|
||||
"contentType": "text/markdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
"provenance": {
|
||||
"source": "unavailable",
|
||||
"reason": "No server-resolved GitHub import provenance is stored for this version."
|
||||
},
|
||||
"security": {
|
||||
"status": "suspicious",
|
||||
"passed": false,
|
||||
"rawStatus": "suspicious",
|
||||
"verdict": "suspicious",
|
||||
"confidence": "high",
|
||||
"summary": "This skill is mostly purpose-aligned, but its optional hook can automatically read session transcripts and persist excerpts with insufficient path scoping and only best-effort redaction.",
|
||||
"model": null,
|
||||
"checkedAt": 1789015140848,
|
||||
"scannerReports": {
|
||||
"aig": null,
|
||||
"skillspector": null
|
||||
}
|
||||
},
|
||||
"signature": {
|
||||
"status": "unsigned"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Errors
|
||||
|
||||
Command failures and integration errors.
|
||||
|
||||
---
|
||||
|
||||
## [ERR-20260916-001] python3-run-tmp-script
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00 (观测于 2026-08-28,回溯补录)
|
||||
**Priority**: medium
|
||||
**Status**: resolved
|
||||
**Area**: backend
|
||||
|
||||
### Summary
|
||||
从 `/tmp` 运行周报生成脚本时,无法导入同目录的 `send_weekly_report.py`,脚本直接崩溃,周报未生成。
|
||||
|
||||
### Error
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "/tmp/gen_g6_report.py", line 1, in <module>
|
||||
from send_weekly_report import send_to_drafts
|
||||
ModuleNotFoundError: No module named 'send_weekly_report'
|
||||
```
|
||||
|
||||
### Context
|
||||
- Command/operation attempted: `python3 /tmp/gen_g6_report.py`(脚本首行 `from send_weekly_report import send_to_drafts`)
|
||||
- Input or parameters used: 临时脚本放在 `/tmp`,模块 `send_weekly_report.py` 在 `/home/yangxuan/.openclaw/workspace-resume/`
|
||||
- Environment details: cwd 已是 workspace-resume,但 Python 只把**脚本所在目录**(`/tmp`)加入 `sys.path`,cwd 不在其中
|
||||
- Summary of relevant output: 退出码 1,无其他副作用(未写草稿、未归档、未入库)
|
||||
|
||||
### Suggested Fix
|
||||
两条任选其一:
|
||||
1. 运行前显式指定模块搜索路径:`PYTHONPATH=/home/yangxuan/.openclaw/workspace-resume python3 /tmp/gen_g6_report.py`
|
||||
2. 直接把临时生成脚本写在 workspace 目录内再运行(`python3 gen_report.py`)
|
||||
|
||||
### Resolution
|
||||
- **Resolved**: 2026-08-28T05:34:00Z
|
||||
- **Notes**: 采用方案 1(PYTHONPATH),周报随即成功归档并同步 MySQL。
|
||||
|
||||
### Metadata
|
||||
- Reproducible: yes
|
||||
- Related Files: send_weekly_report.py
|
||||
- Pattern-Key: deps.module-not-found
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-08-28
|
||||
- Last-Seen: 2026-08-28
|
||||
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
# Feature Requests
|
||||
|
||||
Capabilities requested by the user.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,173 @@
|
||||
# Learnings
|
||||
|
||||
Corrections, insights, and knowledge gaps captured during development.
|
||||
|
||||
**Categories**: correction | insight | knowledge_gap | best_practice
|
||||
|
||||
---
|
||||
|
||||
## [LRN-20260916-001] correction
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00 (观测于 2026-08-27,回溯补录)
|
||||
**Priority**: high
|
||||
**Status**: promoted
|
||||
**Area**: docs
|
||||
|
||||
### Summary
|
||||
`TOOLS.md` / `MEMORY.md` 曾把 `send_weekly_report.py` 描述为「会同步元数据到 db」,实际该脚本从不写 SQLite。
|
||||
|
||||
### Details
|
||||
盘点归档链路时逐条核对脚本行为,确认 `send_to_drafts()` 只有两条落盘动作:
|
||||
- `archive_report()` → 写 Markdown 到 `weekly-reports/YYYY/`
|
||||
- `sync_to_mysql()` → 写 MySQL `resume` 库
|
||||
|
||||
**没有任何 SQLite 写入**。原描述会让后续会话误判「SQLite 已自动入库」,从而漏跑 `sync_reports_db.py` / `import_md_to_db.py`,造成本地索引与 Markdown 静默不一致。
|
||||
|
||||
### Suggested Action
|
||||
已修正 `TOOLS.md` 与 `MEMORY.md` 的表述,明确「脚本不写 SQLite,入库需手动执行」。
|
||||
|
||||
### Metadata
|
||||
- Source: conversation
|
||||
- Related Files: TOOLS.md, MEMORY.md, send_weekly_report.py
|
||||
- Tags: 文档, 归档, 周报, sqlite
|
||||
- Pattern-Key: docs.inaccurate-description
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-08-27
|
||||
- Last-Seen: 2026-08-27
|
||||
- **Promoted**: TOOLS.md, MEMORY.md
|
||||
|
||||
---
|
||||
|
||||
## [LRN-20260916-002] correction
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00
|
||||
**Priority**: medium
|
||||
**Status**: resolved
|
||||
**Area**: docs
|
||||
|
||||
### Summary
|
||||
`AGENTS.md` 的技能小节指向 `skills/weekly-report-g5/SKILL.md`,该路径不存在;在用技能实际是 `skills/weekly-report/SKILL.md`。
|
||||
|
||||
### Details
|
||||
2026-09-16 按 AGENTS.md 指引直接 `read` 该路径,返回 `ENOENT: no such file or directory`。核实后确认:
|
||||
- 实际在用:`skills/weekly-report/SKILL.md`(description 已覆盖 G5/G6)
|
||||
- `weekly-report-g5` 只存在于 Skill Workshop 的 proposal 目录,从未成为 live 技能
|
||||
|
||||
AGENTS.md 是每次会话必读文件,错误路径既浪费工具调用,也可能让后续会话判定「技能不存在」而重复造轮子。
|
||||
|
||||
### Suggested Action
|
||||
把 AGENTS.md 技能小节的路径改为 `skills/weekly-report/SKILL.md`。
|
||||
|
||||
### Resolution
|
||||
- **Resolved**: 2026-09-16T08:15:00Z
|
||||
- **Notes**: 已修正 `AGENTS.md` 技能小节路径,并补登自我改进技能条目;同时清理了同文件第 252 行 OKR 小节与「## 技能」标题粘连的格式残留。
|
||||
|
||||
### Metadata
|
||||
- Source: conversation
|
||||
- Related Files: AGENTS.md, skills/weekly-report/SKILL.md
|
||||
- Tags: 文档, 技能路径, 漂移
|
||||
- Pattern-Key: docs.stale-path
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-09-16
|
||||
- Last-Seen: 2026-09-16
|
||||
- See Also: LRN-20260916-004
|
||||
|
||||
---
|
||||
|
||||
## [LRN-20260916-003] knowledge_gap
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00
|
||||
**Priority**: medium
|
||||
**Status**: pending
|
||||
**Area**: config
|
||||
|
||||
### Summary
|
||||
GNU date 的 `monday this week` 返回的是**下周一**,不是本周一;用它推算周报日期范围会算出一周之后的区间。
|
||||
|
||||
### Details
|
||||
2026-09-16(周三)实测:
|
||||
- `date -d 'monday this week' +%F` → `2026-09-21`(下周一,错的)
|
||||
- `date -d 'last monday' +%F` → `2026-09-14`(本周一,对的)
|
||||
|
||||
周期语义在 GNU date 里不直观,且不会报错——属于「静默算错」,比报错更危险。
|
||||
|
||||
### Suggested Action
|
||||
周报日期范围以**用户提供的任务日期**为准,不用表达式推导;确需推导时只用 `last monday`。
|
||||
|
||||
### Metadata
|
||||
- Source: conversation
|
||||
- Related Files: send_weekly_report.py
|
||||
- Tags: shell, date, 周报, 静默错误
|
||||
- Pattern-Key: shell.date-week-start
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-09-16
|
||||
- Last-Seen: 2026-09-16
|
||||
|
||||
---
|
||||
|
||||
## [LRN-20260916-004] correction
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00
|
||||
**Updated**: 2026-09-16T08:20:00+08:00
|
||||
**Priority**: medium
|
||||
**Status**: resolved
|
||||
**Area**: config
|
||||
|
||||
### Summary
|
||||
(本条已更正)我曾误判 Skill Workshop 中 `weekly-report-g5` 的两个提案为 pending;实际状态是 `stale` 与 `applied`,**均非待处理**。
|
||||
|
||||
### Details
|
||||
首次复盘时,我只凭提案目录里 `PROPOSAL.md` 头部自带的 `status: proposal` 就断定二者「仍待处理」,并把「待处理提案数虚高」写进了改进周报。随后用权威入口核实:
|
||||
|
||||
- `skill_workshop list` → `[stale, create, clean]`(v1)、`[applied, update, clean]`(v2)
|
||||
- `skill_workshop list --status pending` → **No skill proposals matched**
|
||||
|
||||
即:v1 已 stale(基线变动,无法再应用),v2 已 applied(内容确已并入在用技能)。**「虚高待办数」这一结论不成立**,是读错来源所致。
|
||||
|
||||
附带发现:这两个目录现存仅 `PROPOSAL.md`,原先的 `proposal.json` / `rollback.json` 已不在(2026-08-28 时还在),说明其间有自动化(skill-collection-review)做过迁移/清理。
|
||||
|
||||
### Suggested Action
|
||||
判断提案状态**只用 `skill_workshop list`**,不要读 `PROPOSAL.md` 头部的 `status` 字段——那是提案正文自带的静态标记,不代表当前生命周期状态。同理,任何「状态类」结论都应以对应的工具/接口为准,而非文件表面内容。
|
||||
|
||||
### Resolution
|
||||
- **Resolved**: 2026-09-16T08:20:00Z
|
||||
- **Notes**: 已更正 `.learnings` 本条与 `2026-W38-改进周报.md`;经复核**无需清理提案**(本就不存在 pending,删除反而会丢掉 v2 已应用的历史记录)。
|
||||
|
||||
### Metadata
|
||||
- Source: conversation
|
||||
- Related Files: skill-workshop/proposals/, weekly-reports/2026/2026-W38-改进周报.md
|
||||
- Tags: skill-workshop, 提案, 误判, 来源可靠性
|
||||
- Pattern-Key: tooling.wrong-status-source
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-09-16
|
||||
- Last-Seen: 2026-09-16
|
||||
- See Also: LRN-20260916-002
|
||||
|
||||
---
|
||||
|
||||
## [LRN-20260916-005] knowledge_gap
|
||||
|
||||
**Logged**: 2026-09-16T08:10:00+08:00
|
||||
**Priority**: low
|
||||
**Status**: pending
|
||||
**Area**: docs
|
||||
|
||||
### Summary
|
||||
`sessions_search` 用中文错误关键词查询返回空,不适合当作「错误发现」入口。
|
||||
|
||||
### Details
|
||||
2026-09-16 连续两次中文查询(`错误 失败 修正` / `报错 失败 修正 不是这样 应该 重新`)均返回 `results: []`,但同一会话 transcript 中确实存在对应内容(`sessions_history` 可正常读到)。说明该检索路径对中文召回不可靠,会给出「什么都没发生」的假信号。
|
||||
|
||||
### Suggested Action
|
||||
错误复盘以 `.learnings/` 记录为主数据源,配合 transcript / 日志直读;`sessions_search` 仅作辅助,空结果不当作「无此事」。
|
||||
|
||||
### Metadata
|
||||
- Source: conversation
|
||||
- Related Files: .learnings/
|
||||
- Tags: 检索, 召回, 中文
|
||||
- Pattern-Key: tooling.empty-search-result
|
||||
- Recurrence-Count: 1
|
||||
- First-Seen: 2026-09-16
|
||||
- Last-Seen: 2026-09-16
|
||||
|
||||
---
|
||||
@@ -249,11 +249,12 @@
|
||||
- **支持新旧两种模板**(自动识别):
|
||||
- 旧模板(202601 等):多 sheet,每个 sheet 一个考核月;表头行4,目标行5起 6 项,月度评价在 J12/B12
|
||||
- 新模板(8月起):单 sheet `OKR考核`;表头行5-6,目标行8起(跳过行7示例)5 项,B序号/C任务/D关键结果/E完成/G评分/I权重/J得分;人员信息在 B3;跨月周期取考核期末(如 7月27日-8月27日→202608);月度评价字段上级可能未填(NULL)
|
||||
- **Excel 结构(旧模板)**:一个文件多 sheet(202511/202512/202601/...),每个 sheet 一个考核月## 技能
|
||||
- **Excel 结构(旧模板)**:一个文件多 sheet(202511/202512/202601/...),每个 sheet 一个考核月
|
||||
|
||||
## 技能
|
||||
|
||||
- **周报技能**:`skills/weekly-report-g5/SKILL.md`
|
||||
- **周报技能**:`skills/weekly-report/SKILL.md`(G5/G6 通用)
|
||||
- **自我改进技能**:`skills/self-improving-agent/SKILL.md`(日志在 `.learnings/`)
|
||||
|
||||
## 配置文件位置(红线)
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "self-improving-agent",
|
||||
"ownerHandle": "pskoett",
|
||||
"installedAt": 1789545691162,
|
||||
"artifact": {
|
||||
"kind": "archive",
|
||||
"sha256": "89f2a239f9d675c4c5787cf61c17f709cd38cb9c7d395f9dc50f487a59a47291",
|
||||
"integrity": "sha256-ifKiOfnWdcTFeHz2HBf3Cc04y5x9OV+dxQ9IelmkcpE="
|
||||
},
|
||||
"skillFile": {
|
||||
"path": "SKILL.md",
|
||||
"sha256": "eff889950f23bb8a3de90cd617481bbf2557f9495f884bd5b1e9b7604a20817c"
|
||||
},
|
||||
"fileTreeSha256": "sha256:cd72f5c1910da8f21f4e326cc4f3b39de8b5bd371411f6a026f1f72b6747de8d",
|
||||
"installedVersion": "4.0.2"
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this skill are documented here, including anything an
|
||||
agent or user must do when upgrading. Format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/); versions follow
|
||||
[SemVer](https://semver.org/) and match `version` in `SKILL.md` frontmatter.
|
||||
|
||||
Read this before upgrading. General upgrade rules:
|
||||
|
||||
- **OpenClaw hook changes only take effect after re-copying and restarting.**
|
||||
The hook runs from a copy: re-run
|
||||
`cp -r ~/.openclaw/skills/self-improving-agent/hooks/openclaw ~/.openclaw/hooks/self-improvement`
|
||||
and restart the gateway after upgrading.
|
||||
- `.learnings/` files are user data and are never migrated or overwritten by
|
||||
upgrades; first-use initialisation is idempotent and only creates missing
|
||||
files.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [4.0.2] - 2026-08-06
|
||||
|
||||
> 4.0.1 is skipped: that version number was already used by a build
|
||||
> published to ClawHub, so it is not reusable here.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Install commands and clone URL in the docs were wrong** (#24). The
|
||||
advertised `clawdhub install` command does not exist. The documented
|
||||
commands are now `openclaw skills install @pskoett/self-improving-agent`
|
||||
(built-in, installs into the active workspace) and
|
||||
`clawhub install @pskoett/self-improving-agent` (ClawHub CLI,
|
||||
`npm i -g clawhub`, installs into `./skills` under the current working
|
||||
directory). Both take the `@owner/slug` form — a bare slug is only
|
||||
accepted for already-installed or unambiguous skills. Fixed in
|
||||
`README.md`, `SKILL.md`, and `references/openclaw-integration.md`.
|
||||
- The manual `git clone` URL used the wrong owner handle
|
||||
(`peterskoett` → `pskoett`). The old handle only resolved via a web
|
||||
redirect, which `git clone` does not follow, so manual installs failed on
|
||||
a fresh machine.
|
||||
|
||||
### Added
|
||||
|
||||
- GitHub Actions CI (`.github/workflows/ci.yml` at the repo root): runs the
|
||||
hook test suite, type-checks `handler.ts` (strict mode, against a CI-only
|
||||
type stub), and syntax-checks `extract-skill.sh` on every push to master
|
||||
and every PR.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Repo restructured for ClawHub publishing**: the skill package now lives
|
||||
in the repo's `self-improving-agent/` subfolder (SKILL.md, assets, hooks,
|
||||
references, scripts, this changelog), keeping repo-level files (README,
|
||||
`.github/`) out of the published skill. Install by copying the subfolder,
|
||||
not the repo root — install/upgrade commands in the docs are updated.
|
||||
- `SKILL.md` frontmatter `name` corrected from `self-improvement` to
|
||||
`self-improving-agent` to match the skill folder name, as the Agent Skills
|
||||
spec requires. The OpenClaw *hook* keeps its `self-improvement` name, so
|
||||
existing hook installs and `openclaw hooks enable self-improvement` are
|
||||
unaffected.
|
||||
|
||||
### Upgrade notes (4.0.x → 4.0.2)
|
||||
|
||||
Documentation only — no skill behavior, hook, or `.learnings/` changes. No
|
||||
action needed beyond reinstalling if you want the corrected install docs.
|
||||
|
||||
## [4.0.0] - 2026-07-04
|
||||
|
||||
### Removed
|
||||
|
||||
- **This distribution is now OpenClaw-only.** Removed the Claude Code /
|
||||
Codex / Copilot integrations: `scripts/activator.sh`,
|
||||
`scripts/error-detector.sh`, `references/hooks-setup.md`, and the
|
||||
multi-agent setup/support sections in `SKILL.md` and
|
||||
`references/uninstall.md`. For other agents, use the original multi-agent
|
||||
version: https://github.com/pskoett/pskoett-ai-skills.
|
||||
|
||||
### Changed
|
||||
|
||||
- Promotion targets are now the OpenClaw workspace files (`SOUL.md`,
|
||||
`TOOLS.md`, `AGENTS.md`), with the project's own agent file as the target
|
||||
for project-specific patterns.
|
||||
- Hook docs and code comments describe the session-end sweep on its own
|
||||
terms (OpenClaw has no per-tool-call event) instead of contrasting with
|
||||
other platforms.
|
||||
|
||||
### Upgrade notes (0.3.0 → 4.0.0)
|
||||
|
||||
1. If you configured the Claude Code/Codex hooks from earlier versions,
|
||||
remove the stale `.claude/settings.json` / `.codex/settings.json` entries
|
||||
pointing at `activator.sh` / `error-detector.sh`, and switch to the
|
||||
original multi-agent skill for those platforms.
|
||||
2. Nothing changes for OpenClaw installs beyond the usual re-copy of the
|
||||
hook directory; `.learnings/` data and entry formats are unaffected.
|
||||
|
||||
## [0.3.0] - 2026-07-04
|
||||
|
||||
### Added
|
||||
|
||||
- **Pattern-Key generalized to all three log files**: the `ERRORS.md` and
|
||||
`FEATURE_REQUESTS.md` entry formats now carry a `Pattern-Key` field
|
||||
(recommended for errors, optional for features), joining `LEARNINGS.md`
|
||||
where it was previously limited to the simplify-and-harden feed.
|
||||
- **Pattern-Key Taxonomy** section in `SKILL.md`: controlled `area.symptom`
|
||||
namespaces (`api`, `auth`, `build`, `config`, `deps`, `fs`, `net`,
|
||||
`runtime`, `shell`, `vcs`, plus `simplify`/`harden`), with reuse-before-mint
|
||||
and one-key-per-entry rules.
|
||||
- The OpenClaw session-end error sweep stamps deterministic `Pattern-Key`
|
||||
values on auto-detected entries (e.g. `ModuleNotFoundError` →
|
||||
`deps.module-not-found`), making them recurrence-countable with no agent
|
||||
discipline required.
|
||||
|
||||
### Changed
|
||||
|
||||
- Grep-by-`Pattern-Key` is now the documented **default dedup step** when
|
||||
logging; keyword grep is the fallback. Recurrences are folded into the
|
||||
existing entry (`Recurrence-Count`, `Last-Seen`, `See Also`) instead of
|
||||
duplicated.
|
||||
- Tightened `SKILL.md` (hook sections, taxonomy, duplicated setup blocks) to
|
||||
keep the always-loaded skill prompt compact.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `scripts/extract-skill.sh` is now committed with the executable bit set —
|
||||
previously every install needed a manual `chmod +x`.
|
||||
|
||||
### Upgrade notes (0.2.0 → 0.3.0)
|
||||
|
||||
1. Re-copy the OpenClaw hook and restart the gateway (see general rules
|
||||
above) to get Pattern-Key stamping on swept entries.
|
||||
2. Existing `.learnings/` entries without `Pattern-Key` remain valid — the
|
||||
field is additive. Add keys opportunistically when touching old entries.
|
||||
|
||||
## [0.2.0] - 2026-07-04
|
||||
|
||||
### Added
|
||||
|
||||
- **OpenClaw session-end error sweep** (`hooks/openclaw/`): the hook now also
|
||||
fires on `command:new` / `command:reset`, scans the ended session's
|
||||
transcript for the same error patterns as `scripts/error-detector.sh`, and
|
||||
appends pending entries (`Source: openclaw-error-sweep`) to
|
||||
`<workspace>/.learnings/ERRORS.md`. Opt-in: runs only when
|
||||
`<workspace>/.learnings/` exists. Excerpts are capped, truncated, redacted,
|
||||
and deduplicated.
|
||||
- Pending-triage note in the bootstrap reminder when auto-detected error
|
||||
entries await review.
|
||||
- Test suite: `node --test hooks/openclaw/handler.test.js` (no dependencies).
|
||||
- Docs: "Error Detection on OpenClaw" section and platform support matrix in
|
||||
`references/openclaw-integration.md`; uninstall guide in
|
||||
`references/uninstall.md`; this changelog.
|
||||
|
||||
### Changed
|
||||
|
||||
- `hooks/openclaw/HOOK.md` events metadata is now
|
||||
`["agent:bootstrap", "command:new", "command:reset"]`.
|
||||
- Docs now state explicitly that `scripts/error-detector.sh` is Claude Code
|
||||
only (OpenClaw has no `PostToolUse` equivalent).
|
||||
|
||||
### Upgrade notes (0.1.x → 0.2.0)
|
||||
|
||||
1. Re-copy the hook and restart the gateway (see general rules above).
|
||||
2. To enable the new error sweep: `mkdir -p ~/.openclaw/workspace/.learnings`.
|
||||
Without that directory, behavior is identical to 0.1.0.
|
||||
3. No breaking changes; no `.learnings/` migration needed.
|
||||
|
||||
## [0.1.0] - 2026-01-31
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release: `SKILL.md` with logging formats (`LEARNINGS.md`,
|
||||
`ERRORS.md`, `FEATURE_REQUESTS.md`), promotion workflow, and skill
|
||||
extraction.
|
||||
- OpenClaw bootstrap-reminder hook (`agent:bootstrap`).
|
||||
- Claude Code hook scripts: `activator.sh` (UserPromptSubmit) and
|
||||
`error-detector.sh` (PostToolUse).
|
||||
- Reference guides for hooks setup and OpenClaw integration.
|
||||
@@ -0,0 +1,594 @@
|
||||
---
|
||||
name: self-improving-agent
|
||||
description: "Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks."
|
||||
version: "4.0.2"
|
||||
metadata:
|
||||
---
|
||||
|
||||
# Self-Improvement Skill
|
||||
|
||||
Log learnings and errors to markdown files for continuous improvement. Agents can later process these into fixes, and important learnings get promoted to workspace memory. This version of the skill is built for OpenClaw only — for other agents, see the original multi-agent version at https://github.com/pskoett/pskoett-ai-skills.
|
||||
|
||||
## First-Use Initialisation
|
||||
|
||||
Before logging anything, ensure the `.learnings/` directory and files exist in the project or workspace root. If any are missing, create them:
|
||||
|
||||
```bash
|
||||
mkdir -p .learnings
|
||||
[ -f .learnings/LEARNINGS.md ] || printf "# Learnings\n\nCorrections, insights, and knowledge gaps captured during development.\n\n**Categories**: correction | insight | knowledge_gap | best_practice\n\n---\n" > .learnings/LEARNINGS.md
|
||||
[ -f .learnings/ERRORS.md ] || printf "# Errors\n\nCommand failures and integration errors.\n\n---\n" > .learnings/ERRORS.md
|
||||
[ -f .learnings/FEATURE_REQUESTS.md ] || printf "# Feature Requests\n\nCapabilities requested by the user.\n\n---\n" > .learnings/FEATURE_REQUESTS.md
|
||||
```
|
||||
|
||||
Never overwrite existing files. This is a no-op if `.learnings/` is already initialised.
|
||||
|
||||
Do not log secrets, tokens, private keys, environment variables, or full source/config files unless the user explicitly asks for that level of detail. Prefer short summaries or redacted excerpts over raw command output or full transcripts.
|
||||
|
||||
If you want automatic reminders and session-end error detection, enable the opt-in hook described in [Optional: Enable Hook](#optional-enable-hook).
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| Command/operation fails | Log to `.learnings/ERRORS.md` |
|
||||
| User corrects you | Log to `.learnings/LEARNINGS.md` with category `correction` |
|
||||
| User wants missing feature | Log to `.learnings/FEATURE_REQUESTS.md` |
|
||||
| API/external tool fails | Log to `.learnings/ERRORS.md` with integration details |
|
||||
| Knowledge was outdated | Log to `.learnings/LEARNINGS.md` with category `knowledge_gap` |
|
||||
| Found better approach | Log to `.learnings/LEARNINGS.md` with category `best_practice` |
|
||||
| Simplify/Harden recurring patterns | Log/update `.learnings/LEARNINGS.md` with `Source: simplify-and-harden` and a stable `Pattern-Key` |
|
||||
| Similar to existing entry | Grep by `Pattern-Key` first, link with `**See Also**`, bump `Recurrence-Count` |
|
||||
| Workflow improvements | Promote to `AGENTS.md` (workspace) |
|
||||
| Tool gotchas | Promote to `TOOLS.md` (workspace) |
|
||||
| Behavioral patterns | Promote to `SOUL.md` (workspace) |
|
||||
|
||||
## OpenClaw Setup
|
||||
|
||||
OpenClaw uses workspace-based prompt injection with automatic skill loading.
|
||||
|
||||
### Installation
|
||||
|
||||
**Via OpenClaw's built-in installer (recommended)** — installs into the
|
||||
active OpenClaw workspace:
|
||||
```bash
|
||||
openclaw skills install @pskoett/self-improving-agent
|
||||
```
|
||||
|
||||
**Via the ClawHub CLI** (`npm i -g clawhub`) — installs into `./skills`
|
||||
under the current working directory, not the workspace:
|
||||
```bash
|
||||
clawhub install @pskoett/self-improving-agent
|
||||
```
|
||||
|
||||
**Manual** (the skill lives in the repo's `self-improving-agent/` subfolder;
|
||||
copy that folder, not the repo root):
|
||||
```bash
|
||||
git clone https://github.com/pskoett/self-improving-agent.git /tmp/self-improving-agent-repo
|
||||
cp -r /tmp/self-improving-agent-repo/self-improving-agent ~/.openclaw/skills/self-improving-agent
|
||||
```
|
||||
|
||||
Remade for openclaw from original repo : https://github.com/pskoett/pskoett-ai-skills - https://github.com/pskoett/pskoett-ai-skills/tree/main/skills/self-improvement
|
||||
|
||||
### Workspace Structure
|
||||
|
||||
OpenClaw injects these files into every session:
|
||||
|
||||
```
|
||||
~/.openclaw/workspace/
|
||||
├── AGENTS.md # Multi-agent workflows, delegation patterns
|
||||
├── SOUL.md # Behavioral guidelines, personality, principles
|
||||
├── TOOLS.md # Tool capabilities, integration gotchas
|
||||
├── MEMORY.md # Long-term memory (main session only)
|
||||
├── memory/ # Daily memory files
|
||||
│ └── YYYY-MM-DD.md
|
||||
└── .learnings/ # This skill's log files
|
||||
├── LEARNINGS.md
|
||||
├── ERRORS.md
|
||||
└── FEATURE_REQUESTS.md
|
||||
```
|
||||
|
||||
### Create Learning Files
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
Then create the log files (or copy from `assets/`):
|
||||
- `LEARNINGS.md` — corrections, knowledge gaps, best practices
|
||||
- `ERRORS.md` — command failures, exceptions
|
||||
- `FEATURE_REQUESTS.md` — user-requested capabilities
|
||||
|
||||
### Promotion Targets
|
||||
|
||||
When learnings prove broadly applicable, promote them to workspace files:
|
||||
|
||||
| Learning Type | Promote To | Example |
|
||||
|---------------|------------|---------|
|
||||
| Behavioral patterns | `SOUL.md` | "Be concise, avoid disclaimers" |
|
||||
| Workflow improvements | `AGENTS.md` | "Spawn sub-agents for long tasks" |
|
||||
| Tool gotchas | `TOOLS.md` | "Git push needs auth configured first" |
|
||||
|
||||
### Inter-Session Communication
|
||||
|
||||
OpenClaw provides tools to share learnings across sessions:
|
||||
|
||||
- **sessions_list** — View active/recent sessions
|
||||
- **sessions_history** — Read another session's transcript
|
||||
- **sessions_send** — Send a learning to another session
|
||||
- **sessions_spawn** — Spawn a sub-agent for background work
|
||||
|
||||
Use these only in trusted environments and only when the user explicitly wants cross-session sharing. Prefer sending a short sanitized summary and relevant file paths, not raw transcripts, secrets, or full command output.
|
||||
|
||||
### Optional: Enable Hook
|
||||
|
||||
For automatic reminders at session start and error detection at session end:
|
||||
|
||||
```bash
|
||||
cp -r ~/.openclaw/skills/self-improving-agent/hooks/openclaw ~/.openclaw/hooks/self-improvement
|
||||
openclaw hooks enable self-improvement
|
||||
```
|
||||
|
||||
Fires on `agent:bootstrap` (injects the reminder, plus a pending-triage note
|
||||
when auto-detected errors await review) and on `command:new`/`command:reset`
|
||||
(sweeps the ended session's transcript for error patterns into
|
||||
`<workspace>/.learnings/ERRORS.md`; opt-in — runs only when `.learnings/`
|
||||
exists). OpenClaw has no per-tool-call hook event, so error detection happens
|
||||
at session end. See `references/openclaw-integration.md` for details and
|
||||
sweep limitations.
|
||||
|
||||
## Logging Format
|
||||
|
||||
### Learning Entry
|
||||
|
||||
Append to `.learnings/LEARNINGS.md`:
|
||||
|
||||
```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 description of what was learned
|
||||
|
||||
### Details
|
||||
Full context: what happened, what was wrong, what's correct
|
||||
|
||||
### Suggested Action
|
||||
Specific fix or improvement to make
|
||||
|
||||
### Metadata
|
||||
- Source: conversation | error | user_feedback
|
||||
- Related Files: path/to/file.ext
|
||||
- Tags: tag1, tag2
|
||||
- See Also: LRN-20250110-001 (if related to existing entry)
|
||||
- Pattern-Key: area.symptom (recommended; e.g. deps.module-not-found, simplify.dead_code — see Pattern-Key Taxonomy)
|
||||
- Recurrence-Count: 1 (optional)
|
||||
- First-Seen: 2025-01-15 (optional)
|
||||
- Last-Seen: 2025-01-15 (optional)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### Error Entry
|
||||
|
||||
Append to `.learnings/ERRORS.md`:
|
||||
|
||||
```markdown
|
||||
## [ERR-YYYYMMDD-XXX] skill_or_command_name
|
||||
|
||||
**Logged**: ISO-8601 timestamp
|
||||
**Priority**: high
|
||||
**Status**: pending
|
||||
**Area**: frontend | backend | infra | tests | docs | config
|
||||
|
||||
### Summary
|
||||
Brief description of what failed
|
||||
|
||||
### Error
|
||||
```
|
||||
Actual error message or output
|
||||
```
|
||||
|
||||
### Context
|
||||
- Command/operation attempted
|
||||
- Input or parameters used
|
||||
- Environment details if relevant
|
||||
- Summary or redacted excerpt of relevant output (avoid full transcripts and secret-bearing data by default)
|
||||
|
||||
### Suggested Fix
|
||||
If identifiable, what might resolve this
|
||||
|
||||
### Metadata
|
||||
- Reproducible: yes | no | unknown
|
||||
- Related Files: path/to/file.ext
|
||||
- See Also: ERR-20250110-001 (if recurring)
|
||||
- Pattern-Key: area.symptom (recommended; e.g. net.connection-refused — see Pattern-Key Taxonomy)
|
||||
- Recurrence-Count: 1 (optional)
|
||||
- First-Seen: 2025-01-15 (optional)
|
||||
- Last-Seen: 2025-01-15 (optional)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
### Feature Request Entry
|
||||
|
||||
Append to `.learnings/FEATURE_REQUESTS.md`:
|
||||
|
||||
```markdown
|
||||
## [FEAT-YYYYMMDD-XXX] capability_name
|
||||
|
||||
**Logged**: ISO-8601 timestamp
|
||||
**Priority**: medium
|
||||
**Status**: pending
|
||||
**Area**: frontend | backend | infra | tests | docs | config
|
||||
|
||||
### Requested Capability
|
||||
What the user wanted to do
|
||||
|
||||
### User Context
|
||||
Why they needed it, what problem they're solving
|
||||
|
||||
### Complexity Estimate
|
||||
simple | medium | complex
|
||||
|
||||
### Suggested Implementation
|
||||
How this could be built, what it might extend
|
||||
|
||||
### Metadata
|
||||
- Frequency: first_time | recurring
|
||||
- Related Features: existing_feature_name
|
||||
- Pattern-Key: area.symptom (optional — features usually dedupe by capability name; use a key only for recurring themes, e.g. api.missing-endpoint)
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## ID Generation
|
||||
|
||||
Format: `TYPE-YYYYMMDD-XXX`
|
||||
- TYPE: `LRN` (learning), `ERR` (error), `FEAT` (feature)
|
||||
- YYYYMMDD: Current date
|
||||
- XXX: Sequential number or random 3 chars (e.g., `001`, `A7B`)
|
||||
|
||||
Examples: `LRN-20250115-001`, `ERR-20250115-A3F`, `FEAT-20250115-002`
|
||||
|
||||
## Resolving Entries
|
||||
|
||||
When an issue is fixed, update the entry:
|
||||
|
||||
1. Change `**Status**: pending` → `**Status**: resolved`
|
||||
2. Add resolution block after Metadata:
|
||||
|
||||
```markdown
|
||||
### Resolution
|
||||
- **Resolved**: 2025-01-16T09:00:00Z
|
||||
- **Commit/PR**: abc123 or #42
|
||||
- **Notes**: Brief description of what was done
|
||||
```
|
||||
|
||||
Other status values:
|
||||
- `in_progress` - Actively being worked on
|
||||
- `wont_fix` - Decided not to address (add reason in Resolution notes)
|
||||
- `promoted` - Elevated to a workspace file (`SOUL.md`, `TOOLS.md`, `AGENTS.md`)
|
||||
|
||||
## Promoting to Workspace Memory
|
||||
|
||||
When a learning is broadly applicable (not a one-off fix), promote it to a workspace file so every session inherits it.
|
||||
|
||||
### When to Promote
|
||||
|
||||
- Learning applies across multiple files/features
|
||||
- Knowledge any contributor (human or AI) should know
|
||||
- Prevents recurring mistakes
|
||||
- Documents project-specific conventions
|
||||
|
||||
### Promotion Targets
|
||||
|
||||
| Target | What Belongs There |
|
||||
|--------|-------------------|
|
||||
| `SOUL.md` | Behavioral guidelines, communication style, principles |
|
||||
| `TOOLS.md` | Tool capabilities, usage patterns, integration gotchas |
|
||||
| `AGENTS.md` | Workflows, delegation patterns, automation rules |
|
||||
|
||||
When the learning is specific to a project repo you work in (not the
|
||||
workspace), promote to that project's own agent file (e.g. its `AGENTS.md`)
|
||||
instead.
|
||||
|
||||
### How to Promote
|
||||
|
||||
1. **Distill** the learning into a concise rule or fact
|
||||
2. **Add** to appropriate section in target file (create file if needed)
|
||||
3. **Update** original entry:
|
||||
- Change `**Status**: pending` → `**Status**: promoted`
|
||||
- Add `**Promoted**: SOUL.md`, `TOOLS.md`, or `AGENTS.md`
|
||||
|
||||
### Promotion Examples
|
||||
|
||||
**Learning** (verbose):
|
||||
> Project uses pnpm workspaces. Attempted `npm install` but failed.
|
||||
> Lock file is `pnpm-lock.yaml`. Must use `pnpm install`.
|
||||
|
||||
**In TOOLS.md** (concise):
|
||||
```markdown
|
||||
## Build & Dependencies
|
||||
- Package manager: pnpm (not npm) - use `pnpm install`
|
||||
```
|
||||
|
||||
**Learning** (verbose):
|
||||
> When modifying API endpoints, must regenerate TypeScript client.
|
||||
> Forgetting this causes type mismatches at runtime.
|
||||
|
||||
**In AGENTS.md** (actionable):
|
||||
```markdown
|
||||
## After API Changes
|
||||
1. Regenerate client: `pnpm run generate:api`
|
||||
2. Check for type errors: `pnpm tsc --noEmit`
|
||||
```
|
||||
|
||||
## Pattern-Key Taxonomy
|
||||
|
||||
`Pattern-Key` is the stable dedup and recurrence key for entries in all three
|
||||
log files: keyword grep misses semantically identical but differently-worded
|
||||
entries, a shared key does not — and reliable keys are what make
|
||||
`Recurrence-Count` and the promotion rule work.
|
||||
|
||||
**Format**: `area.symptom` — exactly two levels, lowercase, hyphenated
|
||||
(e.g. `deps.module-not-found`). Keep symptoms generic enough to recur: no
|
||||
file names, versions, or hostnames in keys.
|
||||
|
||||
| Area | Scope | Example Keys |
|
||||
|------|-------|--------------|
|
||||
| `api` | External API/service behavior | `api.rate-limit`, `api.schema-mismatch`, `api.missing-endpoint` |
|
||||
| `auth` | Credentials, tokens, scopes | `auth.token-expired`, `auth.missing-scope` |
|
||||
| `build` | Compilation, bundling, CI | `build.type-error`, `build.missing-artifact` |
|
||||
| `config` | Config files, env vars, settings | `config.missing-env`, `config.invalid-json` |
|
||||
| `deps` | Package managers, dependencies | `deps.module-not-found`, `deps.npm-error`, `deps.version-conflict` |
|
||||
| `fs` | Filesystem | `fs.no-such-file`, `fs.permission-denied` |
|
||||
| `net` | Network connectivity | `net.connection-refused`, `net.timeout` |
|
||||
| `runtime` | Language/runtime errors not covered above | `runtime.type-error`, `runtime.python-exception` |
|
||||
| `shell` | Shell/CLI mechanics | `shell.command-not-found`, `shell.nonzero-exit` |
|
||||
| `vcs` | Git and other version control | `vcs.fatal-error`, `vcs.merge-conflict` |
|
||||
| `simplify` / `harden` | Code-quality patterns from the simplify-and-harden feed | `simplify.dead_code`, `harden.input_validation` |
|
||||
|
||||
**Rules:**
|
||||
|
||||
1. **Reuse before minting**: `grep -rh "Pattern-Key:" .learnings/ | sort -u` —
|
||||
a near-match beats a new key.
|
||||
2. **One key per manual entry**; auto-swept OpenClaw entries may carry
|
||||
several — reduce to one when triaging.
|
||||
3. **Mint new areas sparingly** — only when several entries would share one.
|
||||
4. **Generic sweep keys** (`runtime.error`, `runtime.failure`) mean
|
||||
"unclassified" — replace with a specific key during triage.
|
||||
|
||||
## Recurring Pattern Detection
|
||||
|
||||
If logging something similar to an existing entry:
|
||||
|
||||
1. **Search by key first**: `grep -n "Pattern-Key: area.symptom" .learnings/*.md`
|
||||
— this is the default dedup check and catches rewordings that keyword
|
||||
search misses
|
||||
2. **Fallback keyword search**: `grep -ri "keyword" .learnings/` for entries
|
||||
logged without a key
|
||||
3. **Fold, don't duplicate**: on a hit, update the existing entry — bump
|
||||
`Recurrence-Count`, set `Last-Seen`, add `**See Also**` — instead of
|
||||
creating a new one
|
||||
4. **Bump priority** if issue keeps recurring
|
||||
5. **Consider systemic fix**: Recurring issues often indicate:
|
||||
- Missing knowledge (→ promote to `TOOLS.md` or `SOUL.md`)
|
||||
- Missing automation (→ add to `AGENTS.md`)
|
||||
- Architectural problem (→ create tech debt ticket)
|
||||
|
||||
## Simplify & Harden Feed
|
||||
|
||||
Use this workflow to ingest recurring patterns from the `simplify-and-harden`
|
||||
skill and turn them into durable prompt guidance.
|
||||
|
||||
### Ingestion Workflow
|
||||
|
||||
1. Read `simplify_and_harden.learning_loop.candidates` from the task summary.
|
||||
2. For each candidate, use `pattern_key` as the stable dedupe key.
|
||||
3. Search `.learnings/LEARNINGS.md` for an existing entry with that key:
|
||||
- `grep -n "Pattern-Key: <pattern_key>" .learnings/LEARNINGS.md`
|
||||
4. If found:
|
||||
- Increment `Recurrence-Count`
|
||||
- Update `Last-Seen`
|
||||
- Add `See Also` links to related entries/tasks
|
||||
5. If not found:
|
||||
- Create a new `LRN-...` entry
|
||||
- Set `Source: simplify-and-harden`
|
||||
- Set `Pattern-Key`, `Recurrence-Count: 1`, and `First-Seen`/`Last-Seen`
|
||||
|
||||
### Promotion Rule (System Prompt Feedback)
|
||||
|
||||
Promote recurring patterns into agent context/system prompt files when all are true:
|
||||
|
||||
- `Recurrence-Count >= 3`
|
||||
- Seen across at least 2 distinct tasks
|
||||
- Occurred within a 30-day window
|
||||
|
||||
Promotion targets: `SOUL.md`, `TOOLS.md`, or `AGENTS.md` (workspace), or the
|
||||
project's own agent file when the pattern is project-specific.
|
||||
|
||||
Write promoted rules as short prevention rules (what to do before/while coding),
|
||||
not long incident write-ups.
|
||||
|
||||
## Periodic Review
|
||||
|
||||
Review `.learnings/` at natural breakpoints:
|
||||
|
||||
### When to Review
|
||||
- Before starting a new major task
|
||||
- After completing a feature
|
||||
- When working in an area with past learnings
|
||||
- Weekly during active development
|
||||
|
||||
### Quick Status Check
|
||||
```bash
|
||||
# Count pending items
|
||||
grep -h "Status\*\*: pending" .learnings/*.md | wc -l
|
||||
|
||||
# List pending high-priority items
|
||||
grep -B5 "Priority\*\*: high" .learnings/*.md | grep "^## \["
|
||||
|
||||
# Find learnings for a specific area
|
||||
grep -l "Area\*\*: backend" .learnings/*.md
|
||||
```
|
||||
|
||||
### Review Actions
|
||||
- Resolve fixed items
|
||||
- Promote applicable learnings
|
||||
- Link related entries
|
||||
- Escalate recurring issues
|
||||
|
||||
## Detection Triggers
|
||||
|
||||
Automatically log when you notice:
|
||||
|
||||
**Corrections** (→ learning with `correction` category):
|
||||
- "No, that's not right..."
|
||||
- "Actually, it should be..."
|
||||
- "You're wrong about..."
|
||||
- "That's outdated..."
|
||||
|
||||
**Feature Requests** (→ feature request):
|
||||
- "Can you also..."
|
||||
- "I wish you could..."
|
||||
- "Is there a way to..."
|
||||
- "Why can't you..."
|
||||
|
||||
**Knowledge Gaps** (→ learning with `knowledge_gap` category):
|
||||
- User provides information you didn't know
|
||||
- Documentation you referenced is outdated
|
||||
- API behavior differs from your understanding
|
||||
|
||||
**Errors** (→ error entry):
|
||||
- Command returns non-zero exit code
|
||||
- Exception or stack trace
|
||||
- Unexpected output or behavior
|
||||
- Timeout or connection failure
|
||||
|
||||
## Priority Guidelines
|
||||
|
||||
| Priority | When to Use |
|
||||
|----------|-------------|
|
||||
| `critical` | Blocks core functionality, data loss risk, security issue |
|
||||
| `high` | Significant impact, affects common workflows, recurring issue |
|
||||
| `medium` | Moderate impact, workaround exists |
|
||||
| `low` | Minor inconvenience, edge case, nice-to-have |
|
||||
|
||||
## Area Tags
|
||||
|
||||
Use to filter learnings by codebase region:
|
||||
|
||||
| Area | Scope |
|
||||
|------|-------|
|
||||
| `frontend` | UI, components, client-side code |
|
||||
| `backend` | API, services, server-side code |
|
||||
| `infra` | CI/CD, deployment, Docker, cloud |
|
||||
| `tests` | Test files, testing utilities, coverage |
|
||||
| `docs` | Documentation, comments, READMEs |
|
||||
| `config` | Configuration files, environment, settings |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Log immediately** - context is freshest right after the issue
|
||||
2. **Be specific** - future agents need to understand quickly
|
||||
3. **Include reproduction steps** - especially for errors
|
||||
4. **Link related files** - makes fixes easier
|
||||
5. **Suggest concrete fixes** - not just "investigate"
|
||||
6. **Use consistent categories** - enables filtering
|
||||
7. **Promote aggressively** - if in doubt, add to `TOOLS.md` or `SOUL.md`
|
||||
8. **Review regularly** - stale learnings lose value
|
||||
|
||||
## Gitignore Options
|
||||
|
||||
**Keep learnings local** (per-developer):
|
||||
```gitignore
|
||||
.learnings/
|
||||
```
|
||||
|
||||
This repo uses that default to avoid committing sensitive or noisy local logs by accident.
|
||||
|
||||
**Track learnings in repo** (team-wide):
|
||||
Don't add to .gitignore - learnings become shared knowledge.
|
||||
|
||||
**Hybrid** (track templates, ignore entries):
|
||||
```gitignore
|
||||
.learnings/*.md
|
||||
!.learnings/.gitkeep
|
||||
```
|
||||
|
||||
## Upgrading & Uninstalling
|
||||
|
||||
Read `CHANGELOG.md` before upgrading — it carries per-version notes, and
|
||||
hook changes require re-copying the hook and restarting the gateway.
|
||||
To disable or remove the skill, follow `references/uninstall.md`:
|
||||
`.learnings/` is user data (review before deleting), and content promoted to
|
||||
`SOUL.md`/`TOOLS.md`/`AGENTS.md` stays until removed manually.
|
||||
|
||||
## Automatic Skill Extraction
|
||||
|
||||
When a learning is valuable enough to become a reusable skill, extract it using the provided helper.
|
||||
|
||||
### Skill Extraction Criteria
|
||||
|
||||
A learning qualifies for skill extraction when ANY of these apply:
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| **Recurring** | Has `See Also` links to 2+ similar issues |
|
||||
| **Verified** | Status is `resolved` with working fix |
|
||||
| **Non-obvious** | Required actual debugging/investigation to discover |
|
||||
| **Broadly applicable** | Not project-specific; useful across codebases |
|
||||
| **User-flagged** | User says "save this as a skill" or similar |
|
||||
|
||||
### Extraction Workflow
|
||||
|
||||
1. **Identify candidate**: Learning meets extraction criteria
|
||||
2. **Run helper** (or create manually):
|
||||
```bash
|
||||
~/.openclaw/skills/self-improving-agent/scripts/extract-skill.sh skill-name --dry-run
|
||||
~/.openclaw/skills/self-improving-agent/scripts/extract-skill.sh skill-name
|
||||
```
|
||||
3. **Customize SKILL.md**: Fill in template with learning content
|
||||
4. **Update learning**: Set status to `promoted_to_skill`, add `Skill-Path`
|
||||
5. **Verify**: Read skill in fresh session to ensure it's self-contained
|
||||
|
||||
### Manual Extraction
|
||||
|
||||
If you prefer manual creation:
|
||||
|
||||
1. Create `skills/<skill-name>/SKILL.md`
|
||||
2. Use template from `assets/SKILL-TEMPLATE.md`
|
||||
3. Follow [Agent Skills spec](https://agentskills.io/specification):
|
||||
- YAML frontmatter with `name` and `description`
|
||||
- Name must match folder name
|
||||
- No README.md inside skill folder
|
||||
|
||||
### Extraction Detection Triggers
|
||||
|
||||
Watch for these signals that a learning should become a skill:
|
||||
|
||||
**In conversation:**
|
||||
- "Save this as a skill"
|
||||
- "I keep running into this"
|
||||
- "This would be useful for other projects"
|
||||
- "Remember this pattern"
|
||||
|
||||
**In learning entries:**
|
||||
- Multiple `See Also` links (recurring issue)
|
||||
- High priority + resolved status
|
||||
- Category: `best_practice` with broad applicability
|
||||
- User feedback praising the solution
|
||||
|
||||
### Skill Quality Gates
|
||||
|
||||
Before extraction, verify:
|
||||
|
||||
- [ ] Solution is tested and working
|
||||
- [ ] Description is clear without original context
|
||||
- [ ] Code examples are self-contained
|
||||
- [ ] No project-specific hardcoded values
|
||||
- [ ] Follows skill naming conventions (lowercase, hyphens)
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn70cjr952qdec1nx70zs6wefn7ynq2t",
|
||||
"slug": "self-improving-agent",
|
||||
"version": "4.0.2",
|
||||
"publishedAt": 1785999527780
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# Errors Log
|
||||
|
||||
Command failures, exceptions, and unexpected behaviors.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
# Feature Requests
|
||||
|
||||
Capabilities requested by user that don't currently exist.
|
||||
|
||||
---
|
||||
@@ -0,0 +1,45 @@
|
||||
# Learnings
|
||||
|
||||
Corrections, insights, and knowledge gaps captured during development.
|
||||
|
||||
**Categories**: correction | insight | knowledge_gap | best_practice
|
||||
**Areas**: frontend | backend | infra | tests | docs | config
|
||||
**Statuses**: pending | in_progress | resolved | wont_fix | promoted | promoted_to_skill
|
||||
|
||||
## Status Definitions
|
||||
|
||||
| Status | Meaning |
|
||||
|--------|---------|
|
||||
| `pending` | Not yet addressed |
|
||||
| `in_progress` | Actively being worked on |
|
||||
| `resolved` | Issue fixed or knowledge integrated |
|
||||
| `wont_fix` | Decided not to address (reason in Resolution) |
|
||||
| `promoted` | Elevated to SOUL.md, TOOLS.md, or AGENTS.md |
|
||||
| `promoted_to_skill` | Extracted as a reusable skill |
|
||||
|
||||
## Skill Extraction Fields
|
||||
|
||||
When a learning is promoted to a skill, add these fields:
|
||||
|
||||
```markdown
|
||||
**Status**: promoted_to_skill
|
||||
**Skill-Path**: skills/skill-name
|
||||
```
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
## [LRN-20250115-001] best_practice
|
||||
|
||||
**Logged**: 2025-01-15T10:00:00Z
|
||||
**Priority**: high
|
||||
**Status**: promoted_to_skill
|
||||
**Skill-Path**: skills/docker-m1-fixes
|
||||
**Area**: infra
|
||||
|
||||
### Summary
|
||||
Docker build fails on Apple Silicon due to platform mismatch
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# Skill Template
|
||||
|
||||
Template for creating skills extracted from learnings. Copy and customize.
|
||||
|
||||
---
|
||||
|
||||
## SKILL.md Template
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name-here
|
||||
description: "Concise description of when and why to use this skill. Include trigger conditions."
|
||||
---
|
||||
|
||||
# Skill Name
|
||||
|
||||
Brief introduction explaining the problem this skill solves and its origin.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| [Trigger 1] | [Action 1] |
|
||||
| [Trigger 2] | [Action 2] |
|
||||
|
||||
## Background
|
||||
|
||||
Why this knowledge matters. What problems it prevents. Context from the original learning.
|
||||
|
||||
## Solution
|
||||
|
||||
### Step-by-Step
|
||||
|
||||
1. First step with code or command
|
||||
2. Second step
|
||||
3. Verification step
|
||||
|
||||
### Code Example
|
||||
|
||||
\`\`\`language
|
||||
// Example code demonstrating the solution
|
||||
\`\`\`
|
||||
|
||||
## Common Variations
|
||||
|
||||
- **Variation A**: Description and how to handle
|
||||
- **Variation B**: Description and how to handle
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Warning or common mistake #1
|
||||
- Warning or common mistake #2
|
||||
|
||||
## Related
|
||||
|
||||
- Link to related documentation
|
||||
- Link to related skill
|
||||
|
||||
## Source
|
||||
|
||||
Extracted from learning entry.
|
||||
- **Learning ID**: LRN-YYYYMMDD-XXX
|
||||
- **Original Category**: correction | insight | knowledge_gap | best_practice
|
||||
- **Extraction Date**: YYYY-MM-DD
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Minimal Template
|
||||
|
||||
For simple skills that don't need all sections:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name-here
|
||||
description: "What this skill does and when to use it."
|
||||
---
|
||||
|
||||
# Skill Name
|
||||
|
||||
[Problem statement in one sentence]
|
||||
|
||||
## Solution
|
||||
|
||||
[Direct solution with code/commands]
|
||||
|
||||
## Source
|
||||
|
||||
- Learning ID: LRN-YYYYMMDD-XXX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template with Scripts
|
||||
|
||||
For skills that include executable helpers:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: skill-name-here
|
||||
description: "What this skill does and when to use it."
|
||||
---
|
||||
|
||||
# Skill Name
|
||||
|
||||
[Introduction]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `./scripts/helper.sh` | [What it does] |
|
||||
| `./scripts/validate.sh` | [What it does] |
|
||||
|
||||
## Usage
|
||||
|
||||
### Automated (Recommended)
|
||||
|
||||
\`\`\`bash
|
||||
./skills/skill-name/scripts/helper.sh [args]
|
||||
\`\`\`
|
||||
|
||||
### Manual Steps
|
||||
|
||||
1. Step one
|
||||
2. Step two
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `scripts/helper.sh` | Main utility |
|
||||
| `scripts/validate.sh` | Validation checker |
|
||||
|
||||
## Source
|
||||
|
||||
- Learning ID: LRN-YYYYMMDD-XXX
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- **Skill name**: lowercase, hyphens for spaces
|
||||
- Good: `docker-m1-fixes`, `api-timeout-patterns`
|
||||
- Bad: `Docker_M1_Fixes`, `APITimeoutPatterns`
|
||||
|
||||
- **Description**: Start with action verb, mention trigger
|
||||
- Good: "Handles Docker build failures on Apple Silicon. Use when builds fail with platform mismatch."
|
||||
- Bad: "Docker stuff"
|
||||
|
||||
- **Files**:
|
||||
- `SKILL.md` - Required, main documentation
|
||||
- `scripts/` - Optional, executable code
|
||||
- `references/` - Optional, detailed docs
|
||||
- `assets/` - Optional, templates
|
||||
|
||||
---
|
||||
|
||||
## Extraction Checklist
|
||||
|
||||
Before creating a skill from a learning:
|
||||
|
||||
- [ ] Learning is verified (status: resolved)
|
||||
- [ ] Solution is broadly applicable (not one-off)
|
||||
- [ ] Content is complete (has all needed context)
|
||||
- [ ] Name follows conventions
|
||||
- [ ] Description is concise but informative
|
||||
- [ ] Quick Reference table is actionable
|
||||
- [ ] Code examples are tested
|
||||
- [ ] Source learning ID is recorded
|
||||
|
||||
After creating:
|
||||
|
||||
- [ ] Update original learning with `promoted_to_skill` status
|
||||
- [ ] Add `Skill-Path: skills/skill-name` to learning metadata
|
||||
- [ ] Test skill by reading it in a fresh session
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
name: self-improvement
|
||||
description: "Injects self-improvement reminder at bootstrap and sweeps ended sessions for errors"
|
||||
metadata: {"openclaw":{"emoji":"🧠","events":["agent:bootstrap","command:new","command:reset"]}}
|
||||
---
|
||||
|
||||
# Self-Improvement Hook
|
||||
|
||||
Injects a reminder to evaluate learnings during agent bootstrap, and detects
|
||||
errors from ended sessions.
|
||||
|
||||
OpenClaw has no per-tool-call hook event, so errors cannot be detected in
|
||||
real time after each command. This hook detects them with a session-end
|
||||
error sweep instead.
|
||||
|
||||
## What It Does
|
||||
|
||||
**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
|
||||
- If auto-detected errors are awaiting triage, includes a pending-triage note
|
||||
|
||||
**On `command:new` / `command:reset`** (session end):
|
||||
|
||||
- Locates the transcript of the session that just ended
|
||||
(`context.previousSessionEntry.sessionFile`, falling back to
|
||||
`<workspace>/sessions/<sessionId>.jsonl`)
|
||||
- Scans it against a fixed error-pattern list
|
||||
(`Error:`, `command not found`, `Traceback`, `npm ERR!`, …)
|
||||
- Appends a `pending` entry to `<workspace>/.learnings/ERRORS.md` with short,
|
||||
truncated, redacted excerpts (max 5 per sweep) for the next session to triage
|
||||
- Stamps each entry with deterministic `Pattern-Key` values derived from the
|
||||
matched pattern (e.g. `deps.module-not-found`, `shell.command-not-found`),
|
||||
so auto-detected errors can be deduplicated and recurrence-counted by key
|
||||
(see the Pattern-Key Taxonomy in `SKILL.md`)
|
||||
|
||||
## Opt-In and Safety
|
||||
|
||||
- The sweep only runs when `<workspace>/.learnings/` exists — create that
|
||||
directory to enable it, delete it to disable it
|
||||
- `ERRORS.md` is created only if missing and is otherwise appended to, never
|
||||
overwritten
|
||||
- Excerpts are truncated to 200 characters and common secret shapes (bearer
|
||||
tokens, API keys, GitHub/Slack/AWS tokens, JWTs, long opaque blobs) are
|
||||
redacted before writing; excerpts already present in `ERRORS.md` are skipped
|
||||
- Hook failures are swallowed so the gateway is never affected; set
|
||||
`SELF_IMPROVEMENT_HOOK_DEBUG=1` to log failures
|
||||
|
||||
## Configuration
|
||||
|
||||
No configuration needed. Enable with:
|
||||
|
||||
```bash
|
||||
openclaw hooks enable self-improvement
|
||||
```
|
||||
|
||||
Enable the error sweep by creating the learnings directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
node --test hooks/openclaw/handler.test.js
|
||||
```
|
||||
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* Self-Improvement Hook for OpenClaw
|
||||
*
|
||||
* OpenClaw has no per-tool-call hook event, so errors cannot be detected in
|
||||
* real time after each command. This hook detects them at session end
|
||||
* instead:
|
||||
*
|
||||
* - agent:bootstrap Injects the self-improvement reminder before
|
||||
* workspace files are injected, including a note
|
||||
* when auto-detected errors are awaiting triage.
|
||||
* - command:new / :reset Session-end sweep: scans the transcript of the
|
||||
* session that just ended for error patterns and
|
||||
* appends a pending entry to
|
||||
* <workspace>/.learnings/ERRORS.md.
|
||||
*
|
||||
* The sweep is opt-in: it only runs when <workspace>/.learnings/ exists.
|
||||
* Excerpts are truncated and redacted before being written.
|
||||
*/
|
||||
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
|
||||
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
|
||||
const REMINDER_PATH = REMINDER_NAME;
|
||||
const REMINDER_HEADER = '## Self-Improvement Reminder';
|
||||
|
||||
const REMINDER_CONTENT = `
|
||||
${REMINDER_HEADER}
|
||||
|
||||
After completing tasks, evaluate whether any learnings should be captured.
|
||||
|
||||
Only log if this repo or workspace is using the self-improvement skill.
|
||||
|
||||
Before logging:
|
||||
- Create only missing \`.learnings/\` files; never overwrite existing content
|
||||
- Do not log secrets, tokens, private keys, environment variables, or raw transcripts
|
||||
- Prefer short summaries or redacted excerpts over full command output
|
||||
|
||||
**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, and what to do differently.
|
||||
`.trim();
|
||||
|
||||
// Error-detection patterns. Ordered specific → generic: the first matching pattern
|
||||
// supplies the Pattern-Key stamped on swept entries, which is what makes
|
||||
// auto-detected errors dedup-able and recurrence-countable (see the
|
||||
// "Pattern-Key Taxonomy" section in SKILL.md).
|
||||
const ERROR_PATTERN_KEYS = [
|
||||
['command not found', 'shell.command-not-found'],
|
||||
['No such file', 'fs.no-such-file'],
|
||||
['Permission denied', 'fs.permission-denied'],
|
||||
['ModuleNotFoundError', 'deps.module-not-found'],
|
||||
['npm ERR!', 'deps.npm-error'],
|
||||
['Traceback', 'runtime.python-exception'],
|
||||
['SyntaxError', 'runtime.syntax-error'],
|
||||
['TypeError', 'runtime.type-error'],
|
||||
['Exception', 'runtime.exception'],
|
||||
['fatal:', 'vcs.fatal-error'],
|
||||
['exit code', 'shell.nonzero-exit'],
|
||||
['non-zero', 'shell.nonzero-exit'],
|
||||
['error:', 'runtime.error'],
|
||||
['Error:', 'runtime.error'],
|
||||
['ERROR:', 'runtime.error'],
|
||||
['failed', 'runtime.failure'],
|
||||
['FAILED', 'runtime.failure'],
|
||||
];
|
||||
|
||||
const SWEEP_SOURCE = 'openclaw-error-sweep';
|
||||
const MAX_EXCERPTS = 5;
|
||||
const MAX_EXCERPT_LENGTH = 200;
|
||||
const ERRORS_FILE_HEADER = '# Errors\n\nCommand failures and integration errors.\n\n---\n';
|
||||
|
||||
// Best-effort redaction of common secret shapes before anything is written.
|
||||
const REDACTION_RULES = [
|
||||
[/\b(api[_-]?key|token|secret|password|passwd|authorization|credential)s?\b(\s*[=:]\s*)\S+/gi, '$1$2[REDACTED]'],
|
||||
[/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]'],
|
||||
[/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '[REDACTED]'],
|
||||
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, '[REDACTED]'],
|
||||
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED]'],
|
||||
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g, '[REDACTED-JWT]'],
|
||||
[/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED-BLOB]'],
|
||||
];
|
||||
|
||||
function isObject(value) {
|
||||
return !!value && typeof value === 'object';
|
||||
}
|
||||
|
||||
function isInjectedReminderFile(value) {
|
||||
if (!isObject(value) || value.path !== REMINDER_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
value.virtual === true ||
|
||||
(typeof value.content === 'string' && value.content.includes(REMINDER_HEADER))
|
||||
);
|
||||
}
|
||||
|
||||
function redactSensitiveText(text) {
|
||||
let result = text;
|
||||
for (const [pattern, replacement] of REDACTION_RULES) {
|
||||
result = result.replace(pattern, replacement);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitizeExcerptLine(line) {
|
||||
let excerpt = redactSensitiveText(line.trim()).split('```').join("'''");
|
||||
if (excerpt.length > MAX_EXCERPT_LENGTH) {
|
||||
excerpt = `${excerpt.slice(0, MAX_EXCERPT_LENGTH)}…`;
|
||||
}
|
||||
return excerpt;
|
||||
}
|
||||
|
||||
function collectTextFragments(value, out, depth = 0) {
|
||||
if (depth > 4 || out.length > 200) {
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
out.push(value);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
collectTextFragments(item, out, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
if (typeof value.text === 'string') {
|
||||
out.push(value.text);
|
||||
}
|
||||
if ('content' in value) {
|
||||
collectTextFragments(value.content, out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matchErrorPatternKey(line) {
|
||||
for (const [pattern, patternKey] of ERROR_PATTERN_KEYS) {
|
||||
if (line.includes(pattern)) {
|
||||
return patternKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function scanTranscriptForErrors(sessionFilePath) {
|
||||
let raw;
|
||||
try {
|
||||
raw = await fs.readFile(sessionFilePath, 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const excerpts = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const jsonLine of raw.split('\n')) {
|
||||
if (excerpts.length >= MAX_EXCERPTS) {
|
||||
break;
|
||||
}
|
||||
const trimmed = jsonLine.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry;
|
||||
try {
|
||||
entry = JSON.parse(trimmed);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isObject(entry) || !isObject(entry.message)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fragments = [];
|
||||
collectTextFragments(entry.message.content, fragments);
|
||||
|
||||
for (const fragment of fragments) {
|
||||
for (const line of fragment.split('\n')) {
|
||||
const patternKey = matchErrorPatternKey(line);
|
||||
if (!patternKey) {
|
||||
continue;
|
||||
}
|
||||
const excerpt = sanitizeExcerptLine(line);
|
||||
if (!excerpt || seen.has(excerpt)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(excerpt);
|
||||
excerpts.push({ excerpt, patternKey });
|
||||
if (excerpts.length >= MAX_EXCERPTS) {
|
||||
return excerpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return excerpts;
|
||||
}
|
||||
|
||||
function resolveSessionFilePath(context, workspaceDir) {
|
||||
const sessionEntry = isObject(context.previousSessionEntry)
|
||||
? context.previousSessionEntry
|
||||
: isObject(context.sessionEntry)
|
||||
? context.sessionEntry
|
||||
: {};
|
||||
|
||||
if (typeof sessionEntry.sessionFile === 'string' && sessionEntry.sessionFile.trim()) {
|
||||
return sessionEntry.sessionFile;
|
||||
}
|
||||
|
||||
const sessionId =
|
||||
typeof sessionEntry.sessionId === 'string' ? sessionEntry.sessionId.trim() : '';
|
||||
if (sessionId && workspaceDir) {
|
||||
return path.join(workspaceDir, 'sessions', `${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function generateEntryId(timestamp) {
|
||||
const yyyymmdd = timestamp.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const suffix = Math.random().toString(36).slice(2, 5).toUpperCase().padEnd(3, '0');
|
||||
return `ERR-${yyyymmdd}-${suffix}`;
|
||||
}
|
||||
|
||||
function formatErrorEntry(params) {
|
||||
const { excerpts, sessionKey, sessionFilePath, action, timestamp } = params;
|
||||
const plural = excerpts.length === 1 ? '' : 's';
|
||||
const patternKeys = [...new Set(excerpts.map((item) => item.patternKey))];
|
||||
|
||||
return [
|
||||
`## [${generateEntryId(timestamp)}] openclaw_session_sweep`,
|
||||
'',
|
||||
`**Logged**: ${timestamp.toISOString()}`,
|
||||
'**Priority**: medium',
|
||||
'**Status**: pending',
|
||||
'**Area**: config',
|
||||
'',
|
||||
'### Summary',
|
||||
`Session-end sweep detected ${excerpts.length} possible error${plural} in the previous OpenClaw session.`,
|
||||
'',
|
||||
'### Error',
|
||||
'```',
|
||||
...excerpts.map((item) => item.excerpt),
|
||||
'```',
|
||||
'',
|
||||
'### Context',
|
||||
`- Detected by the self-improvement hook on \`/${action}\` (OpenClaw has no per-tool-call hook, so errors are swept from the session transcript at session end)`,
|
||||
`- Session key: ${sessionKey || 'unknown'}`,
|
||||
`- Session transcript: ${sessionFilePath}`,
|
||||
'- Excerpts are truncated and redacted; check the transcript for full context',
|
||||
'',
|
||||
'### Suggested Fix',
|
||||
'Triage this entry: if the error was real and non-obvious, keep it and fill in the fix; otherwise mark it resolved or delete it. Before keeping it, grep for its Pattern-Key(s) and fold recurrences into the existing entry (bump Recurrence-Count) instead of duplicating.',
|
||||
'',
|
||||
'### Metadata',
|
||||
`- Source: ${SWEEP_SOURCE}`,
|
||||
'- Reproducible: unknown',
|
||||
...patternKeys.map((patternKey) => `- Pattern-Key: ${patternKey}`),
|
||||
'',
|
||||
'---',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function handleSessionEndSweep(event) {
|
||||
const context = event.context;
|
||||
const workspaceDir =
|
||||
typeof context.workspaceDir === 'string' && context.workspaceDir.trim()
|
||||
? context.workspaceDir
|
||||
: undefined;
|
||||
if (!workspaceDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Opt-in gate: only sweep when the workspace uses the self-improvement skill.
|
||||
const learningsDir = path.join(workspaceDir, '.learnings');
|
||||
try {
|
||||
const stats = await fs.stat(learningsDir);
|
||||
if (!stats.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionFilePath = resolveSessionFilePath(context, workspaceDir);
|
||||
if (!sessionFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const excerpts = await scanTranscriptForErrors(sessionFilePath);
|
||||
if (excerpts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorsFilePath = path.join(learningsDir, 'ERRORS.md');
|
||||
let existing = '';
|
||||
try {
|
||||
existing = await fs.readFile(errorsFilePath, 'utf-8');
|
||||
} catch {
|
||||
// Missing file is fine; it is created below.
|
||||
}
|
||||
|
||||
const freshExcerpts = excerpts.filter((item) => !existing.includes(item.excerpt));
|
||||
if (freshExcerpts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = formatErrorEntry({
|
||||
excerpts: freshExcerpts,
|
||||
sessionKey: typeof event.sessionKey === 'string' ? event.sessionKey : '',
|
||||
sessionFilePath,
|
||||
action: event.action,
|
||||
timestamp: event.timestamp instanceof Date ? event.timestamp : new Date(),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
try {
|
||||
await fs.writeFile(errorsFilePath, `${ERRORS_FILE_HEADER}\n${entry}\n`, { flag: 'wx' });
|
||||
return;
|
||||
} catch (err) {
|
||||
if (!isObject(err) || err.code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.appendFile(errorsFilePath, `\n${entry}\n`);
|
||||
}
|
||||
|
||||
async function countPendingSweepEntries(workspaceDir) {
|
||||
if (!workspaceDir) {
|
||||
return 0;
|
||||
}
|
||||
let content;
|
||||
try {
|
||||
content = await fs.readFile(path.join(workspaceDir, '.learnings', 'ERRORS.md'), 'utf-8');
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return content
|
||||
.split(/^## /m)
|
||||
.slice(1)
|
||||
.filter(
|
||||
(section) =>
|
||||
section.includes(`Source: ${SWEEP_SOURCE}`) && section.includes('**Status**: pending'),
|
||||
).length;
|
||||
}
|
||||
|
||||
async function handleBootstrap(event) {
|
||||
// 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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const occupiedByOtherFile = event.context.bootstrapFiles.some(
|
||||
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
|
||||
);
|
||||
if (occupiedByOtherFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let reminderContent = REMINDER_CONTENT;
|
||||
const workspaceDir =
|
||||
typeof event.context.workspaceDir === 'string' && event.context.workspaceDir.trim()
|
||||
? event.context.workspaceDir
|
||||
: undefined;
|
||||
const pendingSweepCount = await countPendingSweepEntries(workspaceDir);
|
||||
if (pendingSweepCount > 0) {
|
||||
const plural = pendingSweepCount === 1 ? 'y' : 'ies';
|
||||
reminderContent +=
|
||||
`\n\n**Pending triage:** ${pendingSweepCount} auto-detected error entr${plural} ` +
|
||||
`(Source: ${SWEEP_SOURCE}) in \`.learnings/ERRORS.md\` await review. ` +
|
||||
'Confirm, resolve, or delete them when convenient.';
|
||||
}
|
||||
|
||||
const cleanedBootstrapFiles = event.context.bootstrapFiles.filter(
|
||||
(file, index, files) =>
|
||||
!isInjectedReminderFile(file) ||
|
||||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
|
||||
);
|
||||
|
||||
const reminderFile = {
|
||||
name: REMINDER_NAME,
|
||||
path: REMINDER_PATH,
|
||||
content: reminderContent,
|
||||
missing: false,
|
||||
virtual: true,
|
||||
};
|
||||
|
||||
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
|
||||
if (existingIndex === -1) {
|
||||
cleanedBootstrapFiles.push(reminderFile);
|
||||
} else {
|
||||
cleanedBootstrapFiles[existingIndex] = reminderFile;
|
||||
}
|
||||
|
||||
event.context.bootstrapFiles = cleanedBootstrapFiles;
|
||||
}
|
||||
|
||||
const handler = async (event) => {
|
||||
// Safety checks for event structure
|
||||
if (!event || typeof event !== 'object') {
|
||||
return;
|
||||
}
|
||||
if (!event.context || typeof event.context !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (event.type === 'agent' && event.action === 'bootstrap') {
|
||||
await handleBootstrap(event);
|
||||
return;
|
||||
}
|
||||
if (event.type === 'command' && (event.action === 'new' || event.action === 'reset')) {
|
||||
await handleSessionEndSweep(event);
|
||||
}
|
||||
} catch (err) {
|
||||
// Never break the gateway on hook failure.
|
||||
if (process.env.SELF_IMPROVEMENT_HOOK_DEBUG) {
|
||||
console.error('[self-improvement] hook failed:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = handler;
|
||||
module.exports.default = handler;
|
||||
@@ -0,0 +1,229 @@
|
||||
/**
|
||||
* Tests for the OpenClaw self-improvement hook.
|
||||
*
|
||||
* Run with: node --test hooks/openclaw/
|
||||
* (no dependencies; uses the built-in node:test runner)
|
||||
*/
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test, beforeEach, afterEach } = require('node:test');
|
||||
|
||||
const handler = require('./handler.js');
|
||||
|
||||
let workspaceDir;
|
||||
|
||||
beforeEach(async () => {
|
||||
workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'self-improvement-hook-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(workspaceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function makeBootstrapEvent(overrides = {}) {
|
||||
return {
|
||||
type: 'agent',
|
||||
action: 'bootstrap',
|
||||
sessionKey: 'agent:main:whatsapp',
|
||||
timestamp: new Date('2026-07-04T12:00:00Z'),
|
||||
messages: [],
|
||||
context: { workspaceDir, bootstrapFiles: [] },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommandEvent(action, sessionFile) {
|
||||
return {
|
||||
type: 'command',
|
||||
action,
|
||||
sessionKey: 'agent:main:whatsapp',
|
||||
timestamp: new Date('2026-07-04T12:00:00Z'),
|
||||
messages: [],
|
||||
context: {
|
||||
workspaceDir,
|
||||
commandSource: 'whatsapp',
|
||||
previousSessionEntry: { sessionId: 'abc123', sessionFile },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeTranscript(lines) {
|
||||
const sessionsDir = path.join(workspaceDir, 'sessions');
|
||||
await fs.mkdir(sessionsDir, { recursive: true });
|
||||
const sessionFile = path.join(sessionsDir, 'abc123.jsonl');
|
||||
await fs.writeFile(sessionFile, lines.map((line) => JSON.stringify(line)).join('\n'));
|
||||
return sessionFile;
|
||||
}
|
||||
|
||||
function toolResultLine(text) {
|
||||
return {
|
||||
type: 'message',
|
||||
message: { role: 'toolResult', content: [{ type: 'text', text }] },
|
||||
};
|
||||
}
|
||||
|
||||
const errorsFile = () => path.join(workspaceDir, '.learnings', 'ERRORS.md');
|
||||
|
||||
test('bootstrap injects the reminder as a virtual file', async () => {
|
||||
const event = makeBootstrapEvent();
|
||||
await handler(event);
|
||||
|
||||
assert.equal(event.context.bootstrapFiles.length, 1);
|
||||
const injected = event.context.bootstrapFiles[0];
|
||||
assert.equal(injected.path, 'SELF_IMPROVEMENT_REMINDER.md');
|
||||
assert.equal(injected.virtual, true);
|
||||
assert.match(injected.content, /## Self-Improvement Reminder/);
|
||||
assert.doesNotMatch(injected.content, /Pending triage/);
|
||||
});
|
||||
|
||||
test('bootstrap skips sub-agent sessions', async () => {
|
||||
const event = makeBootstrapEvent({ sessionKey: 'agent:main:subagent:xyz' });
|
||||
await handler(event);
|
||||
assert.equal(event.context.bootstrapFiles.length, 0);
|
||||
});
|
||||
|
||||
test('bootstrap deduplicates a previously injected reminder', async () => {
|
||||
const event = makeBootstrapEvent();
|
||||
event.context.bootstrapFiles.push({
|
||||
name: 'SELF_IMPROVEMENT_REMINDER.md',
|
||||
path: 'SELF_IMPROVEMENT_REMINDER.md',
|
||||
content: '## Self-Improvement Reminder\n\nstale copy',
|
||||
virtual: true,
|
||||
});
|
||||
await handler(event);
|
||||
assert.equal(event.context.bootstrapFiles.length, 1);
|
||||
});
|
||||
|
||||
test('session-end sweep appends detected errors to ERRORS.md', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const sessionFile = await writeTranscript([
|
||||
{ type: 'message', message: { role: 'user', content: 'run the build' } },
|
||||
toolResultLine('npm ERR! missing script: build\nbash: tsc: command not found'),
|
||||
{ type: 'message', message: { role: 'assistant', content: 'Build is broken.' } },
|
||||
]);
|
||||
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
assert.match(content, /^# Errors/);
|
||||
assert.match(content, /## \[ERR-20260704-[A-Z0-9]{3}\] openclaw_session_sweep/);
|
||||
assert.match(content, /npm ERR! missing script: build/);
|
||||
assert.match(content, /command not found/);
|
||||
assert.match(content, /Source: openclaw-error-sweep/);
|
||||
assert.match(content, /\*\*Status\*\*: pending/);
|
||||
assert.match(content, /- Pattern-Key: deps\.npm-error/);
|
||||
assert.match(content, /- Pattern-Key: shell\.command-not-found/);
|
||||
});
|
||||
|
||||
test('sweep stamps the most specific Pattern-Key and dedupes keys', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const sessionFile = await writeTranscript([
|
||||
// 'ModuleNotFoundError' must win over the generic 'Error:'/'Traceback' buckets
|
||||
toolResultLine("ModuleNotFoundError: No module named 'requests'"),
|
||||
toolResultLine('TypeError: cannot read properties of undefined (first)'),
|
||||
toolResultLine('TypeError: cannot read properties of undefined (second)'),
|
||||
]);
|
||||
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
assert.match(content, /- Pattern-Key: deps\.module-not-found/);
|
||||
const typeErrorKeys = content.split('- Pattern-Key: runtime.type-error').length - 1;
|
||||
assert.equal(typeErrorKeys, 1);
|
||||
assert.doesNotMatch(content, /- Pattern-Key: runtime\.error/);
|
||||
});
|
||||
|
||||
test('sweep does nothing when .learnings/ does not exist (opt-in gate)', async () => {
|
||||
const sessionFile = await writeTranscript([toolResultLine('fatal: not a git repository')]);
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
await assert.rejects(fs.access(errorsFile()));
|
||||
});
|
||||
|
||||
test('sweep does nothing when the transcript has no errors', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const sessionFile = await writeTranscript([
|
||||
{ type: 'message', message: { role: 'assistant', content: 'All good!' } },
|
||||
]);
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
await assert.rejects(fs.access(errorsFile()));
|
||||
});
|
||||
|
||||
test('sweep never overwrites an existing ERRORS.md', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
await fs.writeFile(errorsFile(), '# Errors\n\nExisting notes.\n\n---\n');
|
||||
const sessionFile = await writeTranscript([toolResultLine('Error: connection refused')]);
|
||||
|
||||
await handler(makeCommandEvent('reset', sessionFile));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
assert.match(content, /Existing notes\./);
|
||||
assert.match(content, /Error: connection refused/);
|
||||
});
|
||||
|
||||
test('sweep is idempotent for already-logged excerpts', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const sessionFile = await writeTranscript([toolResultLine('Error: connection refused')]);
|
||||
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
const occurrences = content.split('Error: connection refused').length - 1;
|
||||
assert.equal(occurrences, 1);
|
||||
});
|
||||
|
||||
test('sweep redacts secrets and truncates long lines', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const longTail = 'x'.repeat(300);
|
||||
// Fixture secret is assembled at runtime so the literal never appears in
|
||||
// this file and secret scanners don't flag it as an exposed credential.
|
||||
const fakeKey = ['sk', 'live', '1234567890'].join('-');
|
||||
const sessionFile = await writeTranscript([
|
||||
toolResultLine(
|
||||
`Error: request failed with api_key=${fakeKey} Bearer abc.def.ghi token: hunter2 ${longTail}`,
|
||||
),
|
||||
]);
|
||||
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
assert.doesNotMatch(content, new RegExp(fakeKey));
|
||||
assert.doesNotMatch(content, /hunter2/);
|
||||
assert.match(content, /\[REDACTED\]/);
|
||||
assert.doesNotMatch(content, /x{250}/);
|
||||
});
|
||||
|
||||
test('sweep falls back to <workspace>/sessions/<sessionId>.jsonl', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
await writeTranscript([toolResultLine('Traceback (most recent call last):')]);
|
||||
|
||||
await handler(makeCommandEvent('new', undefined));
|
||||
|
||||
const content = await fs.readFile(errorsFile(), 'utf-8');
|
||||
assert.match(content, /Traceback/);
|
||||
});
|
||||
|
||||
test('bootstrap surfaces pending sweep entries for triage', async () => {
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
const sessionFile = await writeTranscript([toolResultLine('Permission denied (publickey)')]);
|
||||
await handler(makeCommandEvent('new', sessionFile));
|
||||
|
||||
const event = makeBootstrapEvent();
|
||||
await handler(event);
|
||||
|
||||
const injected = event.context.bootstrapFiles[0];
|
||||
assert.match(injected.content, /\*\*Pending triage:\*\* 1 auto-detected error entry/);
|
||||
});
|
||||
|
||||
test('handler ignores unrelated events and malformed input', async () => {
|
||||
await handler(null);
|
||||
await handler({ type: 'gateway', action: 'startup', context: {} });
|
||||
await handler({ type: 'command', action: 'stop', context: { workspaceDir } });
|
||||
// A sweep with a missing transcript must not throw.
|
||||
await fs.mkdir(path.join(workspaceDir, '.learnings'), { recursive: true });
|
||||
await handler(makeCommandEvent('new', path.join(workspaceDir, 'sessions', 'missing.jsonl')));
|
||||
await assert.rejects(fs.access(errorsFile()));
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* Self-Improvement Hook for OpenClaw
|
||||
*
|
||||
* OpenClaw has no per-tool-call hook event, so errors cannot be detected in
|
||||
* real time after each command. This hook detects them at session end
|
||||
* instead:
|
||||
*
|
||||
* - agent:bootstrap Injects the self-improvement reminder before
|
||||
* workspace files are injected, including a note
|
||||
* when auto-detected errors are awaiting triage.
|
||||
* - command:new / :reset Session-end sweep: scans the transcript of the
|
||||
* session that just ended for error patterns and
|
||||
* appends a pending entry to
|
||||
* <workspace>/.learnings/ERRORS.md.
|
||||
*
|
||||
* The sweep is opt-in: it only runs when <workspace>/.learnings/ exists.
|
||||
* Excerpts are truncated and redacted before being written.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import type { HookHandler } from 'openclaw/hooks';
|
||||
|
||||
const REMINDER_NAME = 'SELF_IMPROVEMENT_REMINDER.md';
|
||||
const REMINDER_PATH = REMINDER_NAME;
|
||||
const REMINDER_HEADER = '## Self-Improvement Reminder';
|
||||
|
||||
const REMINDER_CONTENT = `${REMINDER_HEADER}
|
||||
|
||||
After completing tasks, evaluate whether any learnings should be captured.
|
||||
|
||||
Only log if this repo or workspace is using the self-improvement skill.
|
||||
|
||||
Before logging:
|
||||
- Create only missing \`.learnings/\` files; never overwrite existing content
|
||||
- Do not log secrets, tokens, private keys, environment variables, or raw transcripts
|
||||
- Prefer short summaries or redacted excerpts over full command output
|
||||
|
||||
**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, and what to do differently.`;
|
||||
|
||||
// Error-detection patterns. Ordered specific → generic: the first matching pattern
|
||||
// supplies the Pattern-Key stamped on swept entries, which is what makes
|
||||
// auto-detected errors dedup-able and recurrence-countable (see the
|
||||
// "Pattern-Key Taxonomy" section in SKILL.md).
|
||||
const ERROR_PATTERN_KEYS: Array<[string, string]> = [
|
||||
['command not found', 'shell.command-not-found'],
|
||||
['No such file', 'fs.no-such-file'],
|
||||
['Permission denied', 'fs.permission-denied'],
|
||||
['ModuleNotFoundError', 'deps.module-not-found'],
|
||||
['npm ERR!', 'deps.npm-error'],
|
||||
['Traceback', 'runtime.python-exception'],
|
||||
['SyntaxError', 'runtime.syntax-error'],
|
||||
['TypeError', 'runtime.type-error'],
|
||||
['Exception', 'runtime.exception'],
|
||||
['fatal:', 'vcs.fatal-error'],
|
||||
['exit code', 'shell.nonzero-exit'],
|
||||
['non-zero', 'shell.nonzero-exit'],
|
||||
['error:', 'runtime.error'],
|
||||
['Error:', 'runtime.error'],
|
||||
['ERROR:', 'runtime.error'],
|
||||
['failed', 'runtime.failure'],
|
||||
['FAILED', 'runtime.failure'],
|
||||
];
|
||||
|
||||
interface SweepExcerpt {
|
||||
excerpt: string;
|
||||
patternKey: string;
|
||||
}
|
||||
|
||||
const SWEEP_SOURCE = 'openclaw-error-sweep';
|
||||
const MAX_EXCERPTS = 5;
|
||||
const MAX_EXCERPT_LENGTH = 200;
|
||||
const ERRORS_FILE_HEADER = '# Errors\n\nCommand failures and integration errors.\n\n---\n';
|
||||
|
||||
// Best-effort redaction of common secret shapes before anything is written.
|
||||
const REDACTION_RULES: Array<[RegExp, string]> = [
|
||||
[/\b(api[_-]?key|token|secret|password|passwd|authorization|credential)s?\b(\s*[=:]\s*)\S+/gi, '$1$2[REDACTED]'],
|
||||
[/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]'],
|
||||
[/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '[REDACTED]'],
|
||||
[/\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, '[REDACTED]'],
|
||||
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED]'],
|
||||
[/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}\b/g, '[REDACTED-JWT]'],
|
||||
[/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED-BLOB]'],
|
||||
];
|
||||
|
||||
type HookEvent = Parameters<HookHandler>[0];
|
||||
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === 'object';
|
||||
}
|
||||
|
||||
function isInjectedReminderFile(value: unknown): boolean {
|
||||
if (!isObject(value) || value.path !== REMINDER_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
value.virtual === true ||
|
||||
(typeof value.content === 'string' && value.content.includes(REMINDER_HEADER))
|
||||
);
|
||||
}
|
||||
|
||||
function redactSensitiveText(text: string): string {
|
||||
let result = text;
|
||||
for (const [pattern, replacement] of REDACTION_RULES) {
|
||||
result = result.replace(pattern, replacement);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function sanitizeExcerptLine(line: string): string {
|
||||
let excerpt = redactSensitiveText(line.trim()).split('```').join("'''");
|
||||
if (excerpt.length > MAX_EXCERPT_LENGTH) {
|
||||
excerpt = `${excerpt.slice(0, MAX_EXCERPT_LENGTH)}…`;
|
||||
}
|
||||
return excerpt;
|
||||
}
|
||||
|
||||
function collectTextFragments(value: unknown, out: string[], depth = 0): void {
|
||||
if (depth > 4 || out.length > 200) {
|
||||
return;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
out.push(value);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
collectTextFragments(item, out, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isObject(value)) {
|
||||
if (typeof value.text === 'string') {
|
||||
out.push(value.text);
|
||||
}
|
||||
if ('content' in value) {
|
||||
collectTextFragments(value.content, out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function matchErrorPatternKey(line: string): string | null {
|
||||
for (const [pattern, patternKey] of ERROR_PATTERN_KEYS) {
|
||||
if (line.includes(pattern)) {
|
||||
return patternKey;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function scanTranscriptForErrors(sessionFilePath: string): Promise<SweepExcerpt[]> {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await fs.readFile(sessionFilePath, 'utf-8');
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
const excerpts: SweepExcerpt[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const jsonLine of raw.split('\n')) {
|
||||
if (excerpts.length >= MAX_EXCERPTS) {
|
||||
break;
|
||||
}
|
||||
const trimmed = jsonLine.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry: unknown;
|
||||
try {
|
||||
entry = JSON.parse(trimmed);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (!isObject(entry) || !isObject(entry.message)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fragments: string[] = [];
|
||||
collectTextFragments(entry.message.content, fragments);
|
||||
|
||||
for (const fragment of fragments) {
|
||||
for (const line of fragment.split('\n')) {
|
||||
const patternKey = matchErrorPatternKey(line);
|
||||
if (!patternKey) {
|
||||
continue;
|
||||
}
|
||||
const excerpt = sanitizeExcerptLine(line);
|
||||
if (!excerpt || seen.has(excerpt)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(excerpt);
|
||||
excerpts.push({ excerpt, patternKey });
|
||||
if (excerpts.length >= MAX_EXCERPTS) {
|
||||
return excerpts;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return excerpts;
|
||||
}
|
||||
|
||||
function resolveSessionFilePath(
|
||||
context: Record<string, unknown>,
|
||||
workspaceDir: string | undefined,
|
||||
): string | undefined {
|
||||
const sessionEntry = isObject(context.previousSessionEntry)
|
||||
? context.previousSessionEntry
|
||||
: isObject(context.sessionEntry)
|
||||
? context.sessionEntry
|
||||
: {};
|
||||
|
||||
const sessionFile = (sessionEntry as Record<string, unknown>).sessionFile;
|
||||
if (typeof sessionFile === 'string' && sessionFile.trim()) {
|
||||
return sessionFile;
|
||||
}
|
||||
|
||||
const sessionIdValue = (sessionEntry as Record<string, unknown>).sessionId;
|
||||
const sessionId = typeof sessionIdValue === 'string' ? sessionIdValue.trim() : '';
|
||||
if (sessionId && workspaceDir) {
|
||||
return path.join(workspaceDir, 'sessions', `${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function generateEntryId(timestamp: Date): string {
|
||||
const yyyymmdd = timestamp.toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const suffix = Math.random().toString(36).slice(2, 5).toUpperCase().padEnd(3, '0');
|
||||
return `ERR-${yyyymmdd}-${suffix}`;
|
||||
}
|
||||
|
||||
function formatErrorEntry(params: {
|
||||
excerpts: SweepExcerpt[];
|
||||
sessionKey: string;
|
||||
sessionFilePath: string;
|
||||
action: string;
|
||||
timestamp: Date;
|
||||
}): string {
|
||||
const { excerpts, sessionKey, sessionFilePath, action, timestamp } = params;
|
||||
const plural = excerpts.length === 1 ? '' : 's';
|
||||
const patternKeys = [...new Set(excerpts.map((item) => item.patternKey))];
|
||||
|
||||
return [
|
||||
`## [${generateEntryId(timestamp)}] openclaw_session_sweep`,
|
||||
'',
|
||||
`**Logged**: ${timestamp.toISOString()}`,
|
||||
'**Priority**: medium',
|
||||
'**Status**: pending',
|
||||
'**Area**: config',
|
||||
'',
|
||||
'### Summary',
|
||||
`Session-end sweep detected ${excerpts.length} possible error${plural} in the previous OpenClaw session.`,
|
||||
'',
|
||||
'### Error',
|
||||
'```',
|
||||
...excerpts.map((item) => item.excerpt),
|
||||
'```',
|
||||
'',
|
||||
'### Context',
|
||||
`- Detected by the self-improvement hook on \`/${action}\` (OpenClaw has no per-tool-call hook, so errors are swept from the session transcript at session end)`,
|
||||
`- Session key: ${sessionKey || 'unknown'}`,
|
||||
`- Session transcript: ${sessionFilePath}`,
|
||||
'- Excerpts are truncated and redacted; check the transcript for full context',
|
||||
'',
|
||||
'### Suggested Fix',
|
||||
'Triage this entry: if the error was real and non-obvious, keep it and fill in the fix; otherwise mark it resolved or delete it. Before keeping it, grep for its Pattern-Key(s) and fold recurrences into the existing entry (bump Recurrence-Count) instead of duplicating.',
|
||||
'',
|
||||
'### Metadata',
|
||||
`- Source: ${SWEEP_SOURCE}`,
|
||||
'- Reproducible: unknown',
|
||||
...patternKeys.map((patternKey) => `- Pattern-Key: ${patternKey}`),
|
||||
'',
|
||||
'---',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function handleSessionEndSweep(event: HookEvent): Promise<void> {
|
||||
const context = event.context as Record<string, unknown>;
|
||||
const workspaceDir =
|
||||
typeof context.workspaceDir === 'string' && context.workspaceDir.trim()
|
||||
? context.workspaceDir
|
||||
: undefined;
|
||||
if (!workspaceDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Opt-in gate: only sweep when the workspace uses the self-improvement skill.
|
||||
const learningsDir = path.join(workspaceDir, '.learnings');
|
||||
try {
|
||||
const stats = await fs.stat(learningsDir);
|
||||
if (!stats.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionFilePath = resolveSessionFilePath(context, workspaceDir);
|
||||
if (!sessionFilePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const excerpts = await scanTranscriptForErrors(sessionFilePath);
|
||||
if (excerpts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorsFilePath = path.join(learningsDir, 'ERRORS.md');
|
||||
let existing = '';
|
||||
try {
|
||||
existing = await fs.readFile(errorsFilePath, 'utf-8');
|
||||
} catch {
|
||||
// Missing file is fine; it is created below.
|
||||
}
|
||||
|
||||
const freshExcerpts = excerpts.filter((item) => !existing.includes(item.excerpt));
|
||||
if (freshExcerpts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = formatErrorEntry({
|
||||
excerpts: freshExcerpts,
|
||||
sessionKey: typeof event.sessionKey === 'string' ? event.sessionKey : '',
|
||||
sessionFilePath,
|
||||
action: event.action,
|
||||
timestamp: event.timestamp instanceof Date ? event.timestamp : new Date(),
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
try {
|
||||
await fs.writeFile(errorsFilePath, `${ERRORS_FILE_HEADER}\n${entry}\n`, { flag: 'wx' });
|
||||
return;
|
||||
} catch (err) {
|
||||
if (!isObject(err) || (err as { code?: string }).code !== 'EEXIST') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.appendFile(errorsFilePath, `\n${entry}\n`);
|
||||
}
|
||||
|
||||
async function countPendingSweepEntries(workspaceDir: string | undefined): Promise<number> {
|
||||
if (!workspaceDir) {
|
||||
return 0;
|
||||
}
|
||||
let content: string;
|
||||
try {
|
||||
content = await fs.readFile(path.join(workspaceDir, '.learnings', 'ERRORS.md'), 'utf-8');
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return content
|
||||
.split(/^## /m)
|
||||
.slice(1)
|
||||
.filter(
|
||||
(section) =>
|
||||
section.includes(`Source: ${SWEEP_SOURCE}`) && section.includes('**Status**: pending'),
|
||||
).length;
|
||||
}
|
||||
|
||||
async function handleBootstrap(event: HookEvent): Promise<void> {
|
||||
// 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;
|
||||
}
|
||||
|
||||
const context = event.context as Record<string, unknown>;
|
||||
|
||||
// Inject the reminder as a virtual bootstrap file
|
||||
// Check that bootstrapFiles is an array before pushing
|
||||
if (!Array.isArray(context.bootstrapFiles)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const occupiedByOtherFile = context.bootstrapFiles.some(
|
||||
(file) => isObject(file) && file.path === REMINDER_PATH && !isInjectedReminderFile(file),
|
||||
);
|
||||
if (occupiedByOtherFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
let reminderContent = REMINDER_CONTENT;
|
||||
const workspaceDir =
|
||||
typeof context.workspaceDir === 'string' && context.workspaceDir.trim()
|
||||
? context.workspaceDir
|
||||
: undefined;
|
||||
const pendingSweepCount = await countPendingSweepEntries(workspaceDir);
|
||||
if (pendingSweepCount > 0) {
|
||||
const plural = pendingSweepCount === 1 ? 'y' : 'ies';
|
||||
reminderContent +=
|
||||
`\n\n**Pending triage:** ${pendingSweepCount} auto-detected error entr${plural} ` +
|
||||
`(Source: ${SWEEP_SOURCE}) in \`.learnings/ERRORS.md\` await review. ` +
|
||||
'Confirm, resolve, or delete them when convenient.';
|
||||
}
|
||||
|
||||
const cleanedBootstrapFiles = context.bootstrapFiles.filter(
|
||||
(file, index, files) =>
|
||||
!isInjectedReminderFile(file) ||
|
||||
files.findIndex((candidate) => isInjectedReminderFile(candidate)) === index,
|
||||
);
|
||||
|
||||
const reminderFile = {
|
||||
name: REMINDER_NAME,
|
||||
path: REMINDER_PATH,
|
||||
content: reminderContent,
|
||||
missing: false,
|
||||
virtual: true,
|
||||
};
|
||||
|
||||
const existingIndex = cleanedBootstrapFiles.findIndex((file) => isInjectedReminderFile(file));
|
||||
if (existingIndex === -1) {
|
||||
cleanedBootstrapFiles.push(reminderFile);
|
||||
} else {
|
||||
cleanedBootstrapFiles[existingIndex] = reminderFile;
|
||||
}
|
||||
|
||||
context.bootstrapFiles = cleanedBootstrapFiles;
|
||||
}
|
||||
|
||||
const handler: HookHandler = async (event) => {
|
||||
// Safety checks for event structure
|
||||
if (!event || typeof event !== 'object') {
|
||||
return;
|
||||
}
|
||||
if (!event.context || typeof event.context !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (event.type === 'agent' && event.action === 'bootstrap') {
|
||||
await handleBootstrap(event);
|
||||
return;
|
||||
}
|
||||
if (event.type === 'command' && (event.action === 'new' || event.action === 'reset')) {
|
||||
await handleSessionEndSweep(event);
|
||||
}
|
||||
} catch (err) {
|
||||
// Never break the gateway on hook failure.
|
||||
if (process.env.SELF_IMPROVEMENT_HOOK_DEBUG) {
|
||||
console.error('[self-improvement] hook failed:', err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1,374 @@
|
||||
# Entry Examples
|
||||
|
||||
Concrete examples of well-formatted entries with all fields.
|
||||
|
||||
## Learning: Correction
|
||||
|
||||
```markdown
|
||||
## [LRN-20250115-001] correction
|
||||
|
||||
**Logged**: 2025-01-15T10:30:00Z
|
||||
**Priority**: high
|
||||
**Status**: pending
|
||||
**Area**: tests
|
||||
|
||||
### Summary
|
||||
Incorrectly assumed pytest fixtures are scoped to function by default
|
||||
|
||||
### Details
|
||||
When writing test fixtures, I assumed all fixtures were function-scoped.
|
||||
User corrected that while function scope is the default, the codebase
|
||||
convention uses module-scoped fixtures for database connections to
|
||||
improve test performance.
|
||||
|
||||
### Suggested Action
|
||||
When creating fixtures that involve expensive setup (DB, network),
|
||||
check existing fixtures for scope patterns before defaulting to function scope.
|
||||
|
||||
### Metadata
|
||||
- Source: user_feedback
|
||||
- Related Files: tests/conftest.py
|
||||
- Tags: pytest, testing, fixtures
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Learning: Knowledge Gap (Resolved)
|
||||
|
||||
```markdown
|
||||
## [LRN-20250115-002] knowledge_gap
|
||||
|
||||
**Logged**: 2025-01-15T14:22:00Z
|
||||
**Priority**: medium
|
||||
**Status**: resolved
|
||||
**Area**: config
|
||||
|
||||
### Summary
|
||||
Project uses pnpm not npm for package management
|
||||
|
||||
### Details
|
||||
Attempted to run `npm install` but project uses pnpm workspaces.
|
||||
Lock file is `pnpm-lock.yaml`, not `package-lock.json`.
|
||||
|
||||
### Suggested Action
|
||||
Check for `pnpm-lock.yaml` or `pnpm-workspace.yaml` before assuming npm.
|
||||
Use `pnpm install` for this project.
|
||||
|
||||
### Metadata
|
||||
- Source: error
|
||||
- Related Files: pnpm-lock.yaml, pnpm-workspace.yaml
|
||||
- Tags: package-manager, pnpm, setup
|
||||
|
||||
### Resolution
|
||||
- **Resolved**: 2025-01-15T14:30:00Z
|
||||
- **Commit/PR**: N/A - knowledge update
|
||||
- **Notes**: Added to TOOLS.md for future reference
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Learning: Promoted to TOOLS.md
|
||||
|
||||
```markdown
|
||||
## [LRN-20250115-003] best_practice
|
||||
|
||||
**Logged**: 2025-01-15T16:00:00Z
|
||||
**Priority**: high
|
||||
**Status**: promoted
|
||||
**Promoted**: TOOLS.md
|
||||
**Area**: backend
|
||||
|
||||
### Summary
|
||||
API responses must include correlation ID from request headers
|
||||
|
||||
### Details
|
||||
All API responses should echo back the X-Correlation-ID header from
|
||||
the request. This is required for distributed tracing. Responses
|
||||
without this header break the observability pipeline.
|
||||
|
||||
### Suggested Action
|
||||
Always include correlation ID passthrough in API handlers.
|
||||
|
||||
### Metadata
|
||||
- Source: user_feedback
|
||||
- Related Files: src/middleware/correlation.ts
|
||||
- Tags: api, observability, tracing
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Learning: Promoted to AGENTS.md
|
||||
|
||||
```markdown
|
||||
## [LRN-20250116-001] best_practice
|
||||
|
||||
**Logged**: 2025-01-16T09:00:00Z
|
||||
**Priority**: high
|
||||
**Status**: promoted
|
||||
**Promoted**: AGENTS.md
|
||||
**Area**: backend
|
||||
|
||||
### Summary
|
||||
Must regenerate API client after OpenAPI spec changes
|
||||
|
||||
### Details
|
||||
When modifying API endpoints, the TypeScript client must be regenerated.
|
||||
Forgetting this causes type mismatches that only appear at runtime.
|
||||
The generate script also runs validation.
|
||||
|
||||
### Suggested Action
|
||||
Add to agent workflow: after any API changes, run `pnpm run generate:api`.
|
||||
|
||||
### Metadata
|
||||
- Source: error
|
||||
- Related Files: openapi.yaml, src/client/api.ts
|
||||
- Tags: api, codegen, typescript
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Error Entry
|
||||
|
||||
```markdown
|
||||
## [ERR-20250115-A3F] docker_build
|
||||
|
||||
**Logged**: 2025-01-15T09:15:00Z
|
||||
**Priority**: high
|
||||
**Status**: pending
|
||||
**Area**: infra
|
||||
|
||||
### Summary
|
||||
Docker build fails on M1 Mac due to platform mismatch
|
||||
|
||||
### Error
|
||||
```
|
||||
error: failed to solve: python:3.11-slim: no match for platform linux/arm64
|
||||
```
|
||||
|
||||
### Context
|
||||
- Command: `docker build -t myapp .`
|
||||
- Dockerfile uses `FROM python:3.11-slim`
|
||||
- Running on Apple Silicon (M1/M2)
|
||||
|
||||
### Suggested Fix
|
||||
Add platform flag: `docker build --platform linux/amd64 -t myapp .`
|
||||
Or update Dockerfile: `FROM --platform=linux/amd64 python:3.11-slim`
|
||||
|
||||
### Metadata
|
||||
- Reproducible: yes
|
||||
- Related Files: Dockerfile
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Error Entry: Recurring Issue
|
||||
|
||||
```markdown
|
||||
## [ERR-20250120-B2C] api_timeout
|
||||
|
||||
**Logged**: 2025-01-20T11:30:00Z
|
||||
**Priority**: critical
|
||||
**Status**: pending
|
||||
**Area**: backend
|
||||
|
||||
### Summary
|
||||
Third-party API timeout during request processing
|
||||
|
||||
### Error
|
||||
```
|
||||
TimeoutError: Request to api.example.com timed out after 30000ms
|
||||
```
|
||||
|
||||
### Context
|
||||
- Command: POST /api/process
|
||||
- Timeout set to 30s
|
||||
- Occurs during peak hours (lunch, evening)
|
||||
|
||||
### Suggested Fix
|
||||
Implement retry with exponential backoff. Consider circuit breaker pattern.
|
||||
|
||||
### Metadata
|
||||
- Reproducible: yes (during peak hours)
|
||||
- Related Files: src/services/api-client.ts
|
||||
- See Also: ERR-20250115-X1Y, ERR-20250118-Z3W
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Feature Request
|
||||
|
||||
```markdown
|
||||
## [FEAT-20250115-001] export_to_csv
|
||||
|
||||
**Logged**: 2025-01-15T16:45:00Z
|
||||
**Priority**: medium
|
||||
**Status**: pending
|
||||
**Area**: backend
|
||||
|
||||
### Requested Capability
|
||||
Export analysis results to CSV format
|
||||
|
||||
### User Context
|
||||
User runs weekly reports and needs to share results with non-technical
|
||||
stakeholders in Excel. Currently copies output manually.
|
||||
|
||||
### Complexity Estimate
|
||||
simple
|
||||
|
||||
### Suggested Implementation
|
||||
Add `--output csv` flag to the analyze command. Use standard csv module.
|
||||
Could extend existing `--output json` pattern.
|
||||
|
||||
### Metadata
|
||||
- Frequency: recurring
|
||||
- Related Features: analyze command, json output
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Feature Request: Resolved
|
||||
|
||||
```markdown
|
||||
## [FEAT-20250110-002] dark_mode
|
||||
|
||||
**Logged**: 2025-01-10T14:00:00Z
|
||||
**Priority**: low
|
||||
**Status**: resolved
|
||||
**Area**: frontend
|
||||
|
||||
### Requested Capability
|
||||
Dark mode support for the dashboard
|
||||
|
||||
### User Context
|
||||
User works late hours and finds the bright interface straining.
|
||||
Several other users have mentioned this informally.
|
||||
|
||||
### Complexity Estimate
|
||||
medium
|
||||
|
||||
### Suggested Implementation
|
||||
Use CSS variables for colors. Add toggle in user settings.
|
||||
Consider system preference detection.
|
||||
|
||||
### Metadata
|
||||
- Frequency: recurring
|
||||
- Related Features: user settings, theme system
|
||||
|
||||
### Resolution
|
||||
- **Resolved**: 2025-01-18T16:00:00Z
|
||||
- **Commit/PR**: #142
|
||||
- **Notes**: Implemented with system preference detection and manual toggle
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Learning: Promoted to Skill
|
||||
|
||||
```markdown
|
||||
## [LRN-20250118-001] best_practice
|
||||
|
||||
**Logged**: 2025-01-18T11:00:00Z
|
||||
**Priority**: high
|
||||
**Status**: promoted_to_skill
|
||||
**Skill-Path**: skills/docker-m1-fixes
|
||||
**Area**: infra
|
||||
|
||||
### Summary
|
||||
Docker build fails on Apple Silicon due to platform mismatch
|
||||
|
||||
### Details
|
||||
When building Docker images on M1/M2 Macs, the build fails because
|
||||
the base image doesn't have an ARM64 variant. This is a common issue
|
||||
that affects many developers.
|
||||
|
||||
### Suggested Action
|
||||
Add `--platform linux/amd64` to docker build command, or use
|
||||
`FROM --platform=linux/amd64` in Dockerfile.
|
||||
|
||||
### Metadata
|
||||
- Source: error
|
||||
- Related Files: Dockerfile
|
||||
- Tags: docker, arm64, m1, apple-silicon
|
||||
- See Also: ERR-20250115-A3F, ERR-20250117-B2D
|
||||
|
||||
---
|
||||
```
|
||||
|
||||
## Extracted Skill Example
|
||||
|
||||
When the above learning is extracted as a skill, it becomes:
|
||||
|
||||
**File**: `skills/docker-m1-fixes/SKILL.md`
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: docker-m1-fixes
|
||||
description: "Fixes Docker build failures on Apple Silicon (M1/M2). Use when docker build fails with platform mismatch errors."
|
||||
---
|
||||
|
||||
# Docker M1 Fixes
|
||||
|
||||
Solutions for Docker build issues on Apple Silicon Macs.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Error | Fix |
|
||||
|-------|-----|
|
||||
| `no match for platform linux/arm64` | Add `--platform linux/amd64` to build |
|
||||
| Image runs but crashes | Use emulation or find ARM-compatible base |
|
||||
|
||||
## The Problem
|
||||
|
||||
Many Docker base images don't have ARM64 variants. When building on
|
||||
Apple Silicon (M1/M2/M3), Docker attempts to pull ARM64 images by
|
||||
default, causing platform mismatch errors.
|
||||
|
||||
## Solutions
|
||||
|
||||
### Option 1: Build Flag (Recommended)
|
||||
|
||||
Add platform flag to your build command:
|
||||
|
||||
\`\`\`bash
|
||||
docker build --platform linux/amd64 -t myapp .
|
||||
\`\`\`
|
||||
|
||||
### Option 2: Dockerfile Modification
|
||||
|
||||
Specify platform in the FROM instruction:
|
||||
|
||||
\`\`\`dockerfile
|
||||
FROM --platform=linux/amd64 python:3.11-slim
|
||||
\`\`\`
|
||||
|
||||
### Option 3: Docker Compose
|
||||
|
||||
Add platform to your service:
|
||||
|
||||
\`\`\`yaml
|
||||
services:
|
||||
app:
|
||||
platform: linux/amd64
|
||||
build: .
|
||||
\`\`\`
|
||||
|
||||
## Trade-offs
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Build flag | No file changes | Must remember flag |
|
||||
| Dockerfile | Explicit, versioned | Affects all builds |
|
||||
| Compose | Convenient for dev | Requires compose |
|
||||
|
||||
## Performance Note
|
||||
|
||||
Running AMD64 images on ARM64 uses Rosetta 2 emulation. This works
|
||||
for development but may be slower. For production, find ARM-native
|
||||
alternatives when possible.
|
||||
|
||||
## Source
|
||||
|
||||
- Learning ID: LRN-20250118-001
|
||||
- Category: best_practice
|
||||
- Extraction Date: 2025-01-18
|
||||
```
|
||||
@@ -0,0 +1,333 @@
|
||||
# OpenClaw Integration
|
||||
|
||||
Complete setup and usage guide for integrating the self-improvement skill with OpenClaw.
|
||||
|
||||
## Overview
|
||||
|
||||
OpenClaw uses workspace-based prompt injection combined with event-driven hooks. Context is injected from workspace files at session start, and hooks can trigger on lifecycle events.
|
||||
|
||||
## Workspace Structure
|
||||
|
||||
```
|
||||
~/.openclaw/
|
||||
├── workspace/ # Working directory
|
||||
│ ├── AGENTS.md # Multi-agent coordination patterns
|
||||
│ ├── SOUL.md # Behavioral guidelines and personality
|
||||
│ ├── TOOLS.md # Tool capabilities and gotchas
|
||||
│ ├── MEMORY.md # Long-term memory (main session only)
|
||||
│ └── memory/ # Daily memory files
|
||||
│ └── YYYY-MM-DD.md
|
||||
├── skills/ # Installed skills
|
||||
│ └── <skill-name>/
|
||||
│ └── SKILL.md
|
||||
└── hooks/ # Custom hooks
|
||||
└── <hook-name>/
|
||||
├── HOOK.md
|
||||
└── handler.ts
|
||||
```
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### 1. Install the Skill
|
||||
|
||||
```bash
|
||||
openclaw skills install @pskoett/self-improving-agent
|
||||
```
|
||||
|
||||
Or with the ClawHub CLI (`npm i -g clawhub`) — note this installs into
|
||||
`./skills` under the current working directory, not the workspace:
|
||||
|
||||
```bash
|
||||
clawhub install @pskoett/self-improving-agent
|
||||
```
|
||||
|
||||
Or copy manually — the skill package is the `self-improving-agent/`
|
||||
subfolder of the repo, not the repo root:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/pskoett/self-improving-agent.git /tmp/self-improving-agent-repo
|
||||
cp -r /tmp/self-improving-agent-repo/self-improving-agent ~/.openclaw/skills/self-improving-agent
|
||||
```
|
||||
|
||||
### 2. Install the Hook (Optional)
|
||||
|
||||
Copy the hook to OpenClaw's hooks directory:
|
||||
|
||||
```bash
|
||||
cp -r ~/.openclaw/skills/self-improving-agent/hooks/openclaw ~/.openclaw/hooks/self-improvement
|
||||
```
|
||||
|
||||
Enable the hook:
|
||||
|
||||
```bash
|
||||
openclaw hooks enable self-improvement
|
||||
```
|
||||
|
||||
The hook does two things:
|
||||
|
||||
- **`agent:bootstrap`** — injects the self-improvement reminder into session
|
||||
context (and flags auto-detected errors awaiting triage)
|
||||
- **`command:new` / `command:reset`** — sweeps the transcript of the session
|
||||
that just ended for error patterns and appends pending entries to
|
||||
`<workspace>/.learnings/ERRORS.md` (only if `.learnings/` exists — see
|
||||
[Error Detection](#error-detection))
|
||||
|
||||
### 3. Create Learning Files
|
||||
|
||||
Create the `.learnings/` directory in your workspace:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
Or in the skill directory:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.openclaw/skills/self-improving-agent/.learnings
|
||||
```
|
||||
|
||||
## Injected Prompt Files
|
||||
|
||||
### AGENTS.md
|
||||
|
||||
Purpose: Multi-agent workflows and delegation patterns.
|
||||
|
||||
```markdown
|
||||
# Agent Coordination
|
||||
|
||||
## Delegation Rules
|
||||
- Use explore agent for open-ended codebase questions
|
||||
- Spawn sub-agents for long-running tasks
|
||||
- Use sessions_send for cross-session communication
|
||||
|
||||
## Session Handoff
|
||||
When delegating to another session:
|
||||
1. Provide full context in the handoff message
|
||||
2. Include relevant file paths
|
||||
3. Specify expected output format
|
||||
```
|
||||
|
||||
### SOUL.md
|
||||
|
||||
Purpose: Behavioral guidelines and communication style.
|
||||
|
||||
```markdown
|
||||
# Behavioral Guidelines
|
||||
|
||||
## Communication Style
|
||||
- Be direct and concise
|
||||
- Avoid unnecessary caveats and disclaimers
|
||||
- Use technical language appropriate to context
|
||||
|
||||
## Error Handling
|
||||
- Admit mistakes promptly
|
||||
- Provide corrected information immediately
|
||||
- Log significant errors to learnings
|
||||
```
|
||||
|
||||
### TOOLS.md
|
||||
|
||||
Purpose: Tool capabilities, integration gotchas, local configuration.
|
||||
|
||||
```markdown
|
||||
# Tool Knowledge
|
||||
|
||||
## Self-Improvement Skill
|
||||
Log learnings to `.learnings/` for continuous improvement.
|
||||
|
||||
## Local Tools
|
||||
- Document tool-specific gotchas here
|
||||
- Note authentication requirements
|
||||
- Track integration quirks
|
||||
```
|
||||
|
||||
## Learning Workflow
|
||||
|
||||
### Capturing Learnings
|
||||
|
||||
1. **In-session**: Log to `.learnings/` as usual
|
||||
2. **Cross-session**: Promote to workspace files
|
||||
|
||||
### Promotion Decision Tree
|
||||
|
||||
```
|
||||
Is the learning project-specific?
|
||||
├── Yes → Keep in .learnings/
|
||||
└── No → Is it behavioral/style-related?
|
||||
├── Yes → Promote to SOUL.md
|
||||
└── No → Is it tool-related?
|
||||
├── Yes → Promote to TOOLS.md
|
||||
└── No → Promote to AGENTS.md (workflow)
|
||||
```
|
||||
|
||||
### Promotion Format Examples
|
||||
|
||||
**From learning:**
|
||||
> Git push to GitHub fails without auth configured - triggers desktop prompt
|
||||
|
||||
**To TOOLS.md:**
|
||||
```markdown
|
||||
## Git
|
||||
- Don't push without confirming auth is configured
|
||||
- Use `gh auth status` to check GitHub CLI auth
|
||||
```
|
||||
|
||||
## Inter-Agent Communication
|
||||
|
||||
OpenClaw provides tools for cross-session communication:
|
||||
|
||||
Use these only when cross-session sharing is explicitly needed and the environment is trusted. Prefer short sanitized summaries over raw transcripts, command output, or secret-bearing content.
|
||||
|
||||
### sessions_list
|
||||
|
||||
View active and recent sessions:
|
||||
```
|
||||
sessions_list(activeMinutes=30, messageLimit=3)
|
||||
```
|
||||
|
||||
### sessions_history
|
||||
|
||||
Read transcript from another session:
|
||||
```
|
||||
sessions_history(sessionKey="session-id", limit=50)
|
||||
```
|
||||
|
||||
Only read another session's transcript when the user explicitly wants shared context or continuation across sessions.
|
||||
|
||||
### sessions_send
|
||||
|
||||
Send message to another session:
|
||||
```
|
||||
sessions_send(sessionKey="session-id", message="Learning: API requires X-Custom-Header")
|
||||
```
|
||||
|
||||
Prefer sending a concise learning summary plus relevant paths rather than forwarding raw transcript content.
|
||||
|
||||
### sessions_spawn
|
||||
|
||||
Spawn a background sub-agent:
|
||||
```
|
||||
sessions_spawn(task="Research X and report back", label="research")
|
||||
```
|
||||
|
||||
## Available Hook Events
|
||||
|
||||
| Event | When It Fires |
|
||||
|-------|---------------|
|
||||
| `agent:bootstrap` | Before workspace files inject |
|
||||
| `command:new` | When `/new` command issued |
|
||||
| `command:reset` | When `/reset` command issued |
|
||||
| `command:stop` | When `/stop` command issued |
|
||||
| `gateway:startup` | When gateway starts |
|
||||
| `gateway:shutdown` | When gateway shuts down |
|
||||
| `message:received` / `message:sent` | Around message delivery |
|
||||
| `session:compact:before` / `:after` | Around session compaction |
|
||||
|
||||
**Important:** OpenClaw has **no per-tool-call event** — nothing fires after
|
||||
each individual tool call, so real-time per-command error detection is not
|
||||
possible. Error detection is done at session end instead (see below).
|
||||
|
||||
## Error Detection
|
||||
|
||||
The skill's hook (`hooks/openclaw/`) implements a **session-end error
|
||||
sweep**:
|
||||
|
||||
1. When `/new` or `/reset` ends a session, the hook resolves the ended
|
||||
session's transcript from `context.previousSessionEntry` (falling back to
|
||||
`<workspace>/sessions/<sessionId>.jsonl`) — the same source OpenClaw's
|
||||
bundled `session-memory` hook uses.
|
||||
2. The transcript is scanned against a fixed error-pattern list (`Error:`,
|
||||
`command not found`, `Traceback`, `npm ERR!`, `Permission denied`, …).
|
||||
3. Matches are appended to `<workspace>/.learnings/ERRORS.md` as a `pending`
|
||||
entry with `Source: openclaw-error-sweep`, containing at most 5 short
|
||||
excerpts (truncated to 200 chars, common secret shapes redacted,
|
||||
duplicates skipped). Each entry is stamped with deterministic
|
||||
`Pattern-Key` values derived from the matched pattern (for example
|
||||
`ModuleNotFoundError` → `deps.module-not-found`), so recurrences can be
|
||||
counted by key during triage — see the Pattern-Key Taxonomy in `SKILL.md`.
|
||||
4. At the next `agent:bootstrap`, the injected reminder includes a
|
||||
**pending triage** note so the agent reviews the auto-detected entries —
|
||||
confirming real errors, filling in fixes, or deleting noise.
|
||||
|
||||
### Enabling / Disabling the Sweep
|
||||
|
||||
The sweep is opt-in and gated on the `.learnings/` directory:
|
||||
|
||||
```bash
|
||||
# Enable
|
||||
mkdir -p ~/.openclaw/workspace/.learnings
|
||||
|
||||
# Disable (reminder injection keeps working)
|
||||
rm -r ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
### Sweep Limitations
|
||||
|
||||
- Sessions that are never ended with `/new` or `/reset` are not swept.
|
||||
- Detection happens at session end, not immediately after the failing
|
||||
command — there is no per-tool-call event to hook.
|
||||
- Pattern matching is heuristic; it can flag false positives such as prose
|
||||
containing the word "failed" — that's what the triage step is for.
|
||||
- Excerpts are redacted with best-effort rules; treat `.learnings/` as
|
||||
potentially sensitive and keep it out of version control by default.
|
||||
|
||||
## Detection Triggers
|
||||
|
||||
### Standard Triggers
|
||||
- User corrections ("No, that's wrong...")
|
||||
- Command failures (non-zero exit codes)
|
||||
- API errors
|
||||
- Knowledge gaps
|
||||
|
||||
### OpenClaw-Specific Triggers
|
||||
|
||||
| Trigger | Action |
|
||||
|---------|--------|
|
||||
| Tool call error | Log to TOOLS.md with tool name |
|
||||
| Session handoff confusion | Log to AGENTS.md with delegation pattern |
|
||||
| Model behavior surprise | Log to SOUL.md with expected vs actual |
|
||||
| Skill issue | Log to .learnings/ or report upstream |
|
||||
|
||||
## Verification
|
||||
|
||||
Check hook is registered:
|
||||
|
||||
```bash
|
||||
openclaw hooks list
|
||||
```
|
||||
|
||||
Check skill is loaded:
|
||||
|
||||
```bash
|
||||
openclaw status
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hook not firing
|
||||
|
||||
1. Ensure hooks enabled in config
|
||||
2. Restart gateway after config changes
|
||||
3. Check gateway logs for errors
|
||||
|
||||
### Learnings not persisting
|
||||
|
||||
1. Verify `.learnings/` directory exists
|
||||
2. Check file permissions
|
||||
3. Ensure workspace path is configured correctly
|
||||
|
||||
### Error sweep not writing entries
|
||||
|
||||
1. Verify `<workspace>/.learnings/` exists (the sweep is opt-in and skips
|
||||
silently without it)
|
||||
2. End the session with `/new` or `/reset` — the sweep only runs on those
|
||||
commands
|
||||
3. Confirm a transcript exists in `<workspace>/sessions/`
|
||||
4. Run the gateway with `SELF_IMPROVEMENT_HOOK_DEBUG=1` to surface hook errors
|
||||
5. Remember excerpts already present in `ERRORS.md` are skipped (dedupe)
|
||||
|
||||
### Skill not loading
|
||||
|
||||
1. Check skill is in skills directory
|
||||
2. Verify SKILL.md has correct frontmatter
|
||||
3. Run `openclaw status` to see loaded skills
|
||||
@@ -0,0 +1,55 @@
|
||||
# Uninstall Guide
|
||||
|
||||
How to disable or fully remove the self-improvement skill. **Disabling** turns
|
||||
off automatic behavior but keeps your data; **removing** deletes the skill's
|
||||
files. They are different operations — do the one you mean.
|
||||
|
||||
> **Your learnings are data, not skill machinery.** `.learnings/` contains the
|
||||
> errors, corrections, and insights the skill captured for you. Review or
|
||||
> archive it before deleting anything. Content the skill *promoted* into
|
||||
> `SOUL.md`, `TOOLS.md`, or `AGENTS.md` is part of those files now — removing
|
||||
> the skill does not (and should not) automatically remove it.
|
||||
|
||||
## Disable only
|
||||
|
||||
```bash
|
||||
# Stop the hook (reminder injection + session-end error sweep)
|
||||
openclaw hooks disable self-improvement
|
||||
```
|
||||
|
||||
To keep the bootstrap reminder but disable only the error sweep, remove the
|
||||
learnings directory instead (archive it first if it has entries):
|
||||
|
||||
```bash
|
||||
rm -r ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
Restart the gateway after hook changes.
|
||||
|
||||
## Remove completely
|
||||
|
||||
```bash
|
||||
# 1. Disable and remove the hook
|
||||
openclaw hooks disable self-improvement
|
||||
rm -r ~/.openclaw/hooks/self-improvement
|
||||
|
||||
# 2. Remove the skill
|
||||
rm -r ~/.openclaw/skills/self-improving-agent
|
||||
|
||||
# 3. Optional — remove captured learnings (REVIEW FIRST, this is your data)
|
||||
rm -r ~/.openclaw/workspace/.learnings
|
||||
```
|
||||
|
||||
Then restart the gateway and verify with `openclaw hooks list` and
|
||||
`openclaw status`.
|
||||
|
||||
Manually review `SOUL.md`, `TOOLS.md`, and `AGENTS.md` in
|
||||
`~/.openclaw/workspace/` for sections promoted by this skill and delete the
|
||||
ones you no longer want. If the skill logged `.learnings/` directories into
|
||||
project repos you worked in, review and remove those separately.
|
||||
|
||||
## Verification
|
||||
|
||||
- `openclaw hooks list` no longer shows `self-improvement`
|
||||
- New sessions no longer contain `SELF_IMPROVEMENT_REMINDER.md`
|
||||
- No stray `.learnings/` directories remain that you didn't choose to keep
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/bin/bash
|
||||
# Skill Extraction Helper
|
||||
# Creates a new skill from a learning entry
|
||||
# Usage: ./extract-skill.sh <skill-name> [--dry-run]
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration
|
||||
SKILLS_DIR="./skills"
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $(basename "$0") <skill-name> [options]
|
||||
|
||||
Create a new skill from a learning entry.
|
||||
|
||||
Arguments:
|
||||
skill-name Name of the skill (lowercase, hyphens for spaces)
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be created without creating files
|
||||
--output-dir Relative output directory under current path (default: ./skills)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") docker-m1-fixes
|
||||
$(basename "$0") api-timeout-patterns --dry-run
|
||||
$(basename "$0") pnpm-setup --output-dir ./skills/custom
|
||||
|
||||
The skill will be created in: \$SKILLS_DIR/<skill-name>/
|
||||
EOF
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1" >&2
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
SKILL_NAME=""
|
||||
DRY_RUN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--output-dir)
|
||||
if [ -z "${2:-}" ] || [[ "${2:-}" == -* ]]; then
|
||||
log_error "--output-dir requires a relative path argument"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
SKILLS_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
log_error "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ -z "$SKILL_NAME" ]; then
|
||||
SKILL_NAME="$1"
|
||||
else
|
||||
log_error "Unexpected argument: $1"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate skill name
|
||||
if [ -z "$SKILL_NAME" ]; then
|
||||
log_error "Skill name is required"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate skill name format (lowercase, hyphens, no spaces)
|
||||
if ! [[ "$SKILL_NAME" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
|
||||
log_error "Invalid skill name format. Use lowercase letters, numbers, and hyphens only."
|
||||
log_error "Examples: 'docker-fixes', 'api-patterns', 'pnpm-setup'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate output path to avoid writes outside current workspace.
|
||||
if [[ "$SKILLS_DIR" = /* ]]; then
|
||||
log_error "Output directory must be a relative path under the current directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKILLS_DIR" =~ (^|/)\.\.(/|$) ]]; then
|
||||
log_error "Output directory cannot include '..' path segments."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SKILLS_DIR="${SKILLS_DIR#./}"
|
||||
SKILLS_DIR="./$SKILLS_DIR"
|
||||
|
||||
SKILL_PATH="$SKILLS_DIR/$SKILL_NAME"
|
||||
|
||||
# Check if skill already exists
|
||||
if [ -d "$SKILL_PATH" ] && [ "$DRY_RUN" = false ]; then
|
||||
log_error "Skill already exists: $SKILL_PATH"
|
||||
log_error "Use a different name or remove the existing skill first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Dry run output
|
||||
if [ "$DRY_RUN" = true ]; then
|
||||
log_info "Dry run - would create:"
|
||||
echo " $SKILL_PATH/"
|
||||
echo " $SKILL_PATH/SKILL.md"
|
||||
echo ""
|
||||
echo "Template content would be:"
|
||||
echo "---"
|
||||
cat << TEMPLATE
|
||||
name: $SKILL_NAME
|
||||
description: "[TODO: Add a concise description of what this skill does and when to use it]"
|
||||
---
|
||||
|
||||
# $(echo "$SKILL_NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
|
||||
|
||||
[TODO: Brief introduction explaining the skill's purpose]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| [Trigger condition] | [What to do] |
|
||||
|
||||
## Usage
|
||||
|
||||
[TODO: Detailed usage instructions]
|
||||
|
||||
## Examples
|
||||
|
||||
[TODO: Add concrete examples]
|
||||
|
||||
## Source Learning
|
||||
|
||||
This skill was extracted from a learning entry.
|
||||
- Learning ID: [TODO: Add original learning ID]
|
||||
- Original File: .learnings/LEARNINGS.md
|
||||
TEMPLATE
|
||||
echo "---"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Create skill directory structure
|
||||
log_info "Creating skill: $SKILL_NAME"
|
||||
|
||||
mkdir -p "$SKILL_PATH"
|
||||
|
||||
# Create SKILL.md from template
|
||||
cat > "$SKILL_PATH/SKILL.md" << TEMPLATE
|
||||
---
|
||||
name: $SKILL_NAME
|
||||
description: "[TODO: Add a concise description of what this skill does and when to use it]"
|
||||
---
|
||||
|
||||
# $(echo "$SKILL_NAME" | sed 's/-/ /g' | awk '{for(i=1;i<=NF;i++) $i=toupper(substr($i,1,1)) tolower(substr($i,2))}1')
|
||||
|
||||
[TODO: Brief introduction explaining the skill's purpose]
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Situation | Action |
|
||||
|-----------|--------|
|
||||
| [Trigger condition] | [What to do] |
|
||||
|
||||
## Usage
|
||||
|
||||
[TODO: Detailed usage instructions]
|
||||
|
||||
## Examples
|
||||
|
||||
[TODO: Add concrete examples]
|
||||
|
||||
## Source Learning
|
||||
|
||||
This skill was extracted from a learning entry.
|
||||
- Learning ID: [TODO: Add original learning ID]
|
||||
- Original File: .learnings/LEARNINGS.md
|
||||
TEMPLATE
|
||||
|
||||
log_info "Created: $SKILL_PATH/SKILL.md"
|
||||
|
||||
# Suggest next steps
|
||||
echo ""
|
||||
log_info "Skill scaffold created successfully!"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Edit $SKILL_PATH/SKILL.md"
|
||||
echo " 2. Fill in the TODO sections with content from your learning"
|
||||
echo " 3. Add references/ folder if you have detailed documentation"
|
||||
echo " 4. Add scripts/ folder if you have executable code"
|
||||
echo " 5. Update the original learning entry with:"
|
||||
echo " **Status**: promoted_to_skill"
|
||||
echo " **Skill-Path**: skills/$SKILL_NAME"
|
||||
@@ -0,0 +1,60 @@
|
||||
## Description:
|
||||
|
||||
Captures learnings, errors, and corrections to enable continuous improvement. Use when: (1) A command or operation fails unexpectedly, (2) User corrects Claude ('No, that's wrong...', 'Actually...'), (3) User requests a capability that doesn't exist, (4) An external API or tool fails, (5) Claude realizes its knowledge is outdated or incorrect, (6) A better approach is discovered for a recurring task. Also review learnings before major tasks.
|
||||
|
||||
This skill is ready for commercial/non-commercial use.
|
||||
|
||||
## Publisher:
|
||||
|
||||
[pskoett](https://clawhub.ai/user/pskoett)
|
||||
|
||||
### License/Terms of Use:
|
||||
|
||||
MIT-0
|
||||
|
||||
## Use Case:
|
||||
|
||||
Developers and agent users use this skill to capture corrections, command failures, feature requests, outdated knowledge, and reusable best practices in workspace learning files. OpenClaw users can also opt into a hook that reminds agents to review learnings and sweeps ended sessions for possible errors.
|
||||
|
||||
### Deployment Geography for Use:
|
||||
|
||||
Global
|
||||
|
||||
## Known Risks and Mitigations:
|
||||
|
||||
Risk: The optional hook may read ended session transcripts and persist excerpts in .learnings, with only best-effort redaction.
|
||||
|
||||
Mitigation: Enable the hook only in trusted workspaces, keep .learnings out of version control, and review captured entries before sharing or promotion.
|
||||
|
||||
Risk: Automatically detected errors can include false positives or incomplete context.
|
||||
|
||||
Mitigation: Triage pending sweep entries before keeping them, promoting them, or converting them into workspace guidance.
|
||||
|
||||
Risk: Uninstall or cleanup commands can delete .learnings, which is user data.
|
||||
|
||||
Mitigation: Review or archive .learnings before running deletion commands.
|
||||
|
||||
## Reference(s):
|
||||
|
||||
- [ClawHub skill page](https://clawhub.ai/pskoett/skills/self-improving-agent)
|
||||
- [OpenClaw integration guide](references/openclaw-integration.md)
|
||||
- [Uninstall guide](references/uninstall.md)
|
||||
- [Examples](references/examples.md)
|
||||
|
||||
## Skill Output:
|
||||
|
||||
**Output Type(s):** [Markdown, Files, Shell commands, Configuration, Guidance]
|
||||
|
||||
**Output Format:** [Markdown entries with inline shell commands and configuration snippets]
|
||||
|
||||
**Output Parameters:** [1D]
|
||||
|
||||
**Other Properties Related to Output:** [Creates or appends to .learnings files; the optional hook writes truncated, redacted error excerpts for later triage.]
|
||||
|
||||
## Skill Version(s):
|
||||
|
||||
4.0.2 (source: frontmatter, changelog, server evidence)
|
||||
|
||||
## Ethical Considerations:
|
||||
|
||||
Users should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.
|
||||
@@ -0,0 +1,124 @@
|
||||
# 改进周报 (2026-W38)
|
||||
|
||||
**周期:** 2026-09-14 ~ 2026-09-16(本周至今,周三)
|
||||
**生成方式:** self-improving-agent 技能
|
||||
**数据来源:** `.learnings/` 日志、会话 transcript、workspace 配置文件实测
|
||||
|
||||
---
|
||||
|
||||
## 一、本周概览
|
||||
|
||||
| 指标 | 数量 |
|
||||
|------|------|
|
||||
| 新增学习条目 (LRN) | 5 |
|
||||
| 新增错误条目 (ERR) | 1 |
|
||||
| 新增需求条目 (FEAT) | 0 |
|
||||
| 已解决 | 3 |
|
||||
| 已升级到配置文档 | 1 条(涉及 TOOLS.md、MEMORY.md 两个文件) |
|
||||
| 待处理 | 2 |
|
||||
|
||||
> 说明:`.learnings/` 本周**首次启用**(此前不存在),因此本轮为回溯补录——
|
||||
> 把此前散落在会话中的教训固化下来,避免下次再踩。
|
||||
|
||||
---
|
||||
|
||||
## 二、关键发现
|
||||
|
||||
### 🔴 1. 陈旧路径导致「技能找不到」假象
|
||||
**类型:** correction | **状态:** ✅ 已修复
|
||||
|
||||
`AGENTS.md` 指向 `skills/weekly-report-g5/SKILL.md`,该路径**不存在**(实测返回 `ENOENT`)。
|
||||
在用技能实际是 `skills/weekly-report/SKILL.md`;`weekly-report-g5` 只存在于 Skill Workshop 提案目录。
|
||||
|
||||
- **影响:** AGENTS.md 是每次会话必读文件,错误路径会浪费工具调用,更可能让后续会话误判「技能不存在」而重复造轮子。
|
||||
- **动作:** 已修正路径,并补登自我改进技能条目。
|
||||
|
||||
### 🟠 2. 文档描述与实际行为不符
|
||||
**类型:** correction | **状态:** ✅ 已升级
|
||||
|
||||
`TOOLS.md` / `MEMORY.md` 曾称 `send_weekly_report.py` 会「同步元数据到 db」。逐条核对脚本行为后确认:该脚本**从不写 SQLite**,只有两个落盘动作——
|
||||
|
||||
| 动作 | 目标 |
|
||||
|------|------|
|
||||
| `archive_report()` | Markdown → `weekly-reports/YYYY/` |
|
||||
| `sync_to_mysql()` | MySQL `resume` 库 |
|
||||
|
||||
- **风险:** 后续会话会误以为 SQLite 已自动入库,从而漏跑 `sync_reports_db.py`,造成本地索引与 Markdown **静默不一致**。
|
||||
- **动作:** 已修正两份文档表述。
|
||||
|
||||
### 🟡 3. shell 日期推导会「静默算错」
|
||||
**类型:** knowledge_gap | **状态:** 待处理
|
||||
|
||||
2026-09-16(周三)实测:
|
||||
|
||||
| 表达式 | 结果 | 判定 |
|
||||
|--------|------|------|
|
||||
| `date -d 'monday this week'` | `2026-09-21` | ❌ 下周一 |
|
||||
| `date -d 'last monday'` | `2026-09-14` | ✅ 本周一 |
|
||||
|
||||
- **风险:** 不报错、直接算出一周之后的区间,属「静默错误」,比崩溃更难发现。
|
||||
- **对策:** 周报日期范围一律以**用户提供的任务日期**为准,不用表达式推导。
|
||||
|
||||
### 🟡 4. 【已更正】我曾把「非待处理」误判为「待处理」
|
||||
**类型:** correction | **状态:** ✅ 已更正
|
||||
|
||||
⚠️ **本条是本次复盘中最值得记的教训:我自己犯了一个「读错来源」的错。**
|
||||
|
||||
首轮我报告「Skill Workshop 有两个 pending 的 `weekly-report-g5` 提案,虚高待办数」。该结论**不成立**——我读的是提案目录里 `PROPOSAL.md` 头部自带的 `status: proposal`,那只是**静态正文标记**,不代表生命周期状态。用权威入口核实后:
|
||||
|
||||
| 提案 | 真实状态 |
|
||||
|------|----------|
|
||||
| `weekly-report-g5-20260626…` (v1) | `stale`(基线变动,无法再应用) |
|
||||
| `weekly-report-g5-20260821…` (v2) | `applied`(内容确已并入在用技能) |
|
||||
| `list --status pending` | **No skill proposals matched** |
|
||||
|
||||
- **正确做法:** 判断提案状态**只用 `skill_workshop list`**。同理,任何「状态类」结论都应以对应工具/接口为准,而非文件表面内容。
|
||||
- **动作:** 已更正本条与 `.learnings` 记录。经复核**无需清理提案**——本就不存在 pending,删除反而会丢掉 v2 已应用的历史记录。
|
||||
- **副产品:** 顺带确认这两个目录现仅存 `PROPOSAL.md`,原 `proposal.json`/`rollback.json` 已不在(2026-08-28 时还在),说明其间有 skill-collection-review 自动化做过迁移清理。
|
||||
|
||||
> 🎯 **这正是本技能的价值所在**:如果不做这轮复盘,这个误判会一直留在记录里,下次还可能照着错误结论去删东西。
|
||||
|
||||
### 🟢 5. 中文检索召回不可靠
|
||||
**类型:** knowledge_gap | **状态:** 待处理
|
||||
|
||||
`sessions_search` 两次中文错误关键词查询均返回空,但同一会话 transcript 中确实存在对应内容。
|
||||
|
||||
- **风险:** 会给出「什么都没发生」的假信号,让复盘遗漏真实错误。
|
||||
- **对策:** 错误复盘以 `.learnings/` 为主数据源,`sessions_search` 空结果不当作「无此事」。
|
||||
|
||||
---
|
||||
|
||||
## 三、本周已修复的错误
|
||||
|
||||
| 编号 | 问题 | 结果 |
|
||||
|------|------|------|
|
||||
| ERR-20260916-001 | `/tmp` 下运行脚本报 `ModuleNotFoundError` | ✅ 已解决(用 `PYTHONPATH` 指定模块路径) |
|
||||
|
||||
**根因:** Python 只把**脚本所在目录**加入 `sys.path`,cwd 不在其中。
|
||||
**教训:** 临时脚本要么写在 workspace 内,要么运行前显式设 `PYTHONPATH`。
|
||||
|
||||
---
|
||||
|
||||
## 四、下周改进重点
|
||||
|
||||
1. **已完事项复核** —— 本周两项均已当场完成:AGENTS.md 技能路径与格式残留已修正;提案核实后确认**无需清理**(不存在 pending)。
|
||||
2. **沉淀状态判断约定** —— 把「状态类结论只以工具/接口为准」写入规范:提案查 `skill_workshop list`、周报日期以用户提供为准。
|
||||
3. **沉淀运行约定** —— 把「PYTHONPATH / 脚本位置」、`date -d 'last monday'` 写进周报技能,避免下次再撞。
|
||||
4. **持续记录** —— 本周起 `.learnings/` 已常态化启用,后续教训即时入账,不再依赖回溯补录。
|
||||
|
||||
---
|
||||
|
||||
## 五、方法论沉淀
|
||||
|
||||
本轮确立的复盘原则:
|
||||
|
||||
- **只用可核实证据** —— 每条结论都有实测输出或文件依据,不写推测性「体会」。
|
||||
- **状态只看权威入口** —— 文件表面的 `status` 字段不代表真实生命周期状态(本轮实际踩坑)。
|
||||
- **区分「报错」与「静默错误」** —— 静默错误优先级更高,因为它不会被发现。
|
||||
- **能修就当场修** —— 陈旧路径、文档不符等确定性缺陷直接修复,不留到「下次」。
|
||||
- **结论需自检** —— 把误判写进记录后再核实,是有效的纠错方式(本轮靠它推翻了自身结论)。
|
||||
|
||||
---
|
||||
|
||||
*归档时间:2026-09-16*
|
||||
*日志位置:`.learnings/LEARNINGS.md`、`.learnings/ERRORS.md`*
|
||||
Reference in New Issue
Block a user