Compare commits

...

25 Commits

Author SHA1 Message Date
cfdaily 166172e0b8 [moz] impl(skill-mgmt): S1+S2 实现 — skill-management Skill + 设计文档修复
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 53s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 0s
S1: AGENTS.md 经验闭环规则(workspace 层,单独管理)
S2: skill-management Skill 完整实现
  - SKILL.md(主:综述 + 四阶段速查 + 验证标准 + 自我修补规则)
  - references/discover-l1.md(各 agent 03:00 自蒸馏操作指南)
  - references/discover-l2.md(庞统 05:00 整合审查操作指南)
  - references/distill.md(蒸馏规范 + 验证标准 + 矛盾处理)
  - references/apply.md(openclaw 原生机制 + per-agent 可见性)
  - references/improve.md(引用追踪 + 淘汰 + 提升)
  - assets/templates/skill-template.md(SKILL.md 标准模板)
  - assets/templates/signal-format.md(信号输出格式模板)
  - assets/checklists/quality-check.md(质量检查清单)

文档修复:cron 错开时间 5min → 15min
2026-06-18 22:13:01 +08:00
cfdaily 0bd1caff90 [moz] fix(docs): §19 审查修复 M1+S1-S4+G1-G2
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
M1: MERGE 流程注明 skill_workshop 只能写 workspace,公共目录用 cp/symlink
S1: 数据源计数统一(12 个含 L1 drafts)
S2: 各 agent L1 cron 错开 5 分钟避免资源争用
S3: MERGE 后通知各 agent quarantine workspace 同名 draft
S4: S6 清理计划补充 SELF_IMPROVEMENT_REMINDER.md
G1: Recurrence-Count 加 30 天时间窗口
G2: proposal 存储路径注明首次自动创建
2026-06-18 21:52:34 +08:00
cfdaily f615326514 [moz] docs: §19 Skill 生命周期管理 v2.0 — 双层 daily 蒸馏 + 融合 self-improvement + 部署目录结构
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 33s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 1s
主要变更:
- D3/D4 改为双层 daily(L1 各 agent 03:00 + L2 庞统 05:00)
- D6 废弃 .learnings/,JSONL 是唯一数据源
- D7 只创建一个 skill-management,四阶段放 references/
- DISCOVER 重写为双层结构
- 验证机制替换为 Recurrence-Count + Skill Extraction Criteria
- 新增 §7.4 per-agent Skill 存放位置与可见性
- 新增 §13 部署目录结构(完整流转路径)
- self-improvement skill 优势融合到 DISCOVER 输出格式
- 参考映射新增 7 条
2026-06-18 21:45:58 +08:00
pangtong-fujunshi c2c6c9a7f6 Merge PR #84: [moz] docs: §19 Skill 生命周期管理 + 经验闭环四阶段设计
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-18 05:49:11 +00:00
cfdaily f26de6cfda [moz] docs: §19 Skill 生命周期管理 + 经验闭环四阶段设计
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-18 13:34:14 +08:00
pangtong-fujunshi 1ff4d98a03 Merge PR #83: [moz] fix(spawner): PromptContext event_type/event_data 缺失修复
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-16 23:21:02 +00:00
cfdaily a953fc0bc7 [moz] fix(spawner): PromptContext 缺少 event_type/event_data 导致通知显示「事件类型: 未知」
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 0s
根因:spawner 构建 PromptContext 时只传了 action_type/action_steps,
遗漏了 event_type 和 event_data。ToolchainContextSection.render() 从
context.event_type 取值,为空时回退到 '未知'。

修复:从 must_haves JSON 同时提取 event_type 和 context(→event_data)。
2026-06-17 07:19:04 +08:00
pangtong-fujunshi 7f17ee69d7 Merge PR #82: [moz] docs: §18 Mail Handler Verify/Prompt 强化设计
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-16 23:16:08 +00:00
cfdaily f1e513cba2 [moz] docs: §18 Mail Handler Verify/Prompt 强化设计
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 28s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-16 23:14:23 +00:00
pangtong-fujunshi 627982db09 Merge PR #81: [moz] feat: Runaway Guard per-task dispatch 上限
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-16 23:10:42 +00:00
cfdaily 9ec601d747 [moz] feat: Runaway Guard per-task dispatch 上限
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 1s
§15 Runaway Guard — per-task dispatch_count 上限,防止无限循环 dispatch

问题:mail/toolchain task 走 handler auto-working(跳过 claim),不受
claim_timeout 3 次重试兜底保护。如果反复 spawn 但永远到不了 done/failed,
会无限循环消耗资源(实际案例:2026-06-15 mention 重复投递事件)。

设计:
- tasks 表新增 dispatch_count 字段
- 每次 ticker 成功 dispatch 时递增
- dispatch_count >= 10 时自动标 failed(reason=runaway_guard)
- 覆盖所有非终态(pending/working/claimed)
- 参考 Hermes v0.13 §3 Per-Task 重试上限

改动文件:
- src/blackboard/db.py: _safe_add_column dispatch_count
- src/blackboard/models.py: Task dataclass 加 dispatch_count
- src/daemon/ticker.py: dispatch 递增 + _check_timeouts runaway guard
- docs/design/15-runaway-guard.md: 设计文档
- tests/integration/test_ticker_integration.py: E13 测试 3 个

测试:456 passed, 3 skipped
2026-06-16 23:10:27 +00:00
pangtong-fujunshi cc5c7f5ad1 Merge PR #80
Deploy / ci (push) Failing after 8s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-16 14:49:17 +00:00
cfdaily d6cb854f68 fix: mention 重复投递 + mail 失败通知竞态保护 + §14 设计文档同步
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 31s
CI / frontend (pull_request) Successful in 12s
CI / notify-on-failure (pull_request) Successful in 0s
Bug 1: spawn_full_agent use_main_session 返回 None 导致 mention 重复投递
- 根因: use_main_session=True 时 session_id=None, return None 被 ticker
  _process_posts 误判为 spawn 失败, 每次 tick 都重试
- 修复: 引入 effective_sid = session_id or 'main', 统一用于
  _register_session / _monitor_process / return value

Bug 2: _mark_task failed 时未检查已完成状态导致误发投递失败通知
- 根因: spawner 标 failed 和 handler 标 done 竞态条件下, 已完成的
  mail task 被误发投递失败通知
- 修复: notify_mail_failed 调用前加防御性检查, 若 task 已 done 则跳过

设计文档: §13 三个 handler sections 列表同步 DeliveryChecklistSection
  及 GiteaConventionSection / WikiGuideSection, 更新 section 复用分析表
  及文件结构 section 计数
2026-06-15 09:48:09 +08:00
pangtong-fujunshi 1f373d5cb5 Merge PR #79
Deploy / ci (push) Failing after 8s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-15 00:06:56 +00:00
cfdaily a8c9d25857 [moz] feat(prompt): L0~L2 prompt improvements
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 13s
CI / notify-on-failure (pull_request) Successful in 0s
- L0 wiki-rule: 扩充检索路径(practices/concepts/docs/design/)+ 检索方式(index→summary→grep→full)
- L1 SOUL.md: 同步测试 + PR 审查(代码改动检查设计文档+测试脚本,PR/CI/CD 三重把关)
- L1 AGENTS.md: 新增测试规范段(生产隔离/残留清理/测试开发分离)
- L2 prompt_composer: 新增 DeliveryChecklistSection(executor/mail/toolchain handler 注册)
- 456 passed, 0 failed
2026-06-15 08:04:42 +08:00
pangtong-fujunshi 660ac4b659 Merge PR #78: [moz] feat(frontend): 工具链面板加 from/to 显示 + 筛选 + 修复事件类型未知
Deploy / ci (push) Successful in 9s
Deploy / deploy (push) Successful in 12s
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-14 09:13:55 +00:00
cfdaily 91685ebfdd [moz] feat(frontend): 工具链面板加 from/to 显示 + 筛选 + 修复事件类型未知
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
- 前端:列表项加 from → to 标签(Agent 中文名)
- 前端:加「全部 / 未处理」筛选按钮
- 前端:详情区也显示 from → to
- 后端:ToolchainContextSection 修复事件类型 fallback 为中文标签
- 后端:加来源/指派信息到 prompt 消息体
2026-06-14 17:12:11 +08:00
pangtong-fujunshi 65910f5417 Merge PR #77: [moz] fix(api): list_tasks 默认排序改为 DESC
Deploy / ci (push) Successful in 10s
Deploy / deploy (push) Successful in 13s
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-14 08:55:14 +00:00
cfdaily 17b87290c8 [moz] fix(api): list_tasks 默认排序改为 created_at DESC(最新在前)
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 10s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-14 16:53:35 +08:00
pangtong-fujunshi bd5735f970 Merge PR #76: [moz] refactor(frontend): 工具链 Tab 移入系统设置子页签
Deploy / ci (push) Successful in 9s
Deploy / deploy (push) Successful in 13s
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-14 08:37:16 +00:00
cfdaily 05f9112fab [moz] refactor(frontend): 工具链 Tab 移入系统设置子页签
CI / lint (pull_request) Successful in 8s
CI / test (pull_request) Successful in 28s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-14 16:36:34 +08:00
jiangwei-infra b926b35703 Merge PR #75: [moz] fix(ci): 修复 deploy push trigger 不触发问题
Deploy / ci (push) Successful in 9s
Deploy / deploy (push) Successful in 13s
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-14 08:31:35 +00:00
jiangwei-infra 8df1d4a83c Merge branch 'main' into fix/cd-push-trigger-yaml
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 27s
CI / frontend (pull_request) Successful in 11s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-14 08:30:20 +00:00
pangtong-fujunshi ad34750075 Merge PR #74: [moz] ci: CI 管道新增 frontend build job 2026-06-14 08:14:15 +00:00
cfdaily cd7e24cd3c [moz] ci: CI 管道新增 frontend build job(tsc + vite build)
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 28s
CI / frontend (pull_request) Successful in 40s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-14 16:12:05 +08:00
27 changed files with 1915 additions and 25 deletions
+23 -3
View File
@@ -62,12 +62,30 @@ jobs:
(echo '=== RETRY WITH VERBOSE ===' && \
PYTHONPATH=$(pwd) /tmp/ci-venv-test/bin/pytest tests/ -m "not e2e" -x -v 2>&1 | tail -30)
# ── Job 3: CI 失败通知 ───────────────────────────────
# ── Job 3: Frontend Build ───────────────────────────
frontend:
runs-on: macos-arm64
needs: lint
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install & Build
run: |
cd src/frontend
npm ci || npm install
npm run build
# ── Job 4: CI 失败通知 ───────────────────────────────
# 使用 needs.<job>.result 直接判断,不查询 commit status API
# 根因:notify 自身的 pending status 会污染 commit status 查询结果(竞态条件)
notify-on-failure:
runs-on: macos-arm64
needs: [lint, test]
needs: [lint, test, frontend]
if: always()
steps:
- name: Check results and notify
@@ -75,12 +93,13 @@ jobs:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
LINT_RESULT: ${{ needs.lint.result }}
TEST_RESULT: ${{ needs.test.result }}
FRONTEND_RESULT: ${{ needs.frontend.result }}
run: |
echo "Lint result: $LINT_RESULT"
echo "Test result: $TEST_RESULT"
# 只有 lint 或 test 明确失败时才发通知
if [ "$LINT_RESULT" = "failure" ] || [ "$TEST_RESULT" = "failure" ]; then
if [ "$LINT_RESULT" = "failure" ] || [ "$TEST_RESULT" = "failure" ] || [ "$FRONTEND_RESULT" = "failure" ]; then
echo "CI has failures, sending notification..."
# 如果是 PR 事件,写评论通知
@@ -90,6 +109,7 @@ jobs:
FAILED_JOBS=""
[ "$LINT_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}lint "
[ "$TEST_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}test "
[ "$FRONTEND_RESULT" = "failure" ] && FAILED_JOBS="${FAILED_JOBS}frontend "
curl -sf -X POST \
-H "Authorization: token $GITEA_TOKEN" \
+251 -5
View File
@@ -1,10 +1,11 @@
---
title: "TaskTypeRegistry + Handler 架构重构"
created: 2026-06-10
version: v3.0
version: v3.1
---
> 状态: ✅ 已完成(Step 1-5 全部合并,394 passed
> v3.1 新增 §18Mail Handler Verify/Prompt 强化(2026-06-16,进行中)
# §1 现状分析(v3.0 更新说明:§1-§13 保留原样,新增 §14-§18,更新 §3/§5/§7
@@ -585,6 +586,18 @@ class PromptComposer:
| 50-59 | 硬约束 | 安全红线、禁止行为 |
| 60-69 | 扩展段 | 保留给未来使用 |
## 共性 Section(三 handler 共享)
以下三个 Section 在 `prompt_composer.py` 中统一定义,被 Task/Mail/Toolchain 三个 handler 共同注入:
| Section | priority | 用途 |
|---------|----------|------|
| `GiteaConventionSection` | 55 | Gitea Issue/PR 标题规范、分支命名、提交格式 |
| `DeliveryChecklistSection` | 55 | 交付前检查清单(产出格式、验证项、必读文档) |
| `WikiGuideSection` | 60 | Wiki 知识库检索指引(检索路径、优先级、知识缺口记录) |
设计意图:将跨 handler 的共性约束从各 handler 的 ConstraintsSection 中抽离,避免重复维护。
---
# §13 三个 Handler 的 Section 注册
@@ -601,6 +614,9 @@ def get_sections(self) -> list[PromptSection]:
RoleSkillSection(priority=30), # BootstrapBuilder 段 3Skill 全文)
TaskApiSection(priority=40), # API 操作指令,success_status="review"
TaskConstraintsSection(priority=50), # 硬约束
GiteaConventionSection(priority=55), # Gitea 协作规范(共性)
WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性)
DeliveryChecklistSection(priority=55), # 交付检查清单(共性)
]
```
@@ -611,6 +627,9 @@ def get_sections(self) -> list[PromptSection]:
| RoleSkillSection | BootstrapBuilder 段 3 | 个性:只有 task 读 Skill 全文 |
| TaskApiSection | spawner `_build_api_section` | **共性基础 + 个性参数**success_status |
| TaskConstraintsSection | BootstrapBuilder 段 4 | 个性:每种 task 约束不同 |
| GiteaConventionSection | prompt_composer.py | **共性**Gitea Issue/PR 规范 |
| WikiGuideSection | prompt_composer.py | **共性**Wiki 检索指引 |
| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 |
## MailHandler sections
@@ -620,6 +639,9 @@ def get_sections(self) -> list[PromptSection]:
MailContextSection(priority=10), # from/to/title/text,区分 inform/request
MailApiSection(priority=40), # API 操作指令,success_status="done"
MailConstraintsSection(priority=50), # 硬约束(禁止状态转换命令等)
GiteaConventionSection(priority=55), # Gitea 协作规范(共性)
WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性)
DeliveryChecklistSection(priority=55), # 交付检查清单(共性)
]
```
@@ -628,6 +650,9 @@ def get_sections(self) -> list[PromptSection]:
| MailContextSection | MAIL_INFORM_TEMPLATE / MAIL_REQUEST_TEMPLATE | 个性:邮件格式 |
| MailApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数**success_status="done",含 Mail API 指令) |
| MailConstraintsSection | 模板中的 ⚠️ 约束 | 个性 |
| GiteaConventionSection | prompt_composer.py | **共性**Gitea Issue/PR 规范 |
| WikiGuideSection | prompt_composer.py | **共性**Wiki 检索指引 |
| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 |
## ToolchainHandler sections
@@ -637,6 +662,9 @@ def get_sections(self) -> list[PromptSection]:
ToolchainContextSection(priority=10), # 事件类型 + 事件详情
ToolchainApiSection(priority=40), # API 操作指令,success_status="done"
ToolchainConstraintsSection(priority=50), # 硬约束
GiteaConventionSection(priority=55), # Gitea 协作规范(共性)
WikiGuideSection(priority=60), # Wiki 知识库检索指引(共性)
DeliveryChecklistSection(priority=55), # 交付检查清单(共性)
]
```
@@ -645,6 +673,9 @@ def get_sections(self) -> list[PromptSection]:
| ToolchainContextSection | toolchain_templates.py + md 文件 | 个性:事件格式 |
| ToolchainApiSection | spawner `_build_api_section` 变体 | **共性基础 + 个性参数** |
| ToolchainConstraintsSection | 新增 | 个性 |
| GiteaConventionSection | prompt_composer.py | **共性**Gitea Issue/PR 规范 |
| WikiGuideSection | prompt_composer.py | **共性**Wiki 检索指引 |
| DeliveryChecklistSection | prompt_composer.py | **共性**:交付前检查清单 |
## Section 复用分析
@@ -655,6 +686,9 @@ def get_sections(self) -> list[PromptSection]:
| *ConstraintsSection | ✅ | ✅ | ✅ | ❌ 约束内容不同,各自实现 |
| PriorOutputsSection | ✅ | ❌ | ❌ | 仅 task |
| RoleSkillSection | ✅ | ❌ | ❌ | 仅 task |
| GiteaConventionSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 |
| WikiGuideSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 |
| DeliveryChecklistSection | ✅ | ✅ | ✅ | **共性**:三 handler 共享,prompt_composer.py 定义 |
**结论**ApiSection 可以抽一个 BaseApiSectioncurl 模板 + success_status 参数),其余 section 各自实现。
@@ -667,9 +701,9 @@ src/daemon/
├── task_type_registry.py # §3 + §4Protocol + Registry
├── prompt_composer.py # §12 PromptSection + PromptContext + PromptComposer
├── base_task_handler.py # §16 BaseTaskHandler 基类
├── task_handler.py # §13 TaskHandler(继承 BaseTaskHandler+ 5 sections
├── mail_handler.py # §13 MailHandler(继承 BaseTaskHandler+ 3 sections
├── toolchain_handler.py # §13 ToolchainHandler(继承 BaseTaskHandler+ 3 sections
├── task_handler.py # §13 TaskHandler(继承 BaseTaskHandler+ 8 sections
├── mail_handler.py # §13 MailHandler(继承 BaseTaskHandler+ 6 sections
├── toolchain_handler.py # §13 ToolchainHandler(继承 BaseTaskHandler+ 6 sections
├── dispatcher.py # §6 改动
├── spawner.py # §6 改动
├── ticker.py # §6 改动
@@ -952,7 +986,219 @@ handler.post_complete(task_id, agent_id, outcome, db_path)
---
## §14. Mail 失败通知机制
## §18. Mail Handler Verify/Prompt 强化
> 日期:2026-06-16 | 作者:庞统 | 状态:方向 1-5 全部已确认
## 18.1 问题背景
### 触发事件
2026-06-12 daemon 重启后,_mail DB 中积压的 E2E 测试遗留邮件(5/18~6/1 创建,type=requestperformative="text")被 dispatch 给 agent。agent 正常处理并输出文本(如"已阅,无需处理"),但 `verify_completion` 判定 no_reply → 标 failed → 触发 `notify_mail_failed` → 产生 38 封 `[投递失败]` 通知邮件,每 ~2.5 分钟一轮,持续 10 轮。
### 根因链
```
E2E 测试脚本 bugtype="text"
→ mail_routes.py 不校验 type 值,直接透传
→ performative="text" ≠ "inform" → 走 _check_reply
→ _check_reply 查 in_reply_to taskagent 没用 Mail API 回复
→ verify 失败 → on_failure 标 failed
→ notify_mail_failed 发 [投递失败] 通知
→ 通知本身也是 task,循环触发
```
### 三种 handler verify 对比
| 维度 | TaskHandler | MailHandler | ToolchainHandler |
|------|------------|-------------|------------------|
| verify 信号 | output / comment(≥50字) / terminal_status(三信号) | in_reply_to task(单信号) | action_report / output / comment(≥20字)(三层 fallback |
| inform 处理 | N/A | 直接通过(不检查执行证据) | N/A |
| verify 失败后 | **留 working**(覆盖 post_complete | **标 failed**base post_complete + mail on_failure | 标 failedbase post_complete + tc on_failure |
| agent 输出持久化 | 靠 agent 主动 POST output/comment | **无**agent 输出只在内存) | 靠 agent 主动 POST action_report |
**关键发现**
1. MailHandler 继承 BaseTaskHandler,未覆盖 `post_complete` → verify 失败时走 base 的 `on_failure` → 标 failed
2. TaskHandler 覆盖了 `post_complete` → verify 失败时留 working,让 ticker 重试
3. MailHandler 的 verify 只有 `in_reply_to` 一条路径,没有 fallback
4. inform 类型直接通过(`VerifyResult(True)`),不检查任何执行证据——inform 是"无需回复"不是"无需检查"
5. E2E 测试用 `TestClient(app)` 写生产 `_mail DB`,且测试脚本用了非标准 `type="text"`
## 18.2 修复方向
### 方向 1mail verify 对齐 toolchain 模式(✅ 已确认)
**问题**mail verify 只有 in_reply_to task 一条路径。task/toolchain 都有多层 fallbackoutputs / comments)。
**方案**mail 对齐 toolchain 模式——prompt 加 action report 要求,verify 优先查 action_report → fallback outputs → fallback comments。in_reply_to 回复邮件从唯一信号降为 request 类型的第 4 优先级信号。
#### prompt 强化(MailApiSection
参照 ToolchainApiSection,在 mail prompt 中追加 action report 要求:
```
### 完成后必须提交 action report
执行完邮件处理后,必须提交 action report
curl -s -X POST "http://localhost:8083/api/projects/_mail/tasks/{task_id}/comments" \
-H "Content-Type: application/json" \
-d '{"author": "{agent_id}", "comment_type": "action_report", "body": "处理结果摘要"}'
⚠️ 不提交 action report 的任务会被标记为 failed。
```
#### verify 改造(MailHandler.verify_completion
```python
def verify_completion(self, task_id, db_path) -> VerifyResult:
performative = self._parse_performative(task_id, db_path)
# 1. 优先检查 action_report comment(所有类型通用)
if self._has_action_report(task_id, db_path):
return VerifyResult(True, "has_action_report", "action_report found")
# 2. fallback: outputs
if self._has_outputs(task_id, db_path):
return VerifyResult(True, "has_output", f"output_count={count}")
# 3. fallback: 有实质内容的 comment(≥20字,非 system
if self._has_comment(task_id, db_path):
return VerifyResult(True, "has_comment", f"comment_count={count}")
# 4. request 特有:检查 in_reply_to 回复邮件
if performative == "request":
if self._check_reply(task_id, db_path):
return VerifyResult(True, "has_reply", "in_reply_to found")
return VerifyResult(False, "no_action",
"no action_report, no output, no comment, no reply")
```
注意:action_report 提交到 moziplus DBcomments 表),不是 Gitea。Gitea comment 是跨 agent 协作用的,不是 verify 检查的依据。
### 方向 2:prompt 约束强化(✅ 已确认)
**问题**:当前 mail prompt 只给了 curl 示例,没有硬约束要求 agent 必须输出处理结果。agent 判断"已阅"后直接跳过,不创建 in_reply_to task。
**方案**mail request/inform prompt 加 JSON 输出约束(参考 toolchain 的 Red Flags 模式)。
#### MailContextSection 强化
**request 类型**追加:
```
### 输出要求
- 你的回复必须包含对邮件的实际处理结果
- 如果是第一次收到:正常处理,输出处理结果
- 如果是重复邮件(你之前处理过相同 ID 的邮件):输出"此前已处理" + 之前的处理结果摘要
- ⚠️ "已阅""无需处理"不是有效处理结果
```
**inform 类型**追加:
```
### 输出要求
- 你的回复必须确认已处理(读取/执行/记录),不能只说"已阅"
- 如果是重复邮件:输出"此前已处理" + 处理结果摘要
- ⚠️ "已阅"不是有效输出
```
**MailConstraintsSection** 追加 Red Flags
```
| Agent 想法 | Red Flag 驳回 |
|------------|--------------|
| "已阅即可" | ❌ 错!必须输出处理结果或确认执行 |
| "重复邮件忽略" | ❌ 错!输出"此前已处理" + 结果摘要 |
| "无需回复" | ❌ 错!request 必须回复,inform 必须确认处理 |
```
### 方向 3:inform 也要检查执行证据(✅ 已确认)
**问题**:当前 inform verify 直接返回 `VerifyResult(True)`,不检查任何执行证据。inform 是"无需回复"不是"无需检查"。
**方案**inform verify 改为检查 agent 是否有实质输出(comment/output),和 request 走不同的验证路径但都需要验证。
**改动文件**`src/daemon/mail_handler.py` `verify_completion` 方法
### 方向 4verify 失败保持 working(✅ 已确认)
**问题**MailHandler 继承 BaseTaskHandlerverify 失败时走 base 的 `on_failure` → 标 failed。而 TaskHandler 覆盖了 `post_complete`verify 失败时留 working。
**原始设计意图**(§2 设计文档):"不通过 → 留 workingticker 重查(最多 3 次,然后标 failed"。
**方案**MailHandler 覆盖 `post_complete`verify 失败时不标 failed,保持 working。ticker 的 `_check_timeouts` 超时兜底:
- `check_completion` 通过(有回复)→ done
- `check_completion` 不通过 → 超时后标 failed
- Runaway Guard(§15 dispatch_count ≥ 10)兜底防止无限循环
**改动文件**`src/daemon/mail_handler.py`,新增 `post_complete` 覆盖
### 方向 5type 校验 + E2E 修复 + DB 清理(✅ 已确认)
#### 5.1 mail_routes.py type 校验
**问题**`mail_type = body.get("type")` 直接透传,传什么存什么。`"text"` 不是标准值。
**方案**:创建时校验 type 只允许 `inform` / `request`,非法值默认 `request`。
```python
mail_type = body.get("type")
if mail_type is None:
mail_type = "inform" if in_reply_to else "request"
elif mail_type not in ("inform", "request"):
# 非标准值,校正为默认值
mail_type = "inform" if in_reply_to else "request"
```
**改动文件**`src/api/mail_routes.py`
#### 5.2 _parse_performative 容错
**问题**`meta.get("performative", meta.get("type", "request"))` 当 performative="text" 时返回 "text",不等于 "inform" → 走 _check_reply。
**方案**:只认 `inform` 和 `request` 两个值,其他一律当 `request`。
```python
def _parse_performative(self, task_id, db_path) -> str:
raw = meta.get("performative", meta.get("type", "request"))
if raw == "inform":
return "inform"
return "request" # 非标准值一律当 request
```
**改动文件**`src/daemon/mail_handler.py` `_parse_performative` 方法
#### 5.3 E2E 测试修复
**问题**`tests/e2e/test_e2e_v27.py` 用 `type="text"` 创建测试邮件,且用 `TestClient(app)` 写生产 `_mail DB`。
**修复**
1. `type="text"` 全部改为 `type="inform"` 或 `type="request"`
2. E2E 测试跑完后清理测试邮件(`mail_ids` 列表中记录的 task
**改动文件**`tests/e2e/test_e2e_v27.py`
#### 5.4 生产 DB 清理
**问题**:生产 `_mail DB` 中残留大量 E2E 测试邮件(5/18~6/3 创建的"筛选测试""详情测试""已读测试""任务分配"等)。
**方案**:手动清理这些测试残留(一次性操作,不需要代码改动)。
## 18.3 影响范围
| 文件 | 改动类型 | 影响面 |
|------|---------|--------|
| `src/daemon/mail_handler.py` | verify + post_complete + prompt section | MailHandler 核心逻辑 |
| `src/api/mail_routes.py` | type 校验 | Mail API 创建入口 |
| `tests/e2e/test_e2e_v27.py` | type 值修正 + 清理 | E2E 测试 |
| 生产 `_mail DB` | 清理测试残留 | 一次性操作 |
## 18.4 验证计划
1. 单元测试:mail_handler verify/prompt 变更
2. 集成测试:mail dispatch → verify → done/working 全链路
3. 回归测试:`pytest -m "not e2e"` 全量
4. 手工验证:创建 inform/request 邮件,确认 verify 行为正确
---
# §14. Mail 失败通知机制
### 20.1 背景
+61
View File
@@ -0,0 +1,61 @@
# §15 Runaway Guard — Per-Task Dispatch 上限
> 设计文档 v1.0 | 2026-06-16
## 问题
mail/toolchain task 走 handler auto-working(跳过 claim 阶段),不受 claim_timeout 的 3 次重试兜底保护。如果一个 auto-working task 反复 spawn 但永远到不了 done/failed,会无限循环消耗资源。
### 实际案例
2026-06-15 mention 重复投递事件:`spawn_full_agent``use_main_session=True` 时返回 `None`ticker `_process_mentions` 误判为失败,每次 tick(30s)都重试。同一 mention 投递了 4 次,直到 retry_count 达到 mention_queue 的 5 次上限才停止。
直接根因已由 PR #80 修复,但如果类似 bug 再次出现,当前没有任何机制阻止 task 层面的无限循环。
## 设计
### 机制
tasks 表新增 `dispatch_count` 字段,每次 ticker 成功 dispatch 一个 task 时递增。当 `dispatch_count >= 10`(全局默认)时,自动标 failed。
### 默认值选择
全局默认 10 次。参考 Hermes v0.13 Best Practices §3 "Per-Task 重试上限"
- 简单任务重试 1 次
- 复杂任务重试 3 次
- crash recovery3 次)+ api_retry3 次)余量 = ~10 次
### 适用范围
所有 task 类型(task/mail/toolchain),所有非终态(pending/working/claimed)。
### 检查时机
`_check_timeouts` 方法开头,先于现有的 claimed/working 超时检查执行。
### 与现有机制的关系
| 机制 | 覆盖场景 | 触发动作 |
|------|---------|---------|
| claim_timeout retry_count >= 3 | 广播任务无人认领 | 升级庞统 |
| crash_limit 3/30min | working 状态 crash | 标 failed |
| api_retry_count | API 连续失败 | 标 failed |
| 续杯 max_retries 3 | 续杯耗尽 | 标 failed |
| working timeout | working 超时 | 标 failed 或 done |
| **runaway_guard 10 次** | **任何状态的无限循环** | **标 failed** |
runaway_guard 是最后一道防线,覆盖所有其他机制遗漏的循环场景。
## 改动文件
| 文件 | 改动 |
|------|------|
| `src/blackboard/db.py` | `_safe_add_column(conn, "tasks", "dispatch_count", "INTEGER DEFAULT 0")` |
| `src/blackboard/models.py` | Task dataclass 加 `dispatch_count: int = 0` |
| `src/daemon/ticker.py` | `_dispatch_pending` / `_dispatch_reviews` 递增 dispatch_count`_check_timeouts` 加 runaway guard 检查 |
## 参考
- Hermes v0.13 Kanban Best Practices §3 "Per-Task 重试上限"
- 实际案例:2026-06-15 mention 重复投递事件(PR #80 修复了直接根因,runaway guard 作为兜底)
@@ -0,0 +1,663 @@
# §19 Skill 生命周期管理 + 经验闭环四阶段设计
> 作者:庞统士元
> 日期:2026-06-18v2.0
> 状态:方案待确认
> 前置:§14 Task 五层架构、§16 知识注入四层体系
## 变更摘要(v2.0
| 变更项 | 原设计 | 新设计 | 理由 |
|--------|--------|--------|------|
| 蒸馏频率 | 庞统每天一次 | **双层 daily**:各 agent 03:00 自蒸馏 + 庞统 05:00 整合 | agent 是自己经验的最佳蒸馏者;庞统负责跨 agent 整合 |
| 蒸馏者 | 庞统一人 | **双层**L1 各 agent + L2 庞统 | 消除蒸馏者偏差;经验是 per-agent 的 |
| .learnings/ | DISCOVER 数据源之一 | **废弃**。JSONL 是唯一数据源 | 信息冗余;agent 执行中不应分心写 .learnings/ |
| 三重验证 | 跨任务复现 + 生成力 + 排他性 | **Recurrence-Count 机制**(融合 self-improvement skill | ≥3 次自动触发提升,比主观判断更客观 |
| Skill 数量 | 未明确 | **一个 skillskill-management** + references/ 四阶段 | 减少上下文开销;DISCOVER/IMPROVE 是 cron 场景不需要独立 skill description |
| self-improvement skill | 未提及 | **废弃**。优势融合到 DISCOVER 输出格式 | 职责重叠;统一为单一闭环 |
| Skill 存放 | 未区分 | **per-agent 目录 + 公共目录** | agent 专属经验不污染其他 agent 上下文 |
## 1. 背景
moziplus v2.0 的 P4 剩余两项:
- T7 C3Skill 生命周期管理(draft → active → deprecated
- T7 C5:经验闭环 IMPROVE 阶段(DISCOVER → DISTILL → APPLY → IMPROVE 中的最后一步)
### 当前实现状态
| 组件 | 状态 | 问题 |
|------|------|------|
| `SkillRegistry`skill_system.py | 死代码 | 只有 register/match 方法,从未被外部调用 |
| `SkillExecutor`skill_system.py | 死代码 | 从未被外部调用 |
| `ExperienceDistiller`experience.py | 空转 | ticker 调用时没传 review_result 和 outputs,蒸馏函数收到 None 直接返回空 |
| `ExperienceStore`experience.py | 空转 | experiences 目录全部为空 |
| `experiences` 表(db.py | 未使用 | 代码用 jsonl 文件不用 DB 表 |
| Skill 生命周期 | 缺失 | 只有 enabled bool,无 draft/active/deprecated 状态流转 |
**结论**:现有的 experience.py 和 skill_system.py 需要重新设计,不是修补能解决的。
### 实际运行的知识体系
实际的 Skill 发现和加载走的是 **openclaw 原生 skill 机制**
- openclaw 扫描 skills 目录 → 生成 `<available_skills>` 列表注入 system prompt
- Agent 按 description 匹配 → `read` SKILL.md → 按内容执行
- moziplus 的 SkillRegistry/SkillExecutor 完全不参与
因此本设计**不重建 moziplus 的 skill 引擎**,而是基于 openclaw 原生机制构建。
## 2. 设计目标
1. 经验从「发现→蒸馏→应用→改善」形成完整闭环
2. Skill 有明确的生命周期管理(draft → active → deprecated
3. 产物统一为 Skill,不再有 experiences.jsonl / .learnings/ 等中间形态散落各处
4. 追踪 Skill 引用情况,支撑淘汰决策
5. 充分利用 openclaw 已有的 skill_workshop 工具和 skill 加载机制
6. **每个 agent 是自己经验的最佳蒸馏者**——经验 per-agent,精益求精
## 3. 核心设计决策
| # | 决策 | 理由 | 参考 |
|---|------|------|------|
| D1 | 统一产物:Skill-only | 不再有中间形态散落各处。Hermes 只有 Skill + Memory 两种载体,没有第三种 | Hermes skill_manage + memory_tool |
| D2 | 生命周期通过 skill_workshop 管理 | OpenClaw 已有 pending → applied → rejected → quarantined 生命周期 | OpenClaw skill_workshop 工具 |
| D3 | 蒸馏频率:双层 daily | L1 各 agent 每天 03:00 自蒸馏;L2 庞统每天 05:00 整合。有距离感的蒸馏优于即时记录 | self-improvement skill daily review;主公确认 |
| D4 | 蒸馏者:双层(各 agent + 庞统) | L1 每个 agent 蒸馏自己的经验(自己最准);L2 庞统负责跨 agent 共性识别 + draft 审查 | Hermes skill_manage:「每个将军都应建立自己的 Skill 库」 |
| D5 | 二级蒸馏抽象为根因模式 | 不固化在特定技术细节。description 描述「问题模式」而非「技术症状」 | Superpowers writing-skillsdescription = when not how |
| D6 | 废弃 .learnings/ 作为数据源 | JSONL 已包含完整信息(工具调用、推理过程、错误输出)。.learnings/ 只是重复抄写,且打断 agent 执行流 | DISCOVER 统一采集;主公确认 |
| D7 | 只创建一个 skillskill-management | 四阶段的详细操作放到 references/ 目录。DISCOVER/IMPROVE 是 cron 场景不需要独立 skill description 常驻上下文 | moziplus skill-engineering practices §4:组合模式 |
## 4. L4 知识层:Skill Workshop
在现有 L0-L3 四层知识体系上新增 L4:
| 层级 | 名称 | 内容 | 加载方式 | 已有 |
|------|------|------|---------|------|
| L0 | 注入式上下文 | MEMORY.md / TOOLS.md | 每次 session 启动 | ✅ |
| L1 | 确定性规则 | SOUL.md / AGENTS.md | 每次 session 启动 | ✅ |
| L2 | 任务上下文 | BootstrapBuilder PromptSection | 按 task_type 注入 | ✅ |
| L3 | 按需 Skill | openclaw skills | description 匹配 → agent read | ✅ |
| **L4** | **Skill 生命周期** | **skill_workshop** | **draft → active → deprecated 管理** | **新增** |
L4 不是一个 prompt 层,而是 Skill 的**管理层**——负责 Skill 的创建、验证、应用、追踪、淘汰。
## 5. DISCOVER 阶段(双层)
### 5.1 L1 各 agent 自蒸馏(每天 03:00
每个 agent 的 cron 扫描**自己当天**的 session JSONL,识别信号,蒸馏为 draft proposal。
**数据源(1 个)**
| 数据源 | 位置 | 包含什么 |
|--------|------|---------|
| 自己的 Session JSONL | ~/.openclaw/agents/<agent_id>/sessions/*.jsonl | 当天完整思考过程、工具调用、错误恢复、用户对话 |
**不需要**扫描黑板/Gitea/Mail 等——那是 L2 庞统的职责。L1 聚焦自己的经验。
**信号识别(5 类高价值信号)**
| 信号类型 | 从哪发现 | 识别特征 |
|---------|---------|---------|
| 失败模式 | task failed、CI failed、review rejected | 有明确的失败原因 |
| 重复问题 | 跨多个任务出现同类问题 | 同关键词出现 ≥2 次 |
| 决策转折 | rebuttal comment、需求澄清、主公纠正 | 原方向被推翻或修正 |
| 新实践 | 设计文档新增、wiki-vault 新页面 | 之前没有的知识 |
| 知识缺口 | agent 表达不确定、查不到的东西 | 查不到/不确定的东西 |
**输出**draft skill proposal(提交到 skill_workshoppending 状态)
### 5.2 L2 庞统整合(每天 05:00
庞统的 cron 在 L1 全部完成后执行,扫描全量数据源 + 审查所有 L1 draft proposal。
**数据源(全量)**
| 数据源 | 位置 | 包含什么 |
|--------|------|---------|
| 黑板 tasks | 各项目 blackboard.db | 任务生命周期:创建、分配、执行、完成/失败 |
| 黑板 reviews | reviews 表 | 审查结论 + 逐步骤 verdict + suggestions |
| 黑板 comments | comments 表 | @mention 讨论、rebuttal 推理、action_report |
| 黑板 outputs | outputs 表 | 任务产出物内容 |
| 黑板 events | events 表 | 状态变更、guardrail 拦截、异常检测 |
| Gitea Issues/PRs | Gitea API | 问题报告、diff、review 评论 |
| Gitea CI | Gitea Actions | lint/test/build 成功/失败 |
| Mail | mail API | 跨 agent 通信、讨论推理过程 |
| **所有 agent 的 Session JSONL** | ~/.openclaw/agents/*/sessions/ | 全团队完整思考过程 |
| MEMORY.md | 各 agent workspace | 长期记忆、已有经验教训 |
| knowledge-gaps.md | wiki-vault/_meta/ | 知识缺口 |
| **L1 draft proposals** | skill_workshop pending | 各 agent 当天提交的 draft |
**核心职责**
a. **跨 agent 共性模式识别**:张飞和关羽都在类似场景踩坑 → 合并为共享 Skill
b. **审查 L1 draft proposals**
- APPROVE:质量达标的个人经验 → 变 active(仅作者 agent 可见)
- MERGE:跨 agent 共性 → 合并为共享 Skill(所有 agent 可见)
- REJECT:质量不够(附原因,agent 看到反馈后改进)
c. **全局提升**:高确定性/高频率经验 → 提升到 AGENTS.md 规则(所有 agent 强制注入)
### 5.3 去重
同一事件在多个数据源出现(CI 失败 → toolchain task → mail → comment 讨论),按时间窗口 + 关键词去重,保留信息量最大的那条。
跨 agent 的同一模式,按 Pattern-Key 去重,合并为共享信号。
### 5.4 输出格式(融合 self-improvement skill 结构化字段)
每条候选信号包含:
```
信号类型 | 来源(task_id / PR / review / session| 时间 | 简述(≤100 字)
ID: SIG-YYYYMMDD-XXX
Priority: low | medium | high | critical
Status: pending | in_progress | resolved | promoted
See Also: SIG-YYYYMMDD-XXX(关联信号)
Recurrence-Count: N(同一模式出现次数)
Pattern-Key: category.subcategory(稳定去重键,如 sync.field_mapping
```
**字段说明**(汲取自 self-improvement skill):
| 字段 | 用途 | 借鉴来源 |
|------|------|---------|
| ID | 唯一标识,便于交叉引用 | self-improvement logging format |
| Priority | 优先级排序,critical/high 优先处理 | self-improvement priority guidelines |
| Status | 生命周期跟踪 | self-improvement status lifecycle |
| See Also | 关联相似信号,发现共性模式 | self-improvement recurring pattern detection |
| Recurrence-Count | 同一模式出现次数,≥3 触发自动提升 | self-improvement recurring pattern + Skill Extraction Criteria |
| Pattern-Key | 稳定去重键,跨 agent 匹配同一模式 | self-improvement Pattern-Key |
## 6. DISTILL 阶段
### 6.1 核心原则:HOW not WHAT
蒸馏的是「怎么做」不是「发生了什么」(nuwa-skill 实践 #5):
```
❌ "PR #83 修复了 event_type 未知的问题"
→ 这是 WHAT,无法复用
✅ "数据消费者与数据生产者解耦时,新增字段必须同步所有生产者的提取逻辑"
→ 这是 HOW,可复用到任何消费者/生产者场景
```
### 6.2 蒸馏产物 = Skill
直接产出 SKILL.md 格式或对现有 Skill 的 patch,提交到 skill_workshop。
**SKILL.md 编写规范**(参考 Superpowers writing-skills):
```yaml
---
name: skill-name
description: Use when [触发条件/问题模式描述],不描述工作流
---
# Skill 标题
## 什么时候用
(具体的触发场景,按问题模式描述,不按技术特定症状)
## 怎么做
(根因分析 + 操作步骤)
## 常见错误
(反模式:什么不该做)
## 来源
evidence:哪些 task/PR/review 提炼了这条经验)
```
**description 关键规则**Superpowers 的核心发现):
- 只描述触发条件(when to use),**绝不描述工作流**how)
- 以「Use when...」开头
- 描述问题模式,不描述技术特定症状
- 原因:测试发现 description 如果总结了工作流,agent 会按 description 执行而跳过读完整 SKILL.md
### 6.3 蒸馏示例
**一级蒸馏**(从具体案例提取):
```yaml
# 案例 1PromptContext event_type 未知
# 案例 2PromptContext from_agent/mail_type 缺失(PR #26 D2
→ 共同根因:消费者/生产者字段同步问题
# 蒸馏为 Skill section(加到 trial-and-error-patterns):
## 消费者/生产者字段同步
**什么时候用**:修改 dataclass 时,如果该 dataclass 由外部 JSON 提取填充
**怎么做**
1. 改 dataclass 定义
2. 检查所有从 JSON 提取字段的代码路径,同步新增提取逻辑
3. 检查所有构造该 dataclass 的调用点,同步新增参数
4. 跑一次构建测试验证字段不为空
**常见错误**:只改 dataclass 不改提取逻辑 → 字段默认值为空 → 运行时不报错但行为异常
```
**二级蒸馏**(从多个一级经验提取通用模式):
如果「消费者/生产者字段同步」经验在 ≥2 个不同场景复现(PromptContext + 其他),验证通过后,可以提升为独立 Skill 或固化到 AGENTS.md 规则。
### 6.4 验证机制(融合 self-improvement Recurrence-Count + Skill Extraction Criteria
从 draft → active 的验证标准:
| 验证维度 | 标准 | 不通过的处理 |
|---------|------|------------|
| Recurrence-Count ≥ 2 | 同一 Pattern-Key 在 ≥2 个不同场景出现过 | 降级为 MEMORY.md 临时记录 |
| 有生成力 | 能给出具体的操作指引 | 丢弃 |
| 有排他性 | 不是「代码要测试」的常识 | 丢弃 |
**提升触发条件**(从 draft 提升为 active Skill,融合 self-improvement Skill Extraction Criteria):
全部满足时触发提升:
- Recurrence-Count ≥ 3(同一模式 30 天内出现 3 次以上)
- 跨 ≥2 个不同任务验证
**时间窗口**Recurrence-Count 以 30 天为窗口,超过 30 天的记录不计入。6 个月内 3 次 vs 1 周内 3 次信号强度不同,30 天窗口确保经验仍然新鲜。
**Skill Extraction 质量 Gate**(汲取自 self-improvement skill):
| 标准 | 描述 |
|------|------|
| Recurring | 有 See Also 链接到 2+ 个相似信号 |
| Verified | Status 是 resolved 且有工作修复 |
| Non-obvious | 需要实际调试才能发现(不是常识) |
| Broadly applicable | 不是项目特定,可跨场景复用 |
### 6.5 质量检查自动化
参考 nuwa-skill quality_check.py,对蒸馏产出做结构化检查:
| 检查项 | 标准 |
|--------|------|
| trigger 是否具体 | 不是「注意代码质量」这种泛泛而谈 |
| action 是否可执行 | 不是「要小心」这种无操作指引 |
| 是否与已有 Skill 重复 | 检查现有 skills 目录中是否已有覆盖 |
| description 是否只含触发条件 | 不包含工作流描述 |
### 6.6 矛盾处理(nuwa-skill 实践 #10
新经验与已有经验冲突时:
- **时间性矛盾**(观点演化)→ 记录演化轨迹,以近期为主
- **领域性矛盾**(不同场景不同规则)→ 分场景记录
- **本质性张力**(价值观内在冲突)→ 标注为「核心张力」,两个版本都保留
**矛盾是特征,不是 Bug。** 强制调和会丢失关键信号。
### 6.7 蒸馏者(双层)
**L1:每个 agent 自己(每天 03:00 cron,各 agent 错开 15 分钟避免资源争用:03:00, 03:15, 03:30, ...**
1. 扫描自己的 session JSONL
2. 用判断力提取根因模式(不是机械提取)
3. 按 SKILL.md 格式产出
4. 提交到 skill_workshoppending proposal
**L2:庞统(每天 05:00 cron**
1. 审查所有 agent 提交的 draft proposalapprove / merge / reject
2. 跨 agent 共性模式识别和合并
3. 高频/高确定性经验提升到 AGENTS.md 规则
未来考虑半自动化(LLM 辅助草案 + agent 审阅确认)。
## 7. APPLY 阶段
### 7.1 统一走 openclaw skill 机制
**不新建 ExperienceSection 或任何 moziplus 自定义注入**。因为产物统一为 Skill,openclaw 已有的机制天然支持:
1. openclaw 扫描 skills 目录 → 生成 `<available_skills>` 列表
2. Agent 按 description 匹配 → `read` SKILL.md
3. Agent 按内容执行
### 7.2 Skill description 编写规范
这是 APPLY 阶段效果好坏的关键。参考 Superpowers writing-skills 的核心发现:
```yaml
# ❌ BAD:描述了工作流,agent 会按 description 执行而跳过读 SKILL.md
description: Use when modifying dataclass — checks all extraction points, runs tests, verifies non-null fields
# ✅ GOOD:只描述触发条件
description: Use when modifying a dataclass that is populated from JSON extraction by another module
# ❌ BAD:太抽象
description: Use for code quality
# ✅ GOOD:描述问题模式
description: Use when a field added to a dataclass appears empty or as default value at runtime
```
### 7.3 渐进式加载
openclaw 已有的机制:
- L1`<available_skills>` 列表(~100 token/skill,只有 name + description
- L2Agent `read` SKILL.md(完整内容)
- L3SKILL.md 内引用的 references/ 文件(按需加载)
### 7.4 Skill 存放位置与可见性
agent 专属经验放到 agent 自己的 workspace skills 目录,全局共享 Skill 放到公共 skills 目录。openclaw 扫描时自动合并。
| Skill 位置 | 谁能看到 | 适用场景 |
|-----------|---------|---------|
| `~/.openclaw/workspace-zhangfei/skills/` | 只有张飞 | 编码模式、个人踩坑经验 |
| `~/.openclaw/workspace-pangtong/skills/` | 只有庞统 | 规划经验、方向把控 |
| `~/.openclaw/workspace-simayi/skills/` | 只有司马懿 | 审查技巧、挑战模式 |
| `~/.sanguo_projects/sanguo_mozi/skills/` | 所有 moziplus agent | 团队共识、协作规范、通用实践 |
**设计原则**
- 个人经验不污染其他 agent 上下文(张飞的编码坑不需要司马懿看到)
- 共性经验自动共享(庞统 MERGE 后放到公共目录)
- openclaw 原生机制天然支持(扫描时合并所有 skills 目录)
## 8. IMPROVE 阶段
### 8.1 Skill 自我修补
参考 Hermes skill_manage 的设计哲学:
> "If you used a skill and hit issues not covered by it, patch it immediately."
> "Skills that aren't maintained become liabilities."
Agent 使用 Skill 时发现问题(缺步骤、过时信息、命令变更)→ 立即通过 skill_workshop 提交 revise proposalpatch)。
这不需要定时任务,靠 agent 的主动维护。关键是在 agent 的 prompt 中注入这条规则(SOUL.md 或 AGENTS.md)。
### 8.2 引用追踪
**设计原则**:不追求精确归因,做时间维度的信号采集。
| 信号 | 采集方式 | 可信度 |
|------|---------|--------|
| Skill 最近被 read 的时间 | 扫描 session JSONL 中 `"tool":"read"` + SKILL.md 路径 | 中 |
| Skill 在 available_skills 中被注入 | 扫描 JSONL 中 available_skills 列表 | 中(被注入但未必被用) |
| Agent 在输出中提及了 skill name | grep skill name in assistant messages | 高(主动提到说明确实用了) |
| Skill 文件最近修改时间 | git log / 文件 mtime | 高 |
**采集频率**:每周一次 cron,扫描过去 7 天的所有 session JSONL。
### 8.3 淘汰机制
**决策流程**
```
30 天无引用信号
→ 生成淘汰候选报告(庞统审阅)
→ 确认淘汰 → skill_workshop quarantine
→ 保留观察 → 标注,下轮再查
→ 更新后保留 → 修改 description / 内容,重置计时
```
**注意**openclaw 本身的 skill~/.openclaw/plugin-skills/ 和全局 skills)也纳入追踪范围。主公可以据此决定哪些 openclaw skill 可以禁用。
### 8.4 经验提升路径
同一 Skill section 被频繁引用(≥5 次)且经过多次验证 → 考虑提升:
| 提升目标 | 条件 | 效果 |
|---------|------|------|
| 独立 Skill | 足够通用,有自己的触发条件 | 独立 SKILL.mddescription 匹配 |
| AGENTS.md 规则 | 确定性高,适用于所有 agent | L1 确定性注入,强制生效 |
| guardrail | 安全相关,不可违反 | 强制检查 |
### 8.5 反馈到 DISCOVER
IMPROVE 发现的经验缺口(「这条 Skill 不适用 XXX 场景」)→ 写入 knowledge-gaps.md → 成为下一轮 DISCOVER L2 的输入。
## 9. 闭环全景
```
DISCOVER L1(每天 03:00,各 agent cron
数据源:自己的 session JSONL
信号识别:5 类高价值信号
输出:draft skill proposalstructured,带 ID/Priority/Pattern-Key/Recurrence-Count
DISCOVER L2(每天 05:00,庞统 cron
数据源:全量 12 个数据源(含 L1 draft proposals
跨 agent 共性模式识别
审查 draft proposalsapprove / merge / reject
DISTILLL2 庞统执行)
原则:HOW not WHAT(根因模式,不固化技术细节)
验证:Recurrence-Count ≥ 2 + 生成力 + 排他性
提升:Recurrence-Count ≥ 3 → 独立 Skill / AGENTS.md 规则
质量:自动化检查 + 矛盾保留
产物:Skill(通过 skill_workshop 管理)
APPLY(实时,openclaw skill 机制)
匹配:description 匹配 → agent read SKILL.md
执行:agent 按内容执行
自我修补:使用时发现问题 → 立即 revise proposal
per-agent 隔离:专属 Skill 在 agent workspace,共享 Skill 在公共目录
IMPROVE(每周 cron,庞统执行)
追踪:scan JSONL 引用信号
淘汰:30天无引用 → 庞统审查 → quarantine
提升:高频引用 → 独立 Skill / AGENTS.md 规则 / guardrail
反馈:知识缺口 → knowledge-gaps.md → 回到 DISCOVER L2
```
## 10. 与现有实现的关系
| 组件 | 处理方式 |
|------|---------|
| `skill_system.py`SkillRegistry/SkillExecutor | **标记 deprecated,后续清理。** 死代码,实际不参与 skill 发现/加载 |
| `experience.py`ExperienceDistiller/ExperienceStore | **标记 deprecated,后续清理。** 空转代码,experiences 目录全空 |
| `experiences` 表 / `experience_tags` 表(db.py | **保留表结构但不再写入。** 未来如果需要 DB 查询可以重新启用 |
| ticker.py:336-348 经验蒸馏逻辑 | **移除。** 不再逐任务蒸馏,改为双层 daily cron |
| `skill_workshop` 工具 | **核心使用。** 所有 Skill 生命周期通过它管理 |
| openclaw `<available_skills>` 机制 | **核心依赖。** APPLY 阶段完全基于此 |
| **self-improvement skill**`~/.openclaw/workspace/skills/self-improving-agent/` | **废弃。** 其优势(结构化 ID/Status/Priority/See Also/Recurrence-Count)已融合到 DISCOVER 输出格式中。原 skill 文件保留但标记 deprecated |
| **.learnings/ 目录**(各 agent workspace | **废弃。** JSONL 是唯一数据源。目录保留但不再写入新内容(历史数据保留) |
| **SELF_IMPROVEMENT_REMINDER.md** | **废弃。** 规则已融合到 skill-management skill 中 |
## 11. 实现计划
| 步骤 | 内容 | 优先级 | 工作量 |
|------|------|--------|--------|
| S1 | 在 SOUL.md / AGENTS.md 加入 Skill 自我修补规则 + 双层 daily 蒸馏规则 | P0 | L1(改文案) |
| S2 | 创建 skill-management Skill(主 SKILL.md + references/ 四阶段详细操作) | P0 | L2 |
| S3 | 创建各 agent 的 03:00 cron(自蒸馏 L1 | P1 | L1 |
| S4 | 创建庞统的 05:00 cron(整合 + 审查 L2 | P1 | L1-L2 |
| S5 | 实现 IMPROVE cronJSONL 引用追踪 + 淘汰报告(每周) | P2 | L2-L3 |
| S6 | 清理 deprecated 代码(skill_system.py / experience.py / self-improvement skill / SELF_IMPROVEMENT_REMINDER.md | P3 | L1 |
S1 和 S2 可以立即做。S3-S5 需要先确认设计文档。
## 12. wiki-vault / 知识库参考实践映射
| 设计决策 | 参考来源 | 核心借鉴 |
|---------|---------|---------|
| 统一产物 Skill-only | Hermes skill_manage + memory_tool | 只有 Skill 和 Memory 两种载体 |
| HOW not WHAT | nuwa-skill 实践 #5 | 蒸馏思维方式不是知识内容 |
| description = when not how | Superpowers writing-skills | description 只描述触发条件 |
| 质量检查自动化 | nuwa-skill quality_check.py | 结构化检查代替主观判断 |
| 矛盾处理 | nuwa-skill 实践 #10 | 矛盾是特征不是 Bug |
| Skill 自我修补 | Hermes skill_manage schema | 使用时发现问题立即 patch |
| 闭环学习循环 | 知识管理体系实践 #1 | DISCOVER→DISTILL→APPLY→IMPROVE |
| Experience→Skill 延迟转化 | moziplus 经验实践 #2 | 多次验证后才固化 |
| Skill 生命周期 draft→active→deprecated | OpenClaw skill_workshop | pending→applied→rejected→quarantined |
| 棘轮机制 | moziplus 经验实践 #2 | 经验只能改进不能退化 |
| 优雅降级 | nuwa-skill 实践 #17 | 信息不足时不要强行蒸馏 |
| 迭代上限 | nuwa-skill 实践 #18 | 最多 2 轮验证,不无限打磨 |
| **双层 daily 蒸馏** | self-improvement skill daily review | 有距离感的蒸馏优于即时记录 |
| **结构化信号格式** | self-improvement skill logging format | ID/Status/Priority/See Also/Recurrence-Count |
| **Recurrence-Count 验证** | self-improvement skill recurring pattern detection | ≥3 次自动触发提升,比主观判断更客观 |
| **Skill Extraction Criteria** | self-improvement skill extraction | Recurring + Verified + Non-obvious + Broadly applicable |
| **per-agent Skill 目录** | Hermes skill_manage + self-improving-agent practice §5 | 每个 agent 建立自己的 Skill 库 |
| **废弃 .learnings/** | DISCOVER 统一采集 | JSONL 是唯一数据源,避免信息冗余 |
| **组合模式(主 skill + references** | moziplus skill-engineering practices §4 | Skill 之间通过产出物松耦合传递 |
## 13. 部署目录结构
### 13.1 openclaw skill 加载优先级
OpenClaw 按 6 级优先级扫描 skill 目录,同名 skill 高优先级覆盖低优先级:
| 优先级 | 来源 | 路径 | 可见性 |
|--------|------|------|--------|
| 1 — 最高 | Workspace skills | `<workspace>/skills` | 只对该 agent |
| 2 | Project agent skills | `<workspace>/.agents/skills` | 只对该 workspace 的 agent |
| 3 | Personal agent skills | `~/.agents/skills` | 所有 agent |
| 4 | Managed / local skills | `~/.openclaw/skills` | 所有 agent |
| 5 | Bundled skills | 随安装包(`/opt/homebrew/.../openclaw/skills/` | 所有 agent |
| 6 — 最低 | Extra dirs + plugin skills | `skills.load.extraDirs` + `~/.openclaw/plugin-skills/` | 所有 agent |
### 13.2 skill-management Skill 目录结构
放在公共目录(`~/.sanguo_projects/sanguo_mozi/skills/`),所有 moziplus agent 可见:
```
~/.sanguo_projects/sanguo_mozi/skills/skill-management/
├── SKILL.md # 主 Skill:综述 + 核心原则 + 各阶段职责摘要
├── references/
│ ├── discover-l1.md # L1 各 agent 自蒸馏详细操作(03:00 cron 读这个)
│ ├── discover-l2.md # L2 庞统整合详细操作(05:00 cron 读这个)
│ ├── distill.md # DISTILL 阶段详细操作(蒸馏规范 + 验证标准)
│ ├── apply.md # APPLY 阶段说明(openclaw 原生机制,简短)
│ └── improve.md # IMPROVE 阶段详细操作(引用追踪 + 淘汰 + 提升)
└── assets/
├── templates/
│ ├── skill-template.md # SKILL.md 标准模板
│ └── signal-format.md # 信号输出格式模板(ID/Priority/Pattern-Key
└── checklists/
└── quality-check.md # 质量检查清单
```
**为什么放公共目录**:所有 agent 都需要触发这个 skillDISCOVER L1 时各 agent 按 description 匹配 → read SKILL.md → 再按需 read references/)。DISCOVER/IMPROVE 是 cron 场景,cron payload 中直接指定 `read references/xxx.md` 按内容执行。
**为什么不拆分为独立 skill**5 个 skill = 5 条 description 常驻上下文(~500-800 token)。其中 DISCOVER 和 IMPROVE 是 cron 触发不是 agent 按描述触发,不需要常驻 description。用 references/ 按需加载更省上下文。
### 13.3 Cron 产出流转路径
```
L1 产出(各 agent 03:00
↓ skill_workshop createpending proposal
↓ 存储:skill_workshop 内部管理(~/.openclaw/workspace-<agent>/.skill-workshop/
L2 审查(庞统 05:00
↓ skill_workshop list → inspect → 决策
├─ APPROVE(个人经验,质量达标)
│ → skill_workshop apply
│ → 写入:~/.openclaw/workspace-<agent>/skills/<skill-name>/SKILL.md
│ → 仅该 agent 可见(workspace skill,优先级 1
├─ MERGE(跨 agent 共性)
│ → 合并多个 proposal 为共享 Skill
│ → skill_workshop apply 到庞统 workspace,然后 cp/symlink 到公共目录
│ → 写入:~/.sanguo_projects/sanguo_mozi/skills/<skill-name>/SKILL.md
│ → 所有 agent 可见(extra dir,优先级 6
│ → 清理:MERGE 后通知各 agent quarantine workspace 中的同名 draft
│ ⚠️ skill_workshop 只能写 workspace skills,不能写 extraDir。
│ MERGE 流程的实际写入方式:庞统在 workspace apply 后,
│ 手动 cp 到公共目录(或配置 skills.load.allowSymlinkTargets 用 symlink)。
├─ REJECT(质量不够)
│ → skill_workshop reject(附原因)
│ → agent 在下次 L1 蒸馏时看到反馈
└─ PROMOTE(高确定性,提升为确定性规则)
→ 手动写入 AGENTS.md / SOUL.md / TOOLS.md
→ 所有 agent 强制注入(L1 确定性规则层)
```
**关键设计**APPROVE 写入 per-agent workspace(优先级 1,最高),MERGE 写入公共目录(优先级 6,最低)。如果同名 skill 在两边都有,workspace 版本覆盖公共版本——agent 可以有自己改进过的版本。
### 13.4 Per-agent Skill 目录
各 agent workspace 下的 skills 目录(目前不存在,L2 审查 APPROVE 后由 skill_workshop 自动创建):
```
~/.openclaw/workspace-zhangfei/skills/ # 张飞的个人经验 Skill
~/.openclaw/workspace-guanyu/skills/ # 关羽的个人经验 Skill
~/.openclaw/workspace-zhaoyun/skills/ # 赵云的个人经验 Skill
~/.openclaw/workspace-simayi/skills/ # 司马懿的个人经验 Skill
~/.openclaw/workspace-pangtong/skills/ # 庞统的个人经验 Skill
~/.openclaw/workspace-jiangwei/skills/ # 姜维的个人经验 Skill
```
**适用场景**
- 张飞的编码踩坑模式 → 只有张飞需要,不污染其他 agent 上下文
- 司马懿的审查技巧 → 只有司马懿需要
- 庞统的规划经验 → 只有庞统需要
### 13.5 Proposal 中间产物存储
```
~/.openclaw/workspace-<agent>/.skill-workshop/
├── proposals/
│ ├── <proposal-id>/
│ │ ├── PROPOSAL.md # 草案内容
│ │ ├── metadata.json # 状态、hash、scanner state
│ │ └── support-files/ # 附带的 references/assets
│ └── ...
├── applied/ # 已 apply 的 proposal 归档
├── rejected/ # 已 reject 的 proposal 归档
└── quarantined/ # 已 quarantine 的 proposal 归档
```
**注意**proposal 存储由 skill_workshop 内部管理,不需要手动操作。首次使用 skill_workshop 时自动创建 `.skill-workshop/` 目录。庞统 L2 cron 通过 `skill_workshop list`(查看所有 agent 的 pending proposal+ `skill_workshop inspect`(查看具体内容)+ `skill_workshop apply/reject/quarantine`(执行决策)完成审查。
### 13.6 全景目录结构
```
# ━━━━━━━ Skill 来源(按 openclaw 优先级) ━━━━━━━━
# P1: Per-agent workspace skills(个人经验,L2 APPROVE 后写入)
~/.openclaw/workspace-<agent>/skills/<skill-name>/SKILL.md
# P4: Managed / local skills(保留,目前为空)
~/.openclaw/skills/
# P5: Bundled skillsopenclaw 自带,不动)
/opt/homebrew/lib/node_modules/openclaw/skills/
# P6: Extra dirs + plugin skills
~/.sanguo_projects/sanguo_mozi/skills/ # moziplus 团队共享 Skill
├── skill-management/ # ← §19 核心 Skill
│ ├── SKILL.md
│ ├── references/{discover-l1, discover-l2, distill, apply, improve}.md
│ └── assets/{templates, checklists}/
├── blackboard-executor/ # 现有
├── blackboard-reviewer/ # 现有
├── trial-and-error-patterns/ # 现有(经验会追加到这里)
└── ...(其他现有 skill)
~/.openclaw/plugin-skills/ # plugin Skillfeishu 等)
# ━━━━━━━ Cron 产出流转 ━━━━━━━━
# L103:00 各 agent
# 输入:~/.openclaw/agents/<agent_id>/sessions/*.jsonl
# 产出:skill_workshop create → proposalpending
# 存储:~/.openclaw/workspace-<agent>/.skill-workshop/proposals/
# L205:00 庞统)
# 输入:全量数据源 + 所有 pending proposals
# 审查:skill_workshop list → inspect → apply/merge/reject
# 产出:
# APPROVE → ~/.openclaw/workspace-<agent>/skills/<skill-name>/per-agent
# MERGE → ~/.sanguo_projects/sanguo_mozi/skills/<skill-name>/(共享)
# REJECT → proposal 归档到 rejected/
# PROMOTE → 手动写入 AGENTS.md / SOUL.md / TOOLS.md
# IMPROVE(每周 庞统)
# 输入:过去 7 天所有 agent 的 session JSONL
# 产出:淘汰候选报告 → skill_workshop quarantine
# ━━━━━━━ 废弃的目录(保留历史,不再写入) ━━━━━━━━
# .learnings/ — 不再写入
~/.openclaw/workspace-*/.learnings/
# self-improvement skill — 不再激活
~/.openclaw/workspace/skills/self-improving-agent/
# SELF_IMPROVEMENT_REMINDER.md — 废弃
# 规则已融合到 skill-management skill 中
```
+66
View File
@@ -0,0 +1,66 @@
---
name: skill-management
description: "Use when managing skill lifecycle through the DISCOVER-DISTILL-APPLY-IMPROVE loop, when doing daily experience distillation, or when reviewing/auditing skill proposals."
---
# Skill Management — 经验闭环 + Skill 生命周期
四阶段闭环:DISCOVER → DISTILL → APPLY → IMPROVE。双层 daily 蒸馏架构。
## 什么时候用
- **L1 自蒸馏**(每天 03:00,各 agent):扫描自己的 session JSONL,蒸馏自己的经验 → 提交 draft proposal
- **L2 整合审查**(每天 05:00,庞统):扫描全量数据源 + 审查所有 L1 draft → approve/merge/reject
- **IMPROVE**(每周,庞统):追踪 Skill 引用情况,淘汰 30 天无引用的 Skill
- **自我修补**(实时,任何 agent):使用 Skill 时发现问题 → 立即 revise proposal
详细操作步骤见 references/ 目录,按当前阶段 `read` 对应文件。
## 核心原则
1. **统一产物 Skill-only**:产物只有 Skillskill_workshop 管理)和 MemoryMEMORY.md),不再有 .learnings/ 等中间形态
2. **HOW not WHAT**:蒸馏「怎么做」不是「发生了什么」。描述问题模式,不固化技术细节
3. **description = when not how**Skill 的 description 只描述触发条件,不描述工作流
4. **双层蒸馏**:L1 各 agent 自己蒸馏(自己最准);L2 庞统负责跨 agent 共性识别 + 审查
5. **矛盾是特征不是 Bug**:保留矛盾,标注类型(时间性/领域性/本质性),不强制调和
## 四阶段速查
| 阶段 | 谁 | 何时 | 做什么 | 详细文档 |
|------|---|------|--------|---------|
| DISCOVER L1 | 每个 agent | 03:00(错开 15min | 扫描自己 JSONL → 蒸馏 → draft proposal | `references/discover-l1.md` |
| DISCOVER L2 | 庞统 | 05:00 | 全量扫描 + 审查 draft → approve/merge/reject | `references/discover-l2.md` |
| DISTILL | L1 各 agent + L2 庞统 | 同 DISCOVER | 提取根因模式,按 SKILL.md 格式产出 | `references/distill.md` |
| APPLY | openclaw 原生 | 实时 | description 匹配 → read SKILL.md → 执行 | `references/apply.md` |
| IMPROVE | 庞统 | 每周 | JSONL 引用追踪 + 淘汰 + 提升 | `references/improve.md` |
## 验证标准(Recurrence-Count 机制)
从 draft → active
| 维度 | 标准 | 不通过 |
|------|------|--------|
| Recurrence-Count ≥ 2 | 同一 Pattern-Key 在 ≥2 个场景出现 | 降级为 MEMORY.md |
| 有生成力 | 能给出具体操作指引 | 丢弃 |
| 有排他性 | 不是常识 | 丢弃 |
提升触发(全部满足):30 天内 ≥3 次 + 跨 ≥2 个任务。
## 自我修补规则
使用 Skill 时发现缺步骤、过时信息、命令变更 → **立即** 通过 skill_workshop 提交 revise proposal。不等定时任务,不等到下次 review。
## 常见错误
| 错误 | 后果 | 正确做法 |
|------|------|---------|
| 蒸馏 WHAT 不 HOW | 经验无法复用 | 描述根因模式 |
| description 包含工作流 | Agent 跳过读完整 SKILL.md | description 只描述触发条件 |
| 缺少 Recurrence-Count | 偶发问题被固化 | 必须 ≥2 次才提升 |
| 强制调和矛盾 | 丢失关键信号 | 保留矛盾,标注类型 |
| skill_workshop 写公共目录 | 操作失败 | skill_workshop 只能写 workspace,公共目录用 cp/symlink |
## 来源
- 设计文档:`docs/design/19-skill-lifecycle-and-experience-loop.md` v2.0
- 参考实践:Hermes skill_manage、nuwa-skill、Superpowers writing-skills、self-improvement skill
@@ -0,0 +1,36 @@
---
name: quality-check
description: "Skill 蒸馏产出质量检查清单"
---
# 质量检查清单
蒸馏产出提交前,逐条检查:
## 结构检查
- [ ] frontmatter 有 name 和 description
- [ ] description 以「Use when...」开头
- [ ] description 只含触发条件,不含工作流
- [ ] 有「什么时候用」章节
- [ ] 有「怎么做」章节
- [ ] 有「常见错误」章节
- [ ] 有「来源」章节
## 内容检查
- [ ] trigger 是否具体(不是「注意代码质量」这种泛泛而谈)
- [ ] action 是否可执行(不是「要小心」这种无操作指引)
- [ ] 蒸馏的是 HOW 不是 WHAT(根因模式,不是事件描述)
- [ ] 没有项目特定的硬编码值
## 验证检查
- [ ] Recurrence-Count ≥ 2(同一模式在 ≥2 个场景出现)
- [ ] 有生成力(能给出具体操作指引)
- [ ] 有排他性(不是常识)
## 重复检查
- [ ] 检查现有 skills 目录中是否已有覆盖
- [ ] 如果是对已有 Skill 的增量更新,使用 revise 而非 create
@@ -0,0 +1,39 @@
---
name: signal-format
description: "DISCOVER 阶段信号输出格式模板"
---
# 信号输出格式
每条候选信号包含:
```
信号类型 | 来源(task_id / PR / review / session| 时间 | 简述(≤100 字)
ID: SIG-YYYYMMDD-XXX
Priority: low | medium | high | critical
Status: pending | in_progress | resolved | promoted
See Also: SIG-YYYYMMDD-XXX(关联信号)
Recurrence-Count: N(同一模式出现次数)
Pattern-Key: category.subcategory(稳定去重键)
```
## 字段说明
| 字段 | 用途 | 示例 |
|------|------|------|
| ID | 唯一标识,便于交叉引用 | SIG-20260618-001 |
| Priority | 优先级排序 | critical: 阻断核心功能; high: 影响常见流程; medium: 有 workaround; low: 边缘场景 |
| Status | 生命周期跟踪 | pending → in_progress → resolved / promoted |
| See Also | 关联相似信号,发现共性模式 | SIG-20260617-003 |
| Recurrence-Count | 同一模式出现次数,≥3 触发自动提升 | 2 |
| Pattern-Key | 稳定去重键,跨 agent 匹配同一模式 | sync.field_mapping |
## 信号类型(5 类)
| 类型 | 识别特征 |
|------|---------|
| 失败模式 | 有明确的失败原因 + 排查过程 |
| 重复问题 | 同关键词出现 ≥2 次 |
| 决策转折 | 原方向被推翻或修正 |
| 新实践 | 之前没有的知识 |
| 知识缺口 | 查不到/不确定的东西 |
@@ -0,0 +1,51 @@
---
name: skill-template
description: "SKILL.md 标准模板 — 蒸馏产出时按此格式编写"
---
# Skill 标准模板
```yaml
---
name: <skill-name>
description: "Use when <触发条件/问题模式描述>"
---
# <Skill 标题>
## 什么时候用
<具体的触发场景,按问题模式描述,不按技术特定症状>
## 怎么做
<根因分析 + 操作步骤>
1. <步骤 1>
2. <步骤 2>
3. <步骤 3>
## 常见错误
<反模式:什么不该做>
- ❌ <错误做法> → <后果>
- ❌ <错误做法> → <后果>
## 来源
<evidence:哪些 task/PR/review 提炼了这条经验>
- task <id>: <简述>
- PR #<num>: <简述>
```
## description 编写要点
- 以「Use when...」开头
- 只描述触发条件(when),**不描述工作流**(how
- 描述问题模式,不描述技术特定症状
- 控制在 1-2 句话
## 质量自检
- [ ] trigger 是否具体(不是「注意代码质量」)
- [ ] action 是否可执行(不是「要小心」)
- [ ] 是否与已有 Skill 重复
- [ ] description 是否只含触发条件
@@ -0,0 +1,34 @@
# APPLY — Skill 应用阶段
## 机制
APPLY 完全基于 openclaw 原生 skill 机制,不需要额外代码:
1. openclaw 扫描 skills 目录 → 生成 `<available_skills>` 列表(只有 name + description
2. Agent 按 description 匹配 → `read` SKILL.md 完整内容
3. Agent 按内容执行
## 渐进式加载
- L1`<available_skills>` 列表(~100 token/skill)— 每次启动注入
- L2Agent `read` SKILL.md — 按需加载
- L3SKILL.md 内引用的 references/ 文件 — 按需加载
## Skill 存放位置与可见性
| 位置 | 可见性 | 优先级 |
|------|--------|--------|
| `~/.openclaw/workspace-<agent>/skills/` | 仅该 agent | 1(最高) |
| `~/.sanguo_projects/sanguo_mozi/skills/` | 所有 moziplus agent | 6(最低) |
workspace 版本覆盖公共版本——agent 可以有自己改进过的版本。
## 自我修补
使用 Skill 时发现问题(缺步骤、过时信息、命令变更)→ **立即** 通过 skill_workshop 提交 revise proposal
```python
skill_workshop(action="revise", proposal_id="<id>", proposal_content="<修改后的内容>")
```
不等定时任务,不等到下次 review。
@@ -0,0 +1,84 @@
# DISCOVER L1 — 各 agent 自蒸馏(每天 03:00
## 你是谁
你是某个 agent(张飞/关羽/赵云/司马懿/庞统/姜维),在每天 03:00 被 cron 唤醒,执行自己的经验蒸馏。
## cron 错开时间
各 agent 错开 15 分钟避免资源争用:
| Agent | 时间 |
|-------|------|
| zhangfei-dev | 03:00 |
| guanyu-dev | 03:15 |
| zhaoyun-data | 03:30 |
| simayi-challenger | 03:45 |
| pangtong-fujunshi | 04:00 |
| jiangwei-infra | 04:15 |
## 操作步骤
### Step 1: 扫描当天 session JSONL
```
输入:~/.openclaw/agents/<your-agent-id>/sessions/*.jsonl
时间范围:过去 24 小时(上次 L1 到现在)
```
重点扫描以下内容:
- `"tool":"exec"` 失败的命令(exit code 非 0
- `"role":"user"` 消息中的纠正(「不对」「错了」「应该是」等)
- `"role":"assistant"` 中的反复返工(同一文件改了 3 次以上)
- task status 变更为 failed 的事件
- review verdict 为 REQUEST_CHANGES 的记录
### Step 2: 信号识别(5 类高价值信号)
| 信号类型 | 识别特征 | 示例 |
|---------|---------|------|
| 失败模式 | 有明确的失败原因 + 排查过程 | 命令报错、CI 失败、review 驳回 |
| 重复问题 | 同关键词在当天出现 ≥2 次 | 反复修改同一段代码、同类错误 |
| 决策转折 | 原方向被推翻或修正 | 主公纠正、需求澄清、rebuttal |
| 新实践 | 之前没有的知识 | 新工具用法、新架构模式 |
| 知识缺口 | 表达不确定、查不到 | 「不确定」「没找到」「推测」 |
### Step 3: 蒸馏(HOW not WHAT
对每个信号,提取根因模式,不是事件描述:
```
❌ "PR #83 修复了 event_type 未知的问题"WHAT,无法复用)
✅ "消费者/生产者字段同步:新增 dataclass 字段时,必须同步所有从 JSON 提取该字段的代码路径"(HOW,可复用)
```
蒸馏规范详见 `references/distill.md`
### Step 4: 产出 draft proposal
对蒸馏后的经验,使用 skill_workshop 提交:
```
skill_workshop(action="create", name="<skill-name>", description="Use when <触发条件>", proposal_content="<SKILL.md 内容>")
```
输出格式(每条信号):
```
信号类型 | 来源(task_id / session| 时间 | 简述(≤100 字)
ID: SIG-YYYYMMDD-XXX
Priority: low | medium | high | critical
Status: pending
Recurrence-Count: N
Pattern-Key: category.subcategory(如 sync.field_mapping
```
### Step 5: 完成
所有 draft proposal 提交后,L1 结束。不需要等待 L2 审查结果(庞统会在 05:00 处理)。
## 注意事项
- 数据源**只有**你自己的 session JSONL,不需要扫描黑板/Gitea/Mail
- 如果当天没有有价值的信号(没踩坑、没被纠正、没新发现),不产出任何 proposal,这是正常的
- 不要为了产出而强行蒸馏——偶发问题降级为 MEMORY.md,不提交 proposal
- 质量优于数量:1 条高质量 proposal 比 5 条流水账有价值
@@ -0,0 +1,118 @@
# DISCOVER L2 — 庞统整合审查(每天 05:00)
## 你是谁
你是庞统,在每天 05:00 被 cron 唤醒,执行跨 agent 整合 + draft proposal 审查。
前提:所有 agent 的 L1 自蒸馏(03:00-04:15)已完成。
## 操作步骤
### Step 1: 获取所有 L1 draft proposals
```
skill_workshop(action="list", status="pending")
```
列出所有 pending 状态的 proposal,检查哪些是今天 L1 产出的。
### Step 2: 全量数据源扫描
扫描以下数据源,识别跨 agent 共性模式:
| 数据源 | 位置 | 关注什么 |
|--------|------|---------|
| 黑板 tasks | 各项目 blackboard.db | task failed、状态异常 |
| 黑板 reviews | reviews 表 | REQUEST_CHANGES verdict + suggestions |
| 黑板 comments | comments 表 | rebuttal 讨论、@mention 争议 |
| 黑板 events | events 表 | guardrail 拦截、异常检测 |
| Gitea Issues/PRs | Gitea API | 新问题、PR review 评论 |
| Gitea CI | Gitea Actions | lint/test/build 失败 |
| Mail | mail API | 跨 agent 讨论、推理过程 |
| 所有 agent JSONL | ~/.openclaw/agents/*/sessions/ | 全团队当天思考过程 |
| MEMORY.md | 各 agent workspace | 已有经验教训 |
| knowledge-gaps.md | wiki-vault/_meta/ | 知识缺口 |
| L1 draft proposals | skill_workshop pending | 各 agent 当天提交 |
### Step 3: 跨 agent 共性模式识别
寻找同一 Pattern-Key 在多个 agent 的 JSONL/proposal 中出现的情况:
```
张飞 SIG-20260618-001: Pattern-Key: sync.field_mapping
关羽 SIG-20260618-002: Pattern-Key: sync.field_mapping
→ 共性信号!Recurrence-Count = 2,可合并为共享 Skill
```
### Step 4: 审查每个 draft proposal
对每个 L1 draft proposal,逐条审查:
```
skill_workshop(action="inspect", proposal_id="<id>")
```
审查维度:
| 维度 | 标准 | 不通过 |
|------|------|--------|
| Recurrence-Count ≥ 2 | 同一 Pattern-Key 在 ≥2 个场景出现 | 降级为 MEMORY.md |
| 有生成力 | 能给出具体操作指引 | 丢弃 |
| 有排他性 | 不是常识 | 丢弃 |
| description 合规 | 只描述触发条件,不含工作流 | 要求 revise |
| trigger 具体 | 不是「注意代码质量」 | 要求 revise |
### Step 5: 执行决策
对每个 proposal 做出决策:
**APPROVE**(个人经验,质量达标):
```python
skill_workshop(action="apply", proposal_id="<id>")
# skill_workshop 自动写入 agent workspace: ~/.openclaw/workspace-<agent>/skills/<skill-name>/
# 仅该 agent 可见
```
**MERGE**(跨 agent 共性):
```python
# 1. 在庞统 workspace apply 合并后的版本
skill_workshop(action="apply", proposal_id="<id>")
# 2. cp 到公共目录(skill_workshop 不能写 extraDir
cp ~/.openclaw/workspace-pangtong/skills/<skill-name>/SKILL.md \
~/.sanguo_projects/sanguo_mozi/skills/<skill-name>/SKILL.md
# 3. 通知各 agent quarantine workspace 中的同名 draft
# 在相关 PR/Issue 中 @agent 说明
```
**REJECT**(质量不够):
```python
skill_workshop(action="reject", proposal_id="<id>", reason="<具体原因>")
# agent 在下次 L1 时看到反馈
```
**PROMOTE**(高确定性经验,提升为规则):
```python
# 手动写入 AGENTS.md / SOUL.md / TOOLS.md 对应区块
# 这不属于 skill_workshop 管理范围
```
### Step 6: 全局提升检查
检查是否有经验达到提升条件(Recurrence-Count ≥ 3 + 跨 ≥2 任务 + 30 天内):
| 提升目标 | 条件 | 效果 |
|---------|------|------|
| 独立 Skill | 足够通用,有自己的触发条件 | 独立 SKILL.md |
| AGENTS.md 规则 | 确定性高,适用于所有 agent | L1 强制注入 |
| guardrail | 安全相关,不可违反 | 强制检查 |
### Step 7: 知识缺口反馈
IMPROVE 发现的经验缺口或 L2 发现的新领域 → 追加到 `knowledge-gaps.md`
## 注意事项
- L2 时间窗口:05:00 执行,确保 L1 全部完成(最后一个 agent 04:15 开始)
- 全量扫描不需要逐行读 JSONL,用 grep 定位关键词再精读匹配段
- MERGE 后必须清理各 agent workspace 的同名 draft(避免覆盖公共版本)
- REJECT 必须附具体原因,帮 agent 改进而非打击
@@ -0,0 +1,137 @@
# DISTILL — 蒸馏规范
## 核心原则:HOW not WHAT
蒸馏的是「怎么做」不是「发生了什么」:
```
❌ "PR #83 修复了 event_type 未知的问题"
→ 这是 WHAT,无法复用
✅ "消费者/生产者字段同步:新增 dataclass 字段时,必须同步所有从 JSON 提取该字段的代码路径"
→ 这是 HOW,可复用到任何消费者/生产者场景
```
## SKILL.md 编写规范
```yaml
---
name: skill-name
description: Use when [触发条件/问题模式描述],不描述工作流
---
# Skill 标题
## 什么时候用
(具体的触发场景,按问题模式描述,不按技术特定症状)
## 怎么做
(根因分析 + 操作步骤)
## 常见错误
(反模式:什么不该做)
## 来源
evidence:哪些 task/PR/review 提炼了这条经验)
```
## description 关键规则
- 只描述触发条件(when to use),**绝不描述工作流**how)
- 以「Use when...」开头
- 描述问题模式,不描述技术特定症状
- 原因:测试发现 description 如果总结了工作流,agent 会按 description 执行而跳过读完整 SKILL.md
### 示例
```yaml
# ❌ BAD:描述了工作流
description: Use when modifying dataclass — checks all extraction points, runs tests
# ✅ GOOD:只描述触发条件
description: Use when modifying a dataclass that is populated from JSON extraction by another module
# ❌ BAD:太抽象
description: Use for code quality
# ✅ GOOD:描述问题模式
description: Use when a field added to a dataclass appears empty or as default value at runtime
```
## 蒸馏示例
**一级蒸馏**(从具体案例提取):
```yaml
# 案例 1PromptContext event_type 未知
# 案例 2PromptContext from_agent/mail_type 缺失(PR #26 D2
→ 共同根因:消费者/生产者字段同步问题
## 消费者/生产者字段同步
**什么时候用**:修改 dataclass 时,如果该 dataclass 由外部 JSON 提取填充
**怎么做**
1. 改 dataclass 定义
2. 检查所有从 JSON 提取字段的代码路径,同步新增提取逻辑
3. 检查所有构造该 dataclass 的调用点,同步新增参数
4. 跑一次构建测试验证字段不为空
**常见错误**:只改 dataclass 不改提取逻辑 → 字段默认值为空 → 运行时不报错但行为异常
```
**二级蒸馏**(从多个一级经验提取通用模式):
如果经验在 ≥2 个不同场景复现,验证通过后,可以提升为独立 Skill 或固化到 AGENTS.md 规则。
## 验证标准
从 draft → active
| 维度 | 标准 | 不通过 |
|------|------|--------|
| Recurrence-Count ≥ 2 | 同一 Pattern-Key 在 ≥2 个场景出现 | 降级为 MEMORY.md |
| 有生成力 | 能给出具体操作指引 | 丢弃 |
| 有排他性 | 不是常识 | 丢弃 |
提升触发(全部满足):30 天内 ≥3 次 + 跨 ≥2 个任务。
## Skill Extraction 质量 Gate
| 标准 | 描述 |
|------|------|
| Recurring | 有 See Also 链接到 2+ 个相似信号 |
| Verified | Status 是 resolved 且有工作修复 |
| Non-obvious | 需要实际调试才能发现 |
| Broadly applicable | 不是项目特定,可跨场景复用 |
## 质量检查
| 检查项 | 标准 |
|--------|------|
| trigger 是否具体 | 不是「注意代码质量」 |
| action 是否可执行 | 不是「要小心」 |
| 是否与已有 Skill 重复 | 检查现有 skills 目录 |
| description 是否只含触发条件 | 不包含工作流描述 |
## 矛盾处理
新经验与已有经验冲突时:
- **时间性矛盾**(观点演化)→ 记录演化轨迹,以近期为主
- **领域性矛盾**(不同场景不同规则)→ 分场景记录
- **本质性张力**(价值观内在冲突)→ 标注为「核心张力」,两个版本都保留
**矛盾是特征,不是 Bug。** 强制调和会丢失关键信号。
## 信号输出格式
每条信号包含:
```
信号类型 | 来源 | 时间 | 简述(≤100 字)
ID: SIG-YYYYMMDD-XXX
Priority: low | medium | high | critical
Status: pending | in_progress | resolved | promoted
See Also: SIG-YYYYMMDD-XXX
Recurrence-Count: N
Pattern-Key: category.subcategory
```
@@ -0,0 +1,70 @@
# IMPROVE — 引用追踪 + 淘汰 + 提升(每周 cron)
## 你是谁
你是庞统,每周执行一次 IMPROVE cron,扫描过去 7 天的所有 session JSONL。
## 操作步骤
### Step 1: 引用追踪
扫描过去 7 天所有 agent 的 session JSONL,采集 Skill 引用信号:
| 信号 | 采集方式 | 可信度 |
|------|---------|--------|
| Skill 被 read 的时间 | grep `"tool":"read"` + SKILL.md 路径 | 中 |
| Skill 在 available_skills 中被注入 | grep available_skills 列表 | 中(注入但未必用) |
| Agent 输出中提及 skill name | grep skill name in assistant messages | 高 |
| Skill 文件最近修改时间 | git log / 文件 mtime | 高 |
### Step 2: 生成淘汰候选报告
对每个 Skill 检查最近 30 天的引用信号:
```
30 天无引用信号
→ 加入淘汰候选列表
```
输出淘汰候选报告:
```
| Skill 名称 | 最后引用时间 | 存放位置 | 建议 |
|-----------|------------|---------|------|
| xxx | 2026-05-15 | 公共目录 | 建议淘汰 |
| yyy | 从未被引用 | 张飞 workspace | 建议淘汰 |
```
### Step 3: 庞统审阅决策
逐条审阅淘汰候选:
- **确认淘汰** → `skill_workshop(action="quarantine", proposal_id="<id>")`
- **保留观察** → 标注,下轮再查
- **更新后保留** → 修改 description / 内容,重置计时
**注意**openclaw 本身的 skill~/.openclaw/plugin-skills/ 和全局 skills)也纳入追踪。报告给主公决定是否禁用。
### Step 4: 经验提升检查
检查是否有 Skill 达到提升条件(被频繁引用 ≥5 次 + 多次验证):
| 提升目标 | 条件 | 效果 |
|---------|------|------|
| 独立 Skill | 足够通用,有自己的触发条件 | 独立 SKILL.md |
| AGENTS.md 规则 | 确定性高,适用于所有 agent | L1 强制注入 |
| guardrail | 安全相关,不可违反 | 强制检查 |
### Step 5: 反馈到 DISCOVER
IMPROVE 发现的经验缺口写入 knowledge-gaps.md
```
- [日期] IMPROVE 发现「<skill-name> 不适用 <场景>」→ 待 DISCOVER 处理
```
成为下一轮 DISCOVER L2 的输入。
## 注意事项
- 不追求精确归因,做时间维度的信号采集
- 淘汰决策由庞统判断,不自动执行
- 提升到 AGENTS.md 的规则需要主公确认(影响所有 agent 的确定性注入)
+1
View File
@@ -117,6 +117,7 @@ def _migrate_v28(conn: sqlite3.Connection) -> None:
_safe_add_column(conn, "tasks", "round_count", "INTEGER DEFAULT 0")
_safe_add_column(conn, "tasks", "resumed_from", "TEXT")
_safe_add_column(conn, "tasks", "dispatch_count", "INTEGER DEFAULT 0")
# 3. checkpoints 表(M3
conn.execute("""CREATE TABLE IF NOT EXISTS checkpoints (
+2
View File
@@ -41,6 +41,8 @@ class Task:
resumed_from: Optional[str] = None # 暂停前状态,恢复时回到原状态
# v2.9 四相循环
round_count: int = 0 # 庞统 review 轮次计数
# §15 Runaway Guard
dispatch_count: int = 0 # 被 ticker dispatch 的总次数
# v2.8 归档
archived: bool = False
archived_at: Optional[str] = None
+1 -1
View File
@@ -208,7 +208,7 @@ class Blackboard:
params.append(parent_task)
if conditions:
query += " WHERE " + " AND ".join(conditions)
query += " ORDER BY priority ASC, created_at ASC"
query += " ORDER BY priority ASC, created_at DESC"
rows = conn.execute(query, params).fetchall()
return [Task.from_row(r) for r in rows]
finally:
+2 -2
View File
@@ -9,7 +9,7 @@ import logging
from pathlib import Path
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
from src.blackboard.db import get_connection
logger = logging.getLogger("moziplus-v2.handler.mail")
@@ -36,7 +36,7 @@ class MailHandler(BaseTaskHandler):
return composer.compose(context)
def get_sections(self) -> list:
return [MailContextSection(), MailApiSection(), MailConstraintsSection(), GiteaConventionSection(), WikiGuideSection()]
return [MailContextSection(), MailApiSection(), MailConstraintsSection(), GiteaConventionSection(), WikiGuideSection(), DeliveryChecklistSection()]
def verify_completion(self, task_id: str, db_path: Path) -> VerifyResult:
"""Mail 完成验证:区分 inform/request。
+24
View File
@@ -174,3 +174,27 @@ class WikiGuideSection:
def should_include(self, context: "PromptContext") -> bool:
return True
# ---------------------------------------------------------------------------
# DeliveryChecklistSection — 交付检查清单
# ---------------------------------------------------------------------------
class DeliveryChecklistSection:
"""交付检查清单 — 提醒 Agent 完成前同步关联成果物。"""
name: str = "delivery_checklist"
priority: int = 55 # CONSTRAINTS(50) 和 EXTENSION(60) 之间
CHECKLIST_TEXT = (
"## 交付检查\n"
"完成代码改动前确认:\n"
"- 改了实现 → docs/design/ 对应设计文档是否需要更新\n"
"- 改了实现 → tests/ 是否有对应测试脚本需要更新\n"
"- 所有成果物变更通过 PR 流程:PR review 把关设计合理性,CI 把关代码质量,CD 把关部署正确性\n"
)
def render(self, context: "PromptContext") -> str:
return self.CHECKLIST_TEXT
def should_include(self, context: "PromptContext") -> bool:
return True
+27 -4
View File
@@ -288,6 +288,8 @@ class AgentSpawner:
mail_type = ""
action_type = ""
action_steps = []
event_type = ""
event_data = {}
try:
meta = json.loads(must_haves) if must_haves else {}
from_agent = meta.get("from", "")
@@ -295,6 +297,8 @@ class AgentSpawner:
# toolchain 字段提取
action_type = meta.get("action_type", "")
action_steps = meta.get("steps", [])
event_type = meta.get("event_type", "")
event_data = meta.get("context", {})
except Exception:
pass
ctx = PromptContext(
@@ -304,6 +308,7 @@ class AgentSpawner:
spawn_type=spawn_type,
from_agent=from_agent, mail_type=mail_type,
action_type=action_type, action_steps=action_steps,
event_type=event_type, event_data=event_data,
)
return handler.build_prompt(ctx)
@@ -625,19 +630,24 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
self._register_session(session_id, agent_id, task_id, proc.pid,
# use_main_session=True 时 session_id 为 None,但 _register_session 和
# _monitor_process 需要一个非 None 的 key;同时 ticker 等调用方用
# `result is not None` 判断 spawn 是否成功,返回 None 会被误判为失败。
# 统一用 "main" 作为占位标识。
effective_sid = session_id or "main"
self._register_session(effective_sid, agent_id, task_id, proc.pid,
broadcast_task_ids=broadcast_task_ids)
logger.info("Spawned agent %s (session=%s, pid=%d)",
agent_id, session_id, proc.pid)
agent_id, effective_sid, proc.pid)
# Schedule monitor(传 wrapped_on_complete)
asyncio.create_task(
self._monitor_process(session_id, proc, agent_id, task_id,
self._monitor_process(effective_sid, proc, agent_id, task_id,
on_complete=_wrapped_on_complete,
db_path=task_db_path or self.db_path)
)
return session_id
return effective_sid
except Exception as e:
# spawn 失败也要 release counter
@@ -1949,6 +1959,19 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_
try:
from src.daemon.mail_notify import _is_mail_project, notify_mail_failed
if _is_mail_project(db_path):
# 防御性检查:如果 task 已经 done,不触发失败通知(竞态保护)
# 场景:spawner 标 failed 和 handler 标 done 同时发生
try:
conn2 = get_connection(db_path)
current_status = conn2.execute(
"SELECT status FROM tasks WHERE id=?", (task_id,)
).fetchone()
conn2.close()
if current_status and current_status["status"] == "done":
logger.info("Task %s already done, skipping mail failure notification", task_id)
return
except Exception:
pass
# Mail 失败:通知发件人,不 @pangtong
notify_mail_failed(db_path, task_id, reason, detail)
else:
+2 -1
View File
@@ -10,7 +10,7 @@ from pathlib import Path
from typing import Dict, Optional
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
from src.blackboard.db import get_connection
logger = logging.getLogger("moziplus-v2.handler")
@@ -315,6 +315,7 @@ class TaskHandler(BaseTaskHandler):
TaskConstraintsSection(),
GiteaConventionSection(),
WikiGuideSection(),
DeliveryChecklistSection(),
]
def build_prompt(self, context: PromptContext) -> str:
+51
View File
@@ -1084,6 +1084,19 @@ Parent Task ID: {parent_task.id}
broadcast_ids = await self._broadcast_claim(broadcast_tasks, db_path, project_id)
dispatched.extend(broadcast_ids)
# §15 Runaway Guard: 统一递增 dispatch_count
if dispatched:
conn = get_connection(db_path)
try:
for tid in dispatched:
conn.execute(
"UPDATE tasks SET dispatch_count = COALESCE(dispatch_count, 0) + 1 WHERE id=?",
(tid,),
)
conn.commit()
finally:
conn.close()
return dispatched
async def _broadcast_claim(self, tasks: list, db_path: Path,
@@ -1376,6 +1389,19 @@ Parent Task ID: {parent_task.id}
except Exception:
logger.exception("Review dispatch failed for %s", task.id)
# §15 Runaway Guard: 统一递增 dispatch_count (review)
if dispatched:
conn = get_connection(db_path)
try:
for tid in dispatched:
conn.execute(
"UPDATE tasks SET dispatch_count = COALESCE(dispatch_count, 0) + 1 WHERE id=?",
(tid,),
)
conn.commit()
finally:
conn.close()
return dispatched
# ------------------------------------------------------------------
@@ -1388,6 +1414,31 @@ Parent Task ID: {parent_task.id}
reclaimed: List[str] = []
now = datetime.utcnow() # UTC,与 SQLite datetime('now') 一致
# §15 Runaway Guard: per-task dispatch_count 上限检查
# 覆盖所有状态,防止无限循环 dispatch
MAX_DISPATCH_COUNT = 10
for status_to_check in ("pending", "working", "claimed"):
tasks_to_check = queries.tasks_by_status(status_to_check)
for task in tasks_to_check:
dispatch_count = getattr(task, 'dispatch_count', 0) or 0
if dispatch_count >= MAX_DISPATCH_COUNT:
conn = get_connection(db_path)
try:
ok = self._transition_status(
conn, task.id, "failed",
agent="daemon",
detail={"reason": "runaway_guard",
"dispatch_count": dispatch_count,
"message": f"dispatch {dispatch_count} 次仍未完成,自动标 failed"},
)
if ok:
reclaimed.append(task.id)
logger.error(
"Task %s: runaway guard triggered (dispatch_count=%d, status=%s), marking failed",
task.id, dispatch_count, status_to_check)
finally:
conn.close()
# claimed 超时 → 重置为 pending(如果 retry_count >= 3 则升级庞统)
claimed = queries.tasks_by_status("claimed")
for task in claimed:
+27 -2
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from typing import Dict, List
from src.daemon.base_task_handler import BaseTaskHandler, VerifyResult
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection
from src.daemon.prompt_composer import PromptComposer, PromptContext, GiteaConventionSection, WikiGuideSection, DeliveryChecklistSection
from src.daemon.toolchain_templates import render_template, _TEMPLATE_MAP
from src.blackboard.db import get_connection
@@ -51,17 +51,41 @@ class ToolchainContextSection:
name: str = "toolchain_context"
priority: int = 10
EVENT_LABELS_ZH: Dict[str, str] = {
"review_request": "Review 请求",
"review_result": "Review 结果",
"review_merged": "PR 合并",
"review_comment": "Review 评论",
"review_updated": "Review 更新",
"ci_failure": "CI 失败",
"deploy_failure": "部署失败",
"issue_assigned": "Issue 指派",
"mention": "@提及",
}
def render(self, context: PromptContext) -> str:
event_type = context.event_type
event_data: Dict = context.event_data or {}
# 事件类型中文标签
event_label = self.EVENT_LABELS_ZH.get(event_type, event_type or '未知')
# from / to 信息
to_agent = context.agent_id or ''
from_agent = 'system'
# Part 1: 事件信息(现有模板引擎)
if event_type in _TEMPLATE_MAP:
variables = {k: str(v) for k, v in event_data.items()}
event_text = render_template(event_type, variables)
# 补充事件类型中文标签 + from/to
header = f"- **事件类型**: {event_label}\n- **来源**: {from_agent}\n- **指派**: {to_agent}\n"
event_text = header + "\n" + event_text
else:
lines = ["## 工具链事件", ""]
lines.append(f"- **事件类型**: {event_type or '未知'}")
lines.append(f"- **事件类型**: {event_label}")
lines.append(f"- **来源**: {from_agent}")
lines.append(f"- **指派**: {to_agent}")
if event_data:
lines.append("- **事件详情**:")
for key, value in event_data.items():
@@ -228,6 +252,7 @@ class ToolchainHandler(BaseTaskHandler):
ToolchainConstraintsSection(),
GiteaConventionSection(),
WikiGuideSection(),
DeliveryChecklistSection(),
]
def build_prompt(self, context: PromptContext) -> str:
-2
View File
@@ -17,7 +17,6 @@ import CourtCeremony from './components/CourtCeremony';
import CourtDiscussion from './components/CourtDiscussion';
import UsagePanel from './components/UsagePanel';
import SettingsPanel from './components/SettingsPanel';
import ToolchainPanel from './components/ToolchainPanel';
import GlobalSearch from './components/GlobalSearch';
import NotificationCenter from './components/NotificationCenter';
@@ -101,7 +100,6 @@ export default function App() {
usage: <UsagePanel />,
morning: <MorningPanel />,
settings: <SettingsPanel />,
toolchain: <ToolchainPanel />,
};
return (
@@ -5,6 +5,7 @@
import { useState, useCallback } from 'react';
import { api, AgentsStatusData } from '../api';
import ToolchainPanel from './ToolchainPanel';
interface ServiceCheckResult {
name: string;
@@ -15,7 +16,7 @@ interface ServiceCheckResult {
}
export default function SettingsPanel() {
const [tab, setTab] = useState<'connections' | 'security' | 'version' | 'logs'>('connections');
const [tab, setTab] = useState<'connections' | 'security' | 'version' | 'logs' | 'toolchain'>('connections');
// 接线状态巡检
const [checking, setChecking] = useState(false);
@@ -95,6 +96,7 @@ export default function SettingsPanel() {
{ key: 'security' as const, label: '🛡️ 安全防务' },
{ key: 'version' as const, label: '📦 版本更新' },
{ key: 'logs' as const, label: '📋 城防日志' },
{ key: 'toolchain' as const, label: '⛓️ 工具链' },
].map((t) => (
<button key={t.key} className={`btn ${tab === t.key ? 'btn-primary' : ''}`} onClick={() => setTab(t.key)}>
{t.label}
@@ -288,6 +290,9 @@ export default function SettingsPanel() {
</div>
</div>
)}
{/* ========== 工具链 ========== */}
{tab === 'toolchain' && <ToolchainPanel />}
</div>
);
}
+47 -2
View File
@@ -4,6 +4,28 @@
*/
import { useEffect, useState } from 'react';
const AGENT_NAMES: Record<string, string> = {
'pangtong-fujunshi': '庞统',
'simayi-challenger': '司马懿',
'zhangfei-dev': '张飞',
'guanyu-dev': '关羽',
'zhaoyun-data': '赵云',
'jiangwei-infra': '姜维',
'system': '系统',
};
const EVENT_LABELS: Record<string, string> = {
'review_request': 'Review 请求',
'review_result': 'Review 结果',
'review_merged': 'PR 合并',
'review_comment': 'Review 评论',
'review_updated': 'Review 更新',
'ci_failure': 'CI 失败',
'deploy_failure': '部署失败',
'issue_assigned': 'Issue 指派',
'mention': '@提及',
};
const STATUS_COLORS: Record<string, string> = {
pending: '#f59e0b22', claimed: '#6a9eff22', working: '#6a9eff22',
review: '#818cf822', done: '#2ecc8a22', failed: '#ef444422',
@@ -36,6 +58,7 @@ export default function ToolchainPanel() {
const [detail, setDetail] = useState<any>(null);
const [searchQuery, setSearchQuery] = useState('');
const [loading, setLoading] = useState(false);
const [filterMode, setFilterMode] = useState<'all' | 'pending'>('all');
const loadTasks = async (q?: string) => {
setLoading(true);
@@ -52,6 +75,10 @@ export default function ToolchainPanel() {
setLoading(false);
};
const displayed = filterMode === 'pending'
? tasks.filter(t => !['done', 'failed', 'cancelled'].includes(t.status))
: tasks;
useEffect(() => { loadTasks(); }, []);
// 搜索防抖 300ms
@@ -120,7 +147,19 @@ export default function ToolchainPanel() {
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: '1px solid #2a3550', background: '#161b2e', color: '#8899aa', cursor: 'pointer',
}}>🔄</button>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{tasks.length} </span>
<button onClick={() => setFilterMode('all')} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: `1px solid ${filterMode === 'all' ? 'var(--acc)' : '#2a3550'}`,
background: filterMode === 'all' ? 'var(--acc)22' : '#161b2e',
color: filterMode === 'all' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
}}></button>
<button onClick={() => setFilterMode('pending')} style={{
padding: '3px 8px', borderRadius: 4, fontSize: 10,
border: `1px solid ${filterMode === 'pending' ? 'var(--acc)' : '#2a3550'}`,
background: filterMode === 'pending' ? 'var(--acc)22' : '#161b2e',
color: filterMode === 'pending' ? 'var(--acc)' : '#8899aa', cursor: 'pointer',
}}></button>
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{filterMode === 'pending' ? displayed.length : tasks.length} </span>
</div>
{/* 事件列表 */}
@@ -130,7 +169,7 @@ export default function ToolchainPanel() {
{loading ? '加载中...' : '暂无工具链事件'}
</div>
)}
{tasks.map((t: any) => (
{displayed.map((t: any) => (
<div key={t.id} onClick={() => setSelectedId(t.id)} style={{
padding: '10px 14px', borderBottom: '1px solid var(--line)',
cursor: 'pointer', transition: 'background .15s',
@@ -151,6 +190,9 @@ export default function ToolchainPanel() {
fontSize: 12, fontWeight: 500, color: '#dde4f8',
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{t.title}</div>
<div style={{ fontSize: 10, color: 'var(--muted)', marginTop: 2 }}>
{AGENT_NAMES['system'] || '系统'} {AGENT_NAMES[t.assignee] || t.assignee || '?'}
</div>
</div>
))}
</div>
@@ -174,6 +216,9 @@ export default function ToolchainPanel() {
<span style={{ fontSize: 10, color: 'var(--muted)' }}>{detail.id}</span>
</div>
<div style={{ fontSize: 18, fontWeight: 700, lineHeight: 1.3 }}>{detail.title}</div>
<div style={{ fontSize: 11, color: 'var(--muted)', marginTop: 4 }}>
{AGENT_NAMES['system'] || '系统'} {AGENT_NAMES[detail.assignee] || detail.assignee || '?'}
</div>
<div style={{ fontSize: 12, color: 'var(--muted)', marginTop: 6 }}>
{fmtTime(detail.created_at)}
</div>
+1 -2
View File
@@ -120,7 +120,7 @@ export function isArchived(t: Task): boolean {
export type TabKey =
| 'tasks' | 'court' | 'monitor' | 'agents'
| 'models' | 'skills' | 'sessions' | 'archives' | 'templates'
| 'usage' | 'settings' | 'officials' | 'morning' | 'mail' | 'toolchain';
| 'usage' | 'settings' | 'officials' | 'morning' | 'mail';
export const TAB_DEFS: { key: TabKey; label: string; icon: string }[] = [
{ key: 'tasks', label: '任务看板', icon: '📜' },
@@ -135,7 +135,6 @@ export const TAB_DEFS: { key: TabKey; label: string; icon: string }[] = [
{ key: 'archives', label: '奏折阁', icon: '📜' },
{ key: 'morning', label: '早朝简报', icon: '🌅' },
{ key: 'templates', label: '任务模板', icon: '📋' },
{ key: 'toolchain', label: '工具链', icon: '⛓️' },
{ key: 'settings', label: '系统设置', icon: '⚙️' },
];
@@ -543,3 +543,94 @@ class TestCheckTimeoutsUnified:
reclaimed = ticker._check_timeouts(db_path)
assert "t-review-dead" not in reclaimed
# ---------------------------------------------------------------------------
# E13: §15 Runaway Guard — per-task dispatch_count 上限
# ---------------------------------------------------------------------------
class TestRunawayGuard:
"""E13: dispatch_count >= 10 → 自动标 failed(覆盖所有非终态)"""
@pytest.fixture
def guard_project(self, tmp_path):
"""创建项目 + 任务"""
data_root = tmp_path / "projects"
registry = ProjectRegistry(data_root)
registry.create_project("guard-proj", "Guard Test", agents=["agent-a"])
db_path = data_root / "guard-proj" / "blackboard.db"
bb = Blackboard(db_path)
return registry, db_path, bb
def test_runaway_guard_triggers_working(self, guard_project):
"""E13.1: working 状态 dispatch_count >= 10 → 标 failed"""
registry, db_path, bb = guard_project
bb.create_task(Task(
id="t-runaway", title="Runaway Task", status="working",
assigned_by="daemon", current_agent="agent-a",
))
conn = bb._conn()
try:
conn.execute(
"UPDATE tasks SET dispatch_count = 10 WHERE id = ?", ("t-runaway",))
conn.commit()
finally:
conn.close()
ticker = Ticker(registry, tick_interval=30)
reclaimed = ticker._check_timeouts(db_path)
assert "t-runaway" in reclaimed
task = Queries(db_path).task_by_id("t-runaway")
assert task.status == "failed"
def test_runaway_guard_triggers_pending(self, guard_project):
"""E13.2: pending 状态 dispatch_count >= 10 → 标 failed"""
registry, db_path, bb = guard_project
bb.create_task(Task(
id="t-pending-runaway", title="Pending Runaway", status="pending",
assigned_by="daemon",
))
conn = bb._conn()
try:
conn.execute(
"UPDATE tasks SET dispatch_count = 10 WHERE id = ?",
("t-pending-runaway",))
conn.commit()
finally:
conn.close()
ticker = Ticker(registry, tick_interval=30)
reclaimed = ticker._check_timeouts(db_path)
assert "t-pending-runaway" in reclaimed
task = Queries(db_path).task_by_id("t-pending-runaway")
assert task.status == "failed"
def test_runaway_guard_not_triggered(self, guard_project):
"""E13.3: dispatch_count < 10 → 正常流程不受影响"""
registry, db_path, bb = guard_project
bb.create_task(Task(
id="t-normal", title="Normal Task", status="working",
assigned_by="daemon", current_agent="agent-a",
))
conn = bb._conn()
try:
conn.execute(
"UPDATE tasks SET dispatch_count = 5 WHERE id = ?", ("t-normal",))
conn.commit()
finally:
conn.close()
ticker = Ticker(registry, tick_interval=30)
reclaimed = ticker._check_timeouts(db_path)
assert "t-normal" not in reclaimed
task = Queries(db_path).task_by_id("t-normal")
assert task.status == "working"