Compare commits
113 Commits
aad5a6b317
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 493d0f017b | |||
| dc1d444fed | |||
| ab59723b1e | |||
| 1f7e7f69ad | |||
| 474db1263b | |||
| 09c763341a | |||
| 11f1c28565 | |||
| a584bb0abf | |||
| 7a2505b137 | |||
| c2e710bf67 | |||
| d2e449bca8 | |||
| 91116520d2 | |||
| 6536d99e7f | |||
| cbb58c3092 | |||
| a402bb2a2d | |||
| ac8444bb41 | |||
| d39f2ba1df | |||
| 548e39d3b8 | |||
| 1a536e84c6 | |||
| d25eea4e82 | |||
| 38c676c627 | |||
| 0c1acbd073 | |||
| b921b094ca | |||
| 7bb7637d24 | |||
| 02883750f5 | |||
| 60925c3395 | |||
| 7335cafa90 | |||
| 1362cc5d64 | |||
| 6df7563070 | |||
| da137ba193 | |||
| ac4e28a57c | |||
| 41d60cca2c | |||
| bae3244e24 | |||
| e238eaec31 | |||
| b0bca0df5c | |||
| 6d1d906551 | |||
| 37b8115485 | |||
| 7c11c6b9aa | |||
| 027ae59d40 | |||
| 12f6ac3b1f | |||
| 623942bd15 | |||
| 280435119e | |||
| 65eb7d6a99 | |||
| f4c995270c | |||
| 4a154f8e1a | |||
| 735b8c4fed | |||
| 2e2edd2b00 | |||
| b384433c80 | |||
| dceddeff45 | |||
| 31028a7e6f | |||
| 6a73d6d6c7 | |||
| 576cd96b43 | |||
| 48c2b8ea3d | |||
| e89bd51d7c | |||
| 4c3d125a30 | |||
| 59499798ea | |||
| 2febe54920 | |||
| 59c95d4125 | |||
| 23d8691b3f | |||
| f4fea8f418 | |||
| 3a11327113 | |||
| ddc5eb9897 | |||
| b0f4572ba6 | |||
| b6e58a164c | |||
| ea04b4c483 | |||
| 04568108a6 | |||
| 6ea43d76e3 | |||
| 09520a414e | |||
| 9388f3ecc1 | |||
| cdc49ac74d | |||
| f74ae30d41 | |||
| 67b504c5db | |||
| 230b8c9cd9 | |||
| 5b73319aaf | |||
| 8c7c277167 | |||
| 999cd1cc10 | |||
| 0475e40529 | |||
| ca9b750656 | |||
| 7e832ee865 | |||
| 1c7be0e782 | |||
| 49d0b3a789 | |||
| 5505ac9c5c | |||
| f5bf671410 | |||
| ccb5d5d3ea | |||
| ee825db818 | |||
| cdf984aa0c | |||
| 6798f098b5 | |||
| 33e38254c1 | |||
| 166172e0b8 | |||
| 0bd1caff90 | |||
| f615326514 | |||
| c2c6c9a7f6 | |||
| f26de6cfda | |||
| 1ff4d98a03 | |||
| a953fc0bc7 | |||
| 7f17ee69d7 | |||
| f1e513cba2 | |||
| 627982db09 | |||
| 9ec601d747 | |||
| cc5c7f5ad1 | |||
| d6cb854f68 | |||
| 1f373d5cb5 | |||
| a8c9d25857 | |||
| 660ac4b659 | |||
| 91685ebfdd | |||
| 65910f5417 | |||
| 17b87290c8 | |||
| bd5735f970 | |||
| 05f9112fab | |||
| b926b35703 | |||
| 8df1d4a83c | |||
| ad34750075 | |||
| cd7e24cd3c |
@@ -0,0 +1,27 @@
|
||||
name: 文档任务
|
||||
about: 编写或更新文档
|
||||
title: "[moz] docs: "
|
||||
labels:
|
||||
- type/docs
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 文档目标
|
||||
description: 要写什么文档,解决什么问题
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: path
|
||||
attributes:
|
||||
label: 文档路径
|
||||
description: 目标文件路径(如 docs/design/xxx.md)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: outline
|
||||
attributes:
|
||||
label: 大纲
|
||||
description: 文档结构和要点
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 实现任务
|
||||
about: 按设计文档实现功能
|
||||
title: "[moz] impl: "
|
||||
labels:
|
||||
- type/feat
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 实现目标
|
||||
description: 清晰描述要实现什么
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: design_doc
|
||||
attributes:
|
||||
label: 设计文档
|
||||
description: 关联的设计文档路径(如 docs/design/xxx.md)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: 实现范围
|
||||
description: 涉及哪些文件/模块
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: acceptance
|
||||
attributes:
|
||||
label: 验收标准
|
||||
validations:
|
||||
required: true
|
||||
@@ -0,0 +1,27 @@
|
||||
name: 重构任务
|
||||
about: 重构现有代码
|
||||
title: "[moz] refactor: "
|
||||
labels:
|
||||
- type/refactor
|
||||
body:
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: 重构目标
|
||||
description: 为什么重构,期望改善什么
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: scope
|
||||
attributes:
|
||||
label: 影响范围
|
||||
description: 涉及哪些文件/模块,是否有 breaking change
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: tests
|
||||
attributes:
|
||||
label: 测试保障
|
||||
description: 重构前哪些测试必须通过
|
||||
validations:
|
||||
required: true
|
||||
+36
-4
@@ -2,6 +2,7 @@
|
||||
#
|
||||
# 触发条件:
|
||||
# - pull_request(opened, synchronize)
|
||||
# - 仅代码改动触发,纯文档改动跳过(paths-ignore)
|
||||
#
|
||||
# 注意:只保留 pull_request 触发,避免 push + pull_request 双倍触发
|
||||
#
|
||||
@@ -16,6 +17,12 @@ name: CI
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mockups/**'
|
||||
- 'prompt_templates/**'
|
||||
- '*.md'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
# ── Job 1: Lint ──────────────────────────────────────
|
||||
@@ -25,7 +32,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
env:
|
||||
no_proxy: "*"
|
||||
run: |
|
||||
rm -rf /tmp/ci-venv-lint
|
||||
python3 -m venv /tmp/ci-venv-lint
|
||||
/tmp/ci-venv-lint/bin/pip install --quiet --upgrade pip
|
||||
/tmp/ci-venv-lint/bin/pip install --quiet flake8
|
||||
@@ -42,11 +52,13 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
env:
|
||||
no_proxy: "*"
|
||||
run: |
|
||||
rm -rf /tmp/ci-venv-test
|
||||
python3 -m venv /tmp/ci-venv-test
|
||||
/tmp/ci-venv-test/bin/pip install --quiet --upgrade pip
|
||||
/tmp/ci-venv-test/bin/pip install --quiet --no-cache-dir fastapi pydantic pyyaml uvicorn requests pytest pytest-asyncio httpx
|
||||
/tmp/ci-venv-test/bin/pip install --quiet 'fastapi' 'pydantic<2' pyyaml uvicorn requests pytest pytest-asyncio httpx
|
||||
|
||||
- name: Debug environment
|
||||
run: |
|
||||
@@ -62,12 +74,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 +105,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 +121,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" \
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#
|
||||
# 触发条件:
|
||||
# - push 到 main 分支
|
||||
# - 仅代码改动触发部署,纯文档改动跳过(paths-ignore)
|
||||
#
|
||||
# Gitea v1.23.4 限制注意:
|
||||
# - 不支持 failure() 表达式
|
||||
@@ -13,6 +14,12 @@ name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'mockups/**'
|
||||
- 'prompt_templates/**'
|
||||
- '*.md'
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
# ── Job 1: CI(main 分支跑完整测试)─────────────────
|
||||
@@ -22,8 +29,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
env:
|
||||
no_proxy: "*"
|
||||
run: |
|
||||
rm -rf /tmp/ci-venv-deploy
|
||||
python3 -m venv /tmp/ci-venv-deploy
|
||||
/tmp/ci-venv-deploy/bin/pip install --quiet --upgrade pip
|
||||
/tmp/ci-venv-deploy/bin/pip install --quiet flake8 fastapi pydantic pyyaml uvicorn requests pytest pytest-asyncio httpx
|
||||
|
||||
- name: Lint
|
||||
|
||||
@@ -35,3 +35,4 @@ inbox/*.jsonl
|
||||
|
||||
# E2E test data
|
||||
data/e2e-*/
|
||||
data/_mail/
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
# toolchain-templates.yaml
|
||||
# §21 §4 steps 模板化 — 按 action_type + business_type 查找
|
||||
# 占位符 {issue_number}/{brief}/{title}/{pr_number} 等运行时渲染
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# issue_assigned(按 Issue label 分 6 路 + infrastructure)
|
||||
# ---------------------------------------------------------------------------
|
||||
issue_assigned:
|
||||
feat:
|
||||
steps:
|
||||
- "理解需求(Issue body)→ 如有不明确在 Issue comment 追问"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b feat/{issue_number}-{brief}"
|
||||
- "编码实现 + 写 UT"
|
||||
- "文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」"
|
||||
- "git add -A && git commit -m '[moz] feat: {title}' && git push origin feat/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**分支**:feat/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**改动文件**:{files}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
impl:
|
||||
steps:
|
||||
- "读设计文档(Issue body 中的路径)→ 理解实现范围"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b impl/{issue_number}-{brief}"
|
||||
- "按设计编码实现 + 写 UT"
|
||||
- "文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」"
|
||||
- "git add -A && git commit -m '[moz] impl: {title}' && git push origin impl/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 设计文档路径)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**设计文档**:{design_doc}
|
||||
**分支**:impl/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**改动文件**:{files}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
bug:
|
||||
steps:
|
||||
- "读 Bug 描述 + 复现步骤(Issue body)"
|
||||
- "定位根因(读代码/日志,不要猜测)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b fix/{issue_number}-{brief}"
|
||||
- "修复 + 写回归测试"
|
||||
- "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "git add -A && git commit -m '[moz] fix: {title}' && git push origin fix/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 根因和修复方式)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**根因**:{root_cause}
|
||||
**修复方式**:{fix_approach}
|
||||
**分支**:fix/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
docs:
|
||||
steps:
|
||||
- "读文档目标(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b docs/{issue_number}-{brief}"
|
||||
- "编写文档到 docs/ 对应目录"
|
||||
- "git add -A && git commit -m '[moz] docs: {title}' && git push origin docs/{issue_number}-{brief}"
|
||||
- "创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**文档路径**:{doc_path}
|
||||
**分支**:docs/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
|
||||
refactor:
|
||||
steps:
|
||||
- "读重构目标 + 影响范围(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b refactor/{issue_number}-{brief}"
|
||||
- "重构 + 确保现有测试不 break(python3 -m pytest tests/unit/ -q)"
|
||||
- "文档同步:如重构涉及架构变更,同步更新 docs/design/ 对应文档"
|
||||
- "git add -A && git commit -m '[moz] refactor: {title}' && git push origin refactor/{issue_number}-{brief}"
|
||||
- "CI 通过后创建 PR(body 含 Closes #{issue_number} + 重构内容和影响范围)"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**重构范围**:{scope}
|
||||
**测试结果**:{test_result} passed
|
||||
**分支**:refactor/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
test:
|
||||
steps:
|
||||
- "读测试目标(Issue body)"
|
||||
- "git checkout main && git pull origin main"
|
||||
- "git checkout -b test/{issue_number}-{brief}"
|
||||
- "编写测试脚本到 tests/ 对应目录"
|
||||
- "运行测试验证(python3 -m pytest {test_file} -v)"
|
||||
- "git add -A && git commit -m '[moz] test: {title}' && git push origin test/{issue_number}-{brief}"
|
||||
- "创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**测试文件**:{test_file}
|
||||
**测试结果**:{test_result}
|
||||
**分支**:test/{issue_number}-{brief}
|
||||
**PR**:#{pr_number}
|
||||
|
||||
infrastructure:
|
||||
steps:
|
||||
- "根据 Issue body 中的错误来源和日志片段排查问题"
|
||||
- "修复基础设施问题(CI runner/网络/Gitea/磁盘等)"
|
||||
- "修复后在 Issue 上 comment 说明修复方式和结果"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**问题**:{problem}
|
||||
**根因**:{root_cause}
|
||||
**修复方式**:{fix}
|
||||
**验证**:{verification}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ci_failure
|
||||
# ---------------------------------------------------------------------------
|
||||
ci_failure:
|
||||
steps:
|
||||
- "查看完整 CI 日志(PR 页面或 Gitea Actions 页面)"
|
||||
- "根据 CI 日志判断失败原因类型:\n a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑\n b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(label 必须包含 type/infrastructure)"
|
||||
- "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中说明判断的原因类型和执行的操作,以及文档是否需要更新"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**原因类型**:{cause_type}
|
||||
**操作**:{action}
|
||||
**CI 重跑**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# review_result — APPROVED
|
||||
# ---------------------------------------------------------------------------
|
||||
review_result_approved:
|
||||
steps:
|
||||
- "合并 PR(Gitea API: POST /repos/{repo}/pulls/{pr_number}/merge)"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**PR #{pr_number}**:merged
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# review_result — REQUEST_CHANGES
|
||||
# ---------------------------------------------------------------------------
|
||||
review_result_request_changes:
|
||||
steps:
|
||||
- "按审查意见逐条修改代码"
|
||||
- "文档同步:如审查涉及设计/接口变更,同步更新 docs/design/ 对应文档"
|
||||
- "push 到原分支 → CI 自动跑"
|
||||
- "CI 通过后等重新 Review"
|
||||
- "提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果"
|
||||
output_template: |
|
||||
[Action Report]
|
||||
**修改内容**:{changes}
|
||||
**CI**:{ci_status}
|
||||
**文档同步**:{doc_status}
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
title: "TaskTypeRegistry + Handler 架构重构"
|
||||
created: 2026-06-10
|
||||
version: v3.0
|
||||
version: v3.1
|
||||
---
|
||||
|
||||
> 状态: ✅ 已完成(Step 1-5 全部合并,394 passed)
|
||||
> v3.1 新增 §18:Mail 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 段 3(Skill 全文)
|
||||
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 可以抽一个 BaseApiSection(curl 模板 + success_status 参数),其余 section 各自实现。
|
||||
|
||||
@@ -667,9 +701,9 @@ src/daemon/
|
||||
├── task_type_registry.py # §3 + §4:Protocol + 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=request,performative="text")被 dispatch 给 agent。agent 正常处理并输出文本(如"已阅,无需处理"),但 `verify_completion` 判定 no_reply → 标 failed → 触发 `notify_mail_failed` → 产生 38 封 `[投递失败]` 通知邮件,每 ~2.5 分钟一轮,持续 10 轮。
|
||||
|
||||
### 根因链
|
||||
|
||||
```
|
||||
E2E 测试脚本 bug(type="text")
|
||||
→ mail_routes.py 不校验 type 值,直接透传
|
||||
→ performative="text" ≠ "inform" → 走 _check_reply
|
||||
→ _check_reply 查 in_reply_to task,agent 没用 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) | 标 failed(base 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 修复方向
|
||||
|
||||
### 方向 1:mail verify 对齐 toolchain 模式(✅ 已确认)
|
||||
|
||||
**问题**:mail verify 只有 in_reply_to task 一条路径。task/toolchain 都有多层 fallback(outputs / 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 DB(comments 表),不是 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` 方法
|
||||
|
||||
### 方向 4:verify 失败保持 working(✅ 已确认)
|
||||
|
||||
**问题**:MailHandler 继承 BaseTaskHandler,verify 失败时走 base 的 `on_failure` → 标 failed。而 TaskHandler 覆盖了 `post_complete`,verify 失败时留 working。
|
||||
|
||||
**原始设计意图**(§2 设计文档):"不通过 → 留 working,ticker 重查(最多 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` 覆盖
|
||||
|
||||
### 方向 5:type 校验 + 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 背景
|
||||
|
||||
|
||||
@@ -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 recovery(3 次)+ api_retry(3 次)余量 = ~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 作为兜底)
|
||||
@@ -391,7 +391,11 @@ def verify_completion(self, task_id: str, db_path: Path) -> VerifyResult:
|
||||
|
||||
#### 完整设计
|
||||
|
||||
三分路的详细伪代码、失败上限、决策依据见 §5.2.1~§5.2.3(on_failure 分路处理详细设计)。
|
||||
三分路的详细设计见 §6.4(基础设施 Issue 转交流程)和 toolchain_handler.py 实现(`_handle_infrastructure_failure` / `_classify_failure`)。
|
||||
|
||||
#### 首次 steps 分支指引
|
||||
|
||||
三分路是 verify 失败后的兜底机制。此外,ci_failure 和 deploy_failure 的首次 toolchain task steps 中**已包含分支指引**——agent 在执行过程中自行判断失败原因,如果是基础设施问题则直接创建 Issue 指派 jiangwei-infra(见 §6.4)。三分路作为第二道防线,覆盖 agent 未正确判断或未执行分支的情况。
|
||||
|
||||
### 5.3 action_report comment 格式
|
||||
|
||||
@@ -431,9 +435,9 @@ Agent 可能写了 action_report 但没真做。缓解机制:
|
||||
| Review 请求 → reviewer | review_request | toolchain | 4 步 | 读 diff + 审查 + 提交 Review + report |
|
||||
| Review 有新提交 → reviewer | review_updated | toolchain | 4 步 | 读 diff + 检查修改 + 提交 Review + report |
|
||||
| Review 评论 → PR 作者 | review_comment | toolchain | 3 步 | 查看评论 + 响应(修改/回复)+ report |
|
||||
| CI 失败 → PR 作者 | ci_failure | toolchain | 4 步 | 查 CI 日志 + 修测试 + push + report |
|
||||
| Issue 指派 → 开发者 | issue_assigned | toolchain | 6 步 | 创建分支 + 编码 + push + CI + PR + report |
|
||||
| 部署失败 → 运维 | deploy_failure | toolchain | 4 步 | 查日志 + 排查 + 修+重部署 + report |
|
||||
| CI 失败 → PR 作者 | ci_failure | toolchain | 3 步 | 查 CI 日志 + 分支判断(代码问题自己修 / 基础设施问题提 Issue 给姜维)+ report |
|
||||
| Issue 指派 → 开发者 | issue_assigned / infrastructure_failure | toolchain | 6 步(编码,含具体 git 命令)或 4 步(运维) | 按 label 分流:type/infrastructure → 运维排查;其他 → git checkout main/pull → 创建分支 → 编码 → push → CI → PR + report |
|
||||
| 部署失败 → 运维 | deploy_failure | toolchain | 3 步 | 查日志 + 分支判断(代码/配置问题自己修 / 基础设施问题提 Issue 给姜维)+ report |
|
||||
| @mention → 被@者 | mention | toolchain | 按 guidance | 按 mention 模板的 response_guidance + report |
|
||||
| PR 合并 → PR 作者 | review_merged | toolchain | 0 步 | 纯通知,走 _send_toolchain_task(steps 为空,verify 始终通过) |
|
||||
|
||||
@@ -502,29 +506,46 @@ event_type: ci_failure
|
||||
action_type: ci_failure
|
||||
steps:
|
||||
1. 查看完整 CI 日志(PR 页面或 Gitea Actions 页面)
|
||||
2. 修复失败的测试
|
||||
3. push → CI 自动重跑
|
||||
4. 提交 action report
|
||||
2. 根据 CI 日志判断失败原因类型:
|
||||
a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑
|
||||
b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见 §6.4),label 必须包含 type/infrastructure
|
||||
3. 提交 action report — 报告中说明判断的原因类型和执行的操作
|
||||
context:
|
||||
pr_number, repo, branch, error_summary
|
||||
```
|
||||
|
||||
**分支设计说明**:原设计假设"CI 失败 = 代码问题",steps 只有"修测试 + push"一条路径。实际运行中发现 CI 失败可能是 runner 环境故障(如 ensurepip 失败),agent 判断为基础设施问题后不知道该做什么。现在 steps 中明确两条分支,agent 自行判断后走对应路径。
|
||||
|
||||
#### Issue 指派 → 开发者
|
||||
|
||||
```
|
||||
event_type: issue_assigned
|
||||
action_type: issue_assigned
|
||||
steps:
|
||||
1. 创建分支 fix/{issue_number}-{brief}
|
||||
event_type: issue_assigned 或 infrastructure_failure
|
||||
action_type: issue_assigned 或 infrastructure_failure
|
||||
steps(按 label 分流):
|
||||
|
||||
# 默认路径(编码任务):
|
||||
1. 在开发目录执行 git 操作:
|
||||
a. git checkout main && git pull origin main (确保从最新代码分叉)
|
||||
b. git checkout -b fix/{issue_number}-{brief} (创建功能分支)
|
||||
2. 编码 + 写 UT
|
||||
3. push → 等 CI
|
||||
4. CI 通过后创建 PR(Gitea API: POST /repos/{repo}/pulls)
|
||||
3. git add -A && git commit -m "[moz] fix: {简述}" && git push origin fix/{issue_number}-{brief}
|
||||
4. CI 通过后创建 PR(Gitea API: POST /repos/{repo}/pulls,head: fix/{issue_number}-{brief}, base: main)
|
||||
5. 等 Review
|
||||
6. 提交 action report
|
||||
|
||||
# type/infrastructure label 路径(运维任务):
|
||||
1. 根据 Issue body 中的错误来源和日志片段排查问题
|
||||
2. 修复基础设施问题(如修复 CI runner 环境、恢复网络、重启服务等)
|
||||
3. 修复后在 Issue 上 comment 说明修复方式和结果
|
||||
4. 提交 action report
|
||||
context:
|
||||
issue_number, repo, issue_title, labels, issue_body, brief
|
||||
```
|
||||
|
||||
**label 分流说明**:issue_assigned handler 检查 label 中是否包含 `type/infrastructure`。如果是,走运维排查 steps(event_type 设为 infrastructure_failure,verify 始终 auto-pass 防递归);否则走编码 steps。
|
||||
|
||||
**编码路径 git 操作具体化说明**(§17 v3 补充):原 steps 只写「创建分支」「push」等抽象指令,agent 需要自己推导具体 git 命令。现在 steps 中写清楚完整 git 操作序列(checkout main → pull → checkout -b → commit → push),降低 agent 出错概率。系统不做分支管理(不通过 Gitea API 预创建分支),分支管理完全由 agent 自己执行。ToolchainApiSection 中新增 Git 操作说明段落作为通用参考。
|
||||
|
||||
#### 部署失败 → 运维
|
||||
|
||||
```
|
||||
@@ -532,9 +553,10 @@ event_type: deploy_failure
|
||||
action_type: deploy_failure
|
||||
steps:
|
||||
1. 检查 deploy 日志
|
||||
2. 排查失败原因
|
||||
3. 修复并重新部署
|
||||
4. 提交 action report
|
||||
2. 根据 deploy 日志判断失败原因类型:
|
||||
a. 代码/配置问题(rsync 路径错、依赖缺失、启动失败)→ 修复 → 重新部署
|
||||
b. 基础设施问题(Gitea 不可用、网络不通、磁盘满、SSH 故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见 §6.4),label 必须包含 type/infrastructure
|
||||
3. 提交 action report — 报告中说明判断的原因类型和执行的操作
|
||||
context:
|
||||
repo, commit_sha, reason
|
||||
```
|
||||
@@ -577,6 +599,89 @@ context:
|
||||
|
||||
**spawn 说明**:review_merged 仍会触发 spawn(Agent 只需阅读通知),verify auto-pass 后标 done。未来可优化为 ticker 直接 auto-done 跳过 spawn。
|
||||
|
||||
### 6.4 基础设施 Issue 转交流程
|
||||
|
||||
当 ci_failure / deploy_failure 的 agent 在调查后发现失败原因是基础设施问题(非代码问题),需要创建 Gitea Issue 指派 jiangwei-infra。
|
||||
|
||||
#### Issue 提在哪里
|
||||
|
||||
**根据问题来源决定**——哪个仓库的 CI/部署失败了,Issue 就提到那个仓库。CI runner 是全局共享的(一个 Mac mini),但 Issue 挂在触发的仓库最自然。如果 runner 故障影响多个仓库,各仓库会各自触发 ci_failure task,姜维看到任何一个 Issue 就能定位全局问题。
|
||||
|
||||
#### Issue 格式规范
|
||||
|
||||
```markdown
|
||||
## 问题描述
|
||||
|
||||
<简要描述问题现象>
|
||||
|
||||
## 错误来源
|
||||
|
||||
- 仓库: <repo>
|
||||
- PR/Commit: <链接>
|
||||
- CI/Deploy run: <Gitea Actions 页面链接>
|
||||
|
||||
## 日志关键片段
|
||||
|
||||
```
|
||||
<错误日志摘要,不需全文,但要让排查者看到关键信息>
|
||||
```
|
||||
|
||||
## 判断依据
|
||||
|
||||
<为什么判断为基础设施问题而非代码问题>
|
||||
```
|
||||
|
||||
**必填字段**:问题描述、错误来源(含链接)、日志片段、判断依据。Issue body 不完整会导致姜维无法高效排查。
|
||||
|
||||
**label 要求**:必须包含 `type/infrastructure`,用于 issue_assigned handler 分流(见下)。
|
||||
|
||||
#### issue_assigned handler label 分流
|
||||
|
||||
当 Gitea Issue 指派触发 webhook 时,issue_assigned handler 检查 label:
|
||||
|
||||
| label 包含 type/infrastructure | event_type | steps |
|
||||
|---|---|---|
|
||||
| 是 | infrastructure_failure | 运维排查:根据 Issue body 排查问题 → 修复 → Issue comment 说明修复方式 → action report |
|
||||
| 否 | issue_assigned | 编码:创建分支 → 编码 → push → CI → PR → 等 Review → action report |
|
||||
|
||||
基础设施路径的 verify 始终 auto-pass(防递归,已有逻辑覆盖)。
|
||||
|
||||
⚠️ **label 分流当前为设计目标**,toolchain_handler.py 尚未实现 issue_assigned 的 label 检查。当前只有 `_handle_infrastructure_failure`(verify 失败时)创建 infrastructure_failure task。issue_assigned handler 的 label 分流在后续代码 PR 中实现。
|
||||
|
||||
⚠️ **label 预创建**:使用前需确认仓库中已创建名为 `type/infrastructure` 的 label。sanguo_moziplus_v2 仓库已创建(ID=98)。其他仓库使用前需先创建。
|
||||
|
||||
#### API 指令位置
|
||||
|
||||
Issue 创建的 API 调用方式(curl 示例)统一在 ToolchainApiSection 中,与 action_report / comment 指引并列。steps 指令中不重复 API 调用方式,只描述"做什么"。
|
||||
|
||||
#### Git 操作说明段落
|
||||
|
||||
ToolchainApiSection 中新增通用 Git 操作说明段落,作为 agent 执行 git 命令的参考:
|
||||
|
||||
```
|
||||
### Git 操作说明
|
||||
|
||||
你的工作目录是开发目录。
|
||||
标准分支操作流程:
|
||||
git checkout main && git pull origin main # 从最新主干开始
|
||||
git checkout -b fix/{branch_name} # 创建功能分支
|
||||
# ... 写代码 ...
|
||||
git add -A && git commit -m 'message' # 提交改动
|
||||
git push origin {branch_name} # 推送到远程
|
||||
|
||||
⚠️ 不要在 main 分支上直接 commit。
|
||||
```
|
||||
|
||||
**设计原则**:系统不做分支管理(不预创建分支、不做 checkout),分支管理完全由 agent 自己执行。ToolchainApiSection 提供通用 git 操作参考,steps 中写清楚具体操作序列。
|
||||
|
||||
#### Red Flags 补充
|
||||
|
||||
硬约束 Red Flags 表新增一条:
|
||||
|
||||
| Agent 想法 | Red Flag 驳回 |
|
||||
|---|---|
|
||||
| "CI/部署失败不是我代码的问题,我什么也不用做" | ❌ 错!即使是基础设施问题,你也必须创建 Issue 指派 jiangwei-infra(body 含错误来源链接 + 日志 + 判断依据),并在 action report 中说明。不能只报告"不是我的问题"就完事 |
|
||||
|
||||
---
|
||||
|
||||
## §7. _send_toolchain_task 函数设计
|
||||
|
||||
@@ -0,0 +1,810 @@
|
||||
# §19 Skill 生命周期管理 + 经验闭环四阶段设计
|
||||
|
||||
> 作者:庞统士元
|
||||
> 日期:2026-06-18(v2.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 数量 | 未明确 | **一个 skill:skill-management** + references/ 四阶段 | 减少上下文开销;DISCOVER/IMPROVE 是 cron 场景不需要独立 skill description |
|
||||
| self-improvement skill | 未提及 | **废弃**。优势融合到 DISCOVER 输出格式 | 职责重叠;统一为单一闭环 |
|
||||
| Skill 存放 | 未区分 | **per-agent 目录 + 公共目录** | agent 专属经验不污染其他 agent 上下文 |
|
||||
|
||||
## 1. 背景
|
||||
|
||||
moziplus v2.0 的 P4 剩余两项:
|
||||
- T7 C3:Skill 生命周期管理(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-skills:description = when not how |
|
||||
| D6 | 废弃 .learnings/ 作为数据源 | JSONL 已包含完整信息(工具调用、推理过程、错误输出)。.learnings/ 只是重复抄写,且打断 agent 执行流 | DISCOVER 统一采集;主公确认 |
|
||||
| D7 | 只创建一个 skill:skill-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_workshop,pending 状态)
|
||||
|
||||
### 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
|
||||
# 案例 1:PromptContext event_type 未知
|
||||
# 案例 2:PromptContext 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_workshop(pending proposal)
|
||||
|
||||
**L2:庞统(每天 05:00 cron)**
|
||||
1. 审查所有 agent 提交的 draft proposal(approve / 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)
|
||||
- L2:Agent `read` SKILL.md(完整内容)
|
||||
- L3:SKILL.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 proposal(patch)。
|
||||
|
||||
这不需要定时任务,靠 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.md,description 匹配 |
|
||||
| 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 proposal(structured,带 ID/Priority/Pattern-Key/Recurrence-Count)
|
||||
↓
|
||||
DISCOVER L2(每天 05:00,庞统 cron)
|
||||
数据源:全量 12 个数据源(含 L1 draft proposals)
|
||||
跨 agent 共性模式识别
|
||||
审查 draft proposals:approve / merge / reject
|
||||
↓
|
||||
DISTILL(L2 庞统执行)
|
||||
原则: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 cron:JSONL 引用追踪 + 淘汰报告(每周) | P2 | L2-L3 |
|
||||
| S6 | 清理 deprecated 代码(skill_system.py / experience.py / self-improvement skill / SELF_IMPROVEMENT_REMINDER.md) | P3 | L1 |
|
||||
|
||||
S1 和 S2 已完成(PR #85)。S3-S5 设计见下方 §11A。
|
||||
|
||||
## 11A. Cron 配置方案(S3-S5 详细设计)
|
||||
|
||||
### 设计决策
|
||||
|
||||
**每个 agent 用自己的 agentId 执行 L1 cron**,不由庞统代理。
|
||||
|
||||
理由(对照设计目标 D4):
|
||||
- L1 核心价值是"每个 agent 是自己经验的最佳蒸馏者"——agent 扫描自己的 JSONL,用自己的判断力识别信号
|
||||
- 如果庞统代理,变成庞统替别人蒸馏,消除不了蒸馏者偏差(D4 要解决的正是这个问题)
|
||||
- openclaw cron 原生支持 `agentId` 参数 + `sessionTarget: "isolated"`,技术上无障碍
|
||||
|
||||
### S3: L1 各 agent 自蒸馏 cron
|
||||
|
||||
6 个 agent,各创建一个 isolated cron,错开 15 分钟(和 discover-l1.md 时间表一致):
|
||||
|
||||
| Agent | agentId | cron 表达式 | 时区 |
|
||||
|-------|---------|-----------|------|
|
||||
| 张飞 | zhangfei-dev | `0 3 * * *` | Asia/Shanghai |
|
||||
| 关羽 | guanyu-dev | `15 3 * * *` | Asia/Shanghai |
|
||||
| 赵云 | zhaoyun-data | `30 3 * * *` | Asia/Shanghai |
|
||||
| 司马懿 | simayi-challenger | `45 3 * * *` | Asia/Shanghai |
|
||||
| 庞统 | pangtong-fujunshi | `0 4 * * *` | Asia/Shanghai |
|
||||
| 姜维 | jiangwei-infra | `15 4 * * *` | Asia/Shanghai |
|
||||
|
||||
**Cron 配置规范**(每个 L1 cron job):
|
||||
|
||||
```json
|
||||
{
|
||||
"schedule": { "kind": "cron", "expr": "<时间>", "tz": "Asia/Shanghai" },
|
||||
"sessionTarget": "isolated",
|
||||
"agentId": "<agent-id>",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "L1 自蒸馏 cron。请执行:\n1. read ~/.sanguo_projects/sanguo_mozi/skills/skill-management/SKILL.md\n2. read ~/.sanguo_projects/sanguo_mozi/skills/skill-management/references/discover-l1.md\n3. 按 discover-l1.md 步骤执行自蒸馏\n4. 如有信号:蒸馏为 HOW 格式,使用 skill_workshop(action=create) 提交 draft proposal\n5. 如无有价值信号:不产出,这是正常的",
|
||||
"timeoutSeconds": 600
|
||||
},
|
||||
"delivery": { "mode": "announce" }
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- `sessionTarget: "isolated"`:每次创建临时 session,不污染 main session context
|
||||
- `delivery.mode: "announce"`:执行结果投递到 Control UI,保持可见性(早期使用 `none` 导致 cron 执行后零可见性,已修正)
|
||||
- `timeoutSeconds: 600`:10 分钟足够(扫描 JSONL + 蒸馏 + 提交 proposal)
|
||||
- message 指引 read SKILL.md + discover-l1.md:agent 按 references 指南执行,不依赖 memory
|
||||
|
||||
### S4: L2 庞统整合审查 cron
|
||||
|
||||
庞统的 L2 cron 在所有 L1 完成后执行(最后一个 agent 04:15 开始,L2 设在 05:00):
|
||||
|
||||
| 角色 | agentId | cron 表达式 | 时区 |
|
||||
|------|---------|-----------|------|
|
||||
| 庞统 | pangtong-fujunshi | `0 5 * * *` | Asia/Shanghai |
|
||||
|
||||
**Cron 配置**:
|
||||
|
||||
```json
|
||||
{
|
||||
"schedule": { "kind": "cron", "expr": "0 5 * * *", "tz": "Asia/Shanghai" },
|
||||
"sessionTarget": "isolated",
|
||||
"agentId": "pangtong-fujunshi",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "L2 整合审查 cron。请执行:\n1. read ~/.sanguo_projects/sanguo_mozi/skills/skill-management/references/discover-l2.md\n2. 按 discover-l2.md 步骤执行:\n a. skill_workshop(action=list, status=pending) 获取所有 L1 draft proposals\n b. 全量数据源扫描,识别跨 agent 共性模式\n c. 逐个审查 proposal:approve / merge / reject\n d. 全局提升检查(Recurrence-Count >= 3 的经验提升为规则)\n e. 知识缺口反馈到 knowledge-gaps.md",
|
||||
"timeoutSeconds": 1200
|
||||
},
|
||||
"delivery": { "mode": "announce" }
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- `delivery.mode: "announce"`:审查决策结果投递到 Control UI,主公可见
|
||||
- `timeoutSeconds: 1200`(20 分钟):L2 需要扫描全量数据源 + 审查多个 proposal,时间更长
|
||||
- 庞统可以访问所有 agent 的 JSONL 和 skill_workshop proposals
|
||||
|
||||
### S5: IMPROVE 每周引用追踪 cron
|
||||
|
||||
庞统每周日 06:00 执行引用追踪(周日选活动量最低的时段):
|
||||
|
||||
| 角色 | agentId | cron 表达式 | 时区 |
|
||||
|------|---------|-----------|------|
|
||||
| 庞统 | pangtong-fujunshi | `0 6 * * 0` | Asia/Shanghai |
|
||||
|
||||
**Cron 配置**:
|
||||
|
||||
```json
|
||||
{
|
||||
"schedule": { "kind": "cron", "expr": "0 6 * * 0", "tz": "Asia/Shanghai" },
|
||||
"sessionTarget": "isolated",
|
||||
"agentId": "pangtong-fujunshi",
|
||||
"payload": {
|
||||
"kind": "agentTurn",
|
||||
"message": "IMPROVE 每周引用追踪 cron。请执行:\n1. read ~/.sanguo_projects/sanguo_mozi/skills/skill-management/references/improve.md\n2. 按 improve.md 步骤执行:\n a. 扫描过去 7 天所有 agent 的 session JSONL,采集 Skill 引用信号\n b. 生成淘汰候选报告(30 天无引用的 Skill)\n c. 庞统审阅决策:quarantine / 保留观察 / 更新后保留\n d. 经验提升检查(被频繁引用 >= 5 次的 Skill)\n e. 反馈知识缺口到 knowledge-gaps.md",
|
||||
"timeoutSeconds": 1800
|
||||
},
|
||||
"delivery": { "mode": "announce" }
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点**:
|
||||
- `delivery.mode: "announce"`:淘汰/提升报告投递到 Control UI
|
||||
- `timeoutSeconds: 1800`(30 分钟):全量 JSONL 扫描是最重的操作
|
||||
- 每周一次频率足够——Skill 引用变化不会很快
|
||||
- 淘汰决策通过 skill_workshop quarantine 执行,提升决策通过手动编辑 AGENTS.md
|
||||
|
||||
### Cron 创建方式
|
||||
|
||||
使用 openclaw cron 工具创建。**不是 moziplus 管理**,而是直接在 openclaw 层面配置。
|
||||
|
||||
创建顺序:先 S3(L1),再 S4(L2),最后 S5(IMPROVE)。
|
||||
|
||||
创建后验证:`cron(action=list)` 确认所有 job 存在且 enabled=true。
|
||||
|
||||
### 去重和幂等
|
||||
|
||||
每个 cron job 的 name 包含 agent 名,避免命名冲突:
|
||||
- `l1-distill-zhangfei`
|
||||
- `l1-distill-guanyu`
|
||||
- `l1-distill-zhaoyun`
|
||||
- `l1-distill-simayi`
|
||||
- `l1-distill-pangtong`
|
||||
- `l1-distill-jiangwei`
|
||||
- `l2-review-pangtong`
|
||||
- `improve-weekly-pangtong`
|
||||
|
||||
创建前先 `cron(action=list)` 检查同名 job 是否已存在,避免重复创建。
|
||||
|
||||
## 11B. 一致性偏差修复清单(S6 补充)
|
||||
|
||||
§19 设计-实现一致性检查(2026-06-18)发现以下偏差,列入 S6 一并修复:
|
||||
|
||||
| # | 偏差 | 严重度 | 修复方式 |
|
||||
|---|------|--------|--------|
|
||||
| B4 | ticker.py:336-348 ExperienceDistiller 调用未移除 | 中 | 移除 experience_distiller 参数和调用,日志改为 debug 级空转提示 |
|
||||
| B5 | skill_system.py / experience.py 未标记 deprecated | 低 | 文件头部加 `# DEPRECATED — §19 重设计,不再参与 skill 发现/加载` 注释 |
|
||||
| B6 | SELF_IMPROVEMENT_REMINDER.md 引用残留 | 低 | AGENTS.md 中已标注废弃,但 system prompt 仍注入。从 workspace 文件列表中移除该文件 |
|
||||
|
||||
**B4 修复细节**:
|
||||
|
||||
ticker.py 构造函数 `__init__` 接受 `experience_distiller` 参数(默认 None),tick() 中第 336-348 行有条件调用。修复方式:
|
||||
- 保留参数(向后兼容),但条件块内加 `logger.debug("ExperienceDistiller deprecated per §19, skipping")` 后直接 return
|
||||
- 不删除代码(P3 级清理时再做物理删除)
|
||||
|
||||
**B6 修复细节**:
|
||||
|
||||
当前 Project Context 中注入了 `SELF_IMPROVEMENT_REMINDER.md` 的内容。该文件在 workspace-pangtong 中已不存在(被删),但 system prompt 模板仍引用它。修复方式:确认文件不存在即可——openclaw 会跳过不存在的注入文件。实际已无影响,标注为 resolved。
|
||||
|
||||
## 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 都需要触发这个 skill(DISCOVER 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 create(pending 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 skills(openclaw 自带,不动)
|
||||
/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 Skill(feishu 等)
|
||||
|
||||
# ━━━━━━━ Cron 产出流转 ━━━━━━━━
|
||||
|
||||
# L1(03:00 各 agent)
|
||||
# 输入:~/.openclaw/agents/<agent_id>/sessions/*.jsonl
|
||||
# 产出:skill_workshop create → proposal(pending)
|
||||
# 存储:~/.openclaw/workspace-<agent>/.skill-workshop/proposals/
|
||||
|
||||
# L2(05: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 中
|
||||
```
|
||||
@@ -0,0 +1,380 @@
|
||||
---
|
||||
title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面"
|
||||
created: 2026-06-19
|
||||
version: v2.1 draft
|
||||
status: superseded by §21 (toolchain 部分)
|
||||
changelog: v2.1 修正 M1(dispatcher 直接 SQL 声明)+ M2(Phase 格式)+ S1/S2(TaskAdapter 残留清理)
|
||||
v2.0 纳入姜维+司马懿 Review 反馈 + 庞统 Repository 模式修正
|
||||
v1.1 纳入姜维 Review 反馈
|
||||
v1.0 初版
|
||||
---
|
||||
|
||||
# Issue-Centric Orchestration
|
||||
|
||||
> **作者**: 庞统(副军师)🐦
|
||||
> **日期**: 2026-06-19
|
||||
> **定位**: 将黑板 DB 的协作面迁移到 Gitea Issue,daemon 逻辑保持不变
|
||||
> **前置文档**: PRD-v3.0(共享意识空间)、§14 TaskTypeRegistry、§17 ToolchainHandler
|
||||
|
||||
---
|
||||
|
||||
## §1. 设计目标
|
||||
|
||||
| # | 目标 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | 黑板 DB 协作面迁移到 Gitea Issue | 需求、讨论、产出从黑板 DB 迁到 Issue |
|
||||
| 2 | 成果物以 Gitea 为基础存放 | 分支 commit + PR |
|
||||
| 3 | webhook 部分替代 ticker | 主动触发替代轮询,ticker 保留兜底 |
|
||||
| 4 | task 状态 + spawner 逻辑不变 | daemon 内部状态机不变 |
|
||||
| 5 | prompt 改造 | 黑板 API 引用改为 Gitea API,告知 agent 使用 Gitea 协作 |
|
||||
|
||||
**核心原则**: 只有数据存储位置变了(黑板 DB → Gitea Issue),daemon 的调度逻辑(dispatcher/ticker/spawner)不变。
|
||||
|
||||
---
|
||||
|
||||
## §2. 现状分析
|
||||
|
||||
### 2.1 黑板 DB 当前承担的角色
|
||||
|
||||
根据 PRD-v3.0,黑板是**共享意识空间**——所有 agent 通过它读写状态、感知变化、协调工作。
|
||||
|
||||
黑板 DB 包含 14 张表:
|
||||
|
||||
| 表 | 用途 | 分类 |
|
||||
|---|------|------|
|
||||
| tasks | 任务(标题、描述、状态、指派、retry) | 协作面 + 执行面 |
|
||||
| comments | 讨论、@mention、action_report | 协作面 |
|
||||
| outputs | 产出物(文本摘要、文件路径) | 协作面 |
|
||||
| events | 事件流(SSE 推送) | 协作面 |
|
||||
| reviews | 审查记录(verdict、round、consensus) | 执行面 |
|
||||
| checkpoints | 阶段审查(approve/reject) | 执行面 |
|
||||
| decisions | 决策记录 | 执行面 |
|
||||
| observations | 风险观察 | 执行面 |
|
||||
| experiences | 经验沉淀 | 执行面 |
|
||||
| routing_decisions | 路由记录 | 执行面 |
|
||||
| task_attempts | 重试历史 | 执行面 |
|
||||
| mention_queue | @mention 队列 | 执行面 |
|
||||
| experience_tags | 经验标签 | 执行面(§19 已标记废弃) |
|
||||
| agents | Agent 注册信息 | 执行面 |
|
||||
|
||||
**协作面**(tasks/comments/outputs/events)= 迁移到 Gitea Issue
|
||||
**执行面**(reviews/checkpoints/decisions 等)= 保留在 daemon 内部
|
||||
|
||||
### 2.2 daemon 数据访问方式
|
||||
|
||||
当前 daemon 三个核心模块如何读写黑板 DB:
|
||||
|
||||
| 模块 | 读什么 | 怎么读 |
|
||||
|------|-------|-------|
|
||||
| ticker | pending task 列表 | `SELECT * FROM tasks WHERE status='pending'` (SQLite) |
|
||||
| dispatcher | task 详情(title/description/must_haves) | `Task.from_row(row)` 从 SQLite 行构建 |
|
||||
| spawner | task 上下文构建 prompt | 从 task 对象的 title/description/must_haves 字段 |
|
||||
|
||||
| 模块 | 写什么 | 怎么写 |
|
||||
|------|-------|-------|
|
||||
| dispatcher | task status(pending→claimed→working) | `UPDATE tasks SET status=?` |
|
||||
| spawner | task status(working→done/failed) | `UPDATE tasks SET status=?` |
|
||||
| handler | comment / output | `INSERT INTO comments/outputs` |
|
||||
|
||||
**关键发现**: daemon 大量依赖 `SELECT * FROM tasks WHERE status=?` 这种 SQL 查询来发现和调度 task。如果数据源迁到 Gitea Issue,这些查询的方式会变(从 SQLite 变为 Gitea API 或本地索引),但**查询的语义和返回的数据结构不变**。
|
||||
|
||||
---
|
||||
|
||||
## §3. 目标架构
|
||||
|
||||
### 3.1 分层
|
||||
|
||||
```
|
||||
Gitea(协作介质,替代黑板 DB 协作面)
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Issue #42: "实现功能 A" │
|
||||
│ body: 需求描述 + 验收标准 │
|
||||
│ assignee: zhangfei-dev │
|
||||
│ labels: type/feat, priority/P2 │
|
||||
│ comments: 讨论、@mention、进展汇报 │
|
||||
│ │
|
||||
│ 分支: fix/42-feature-a │
|
||||
│ PR #43: fix/42 → main │
|
||||
│ CI: lint + test │
|
||||
│ Review: APPROVE / REQUEST_CHANGES │
|
||||
│ → merge → Issue auto-close │
|
||||
└──────────────────────────────────────────┘
|
||||
↕ webhook(被动) ↕ API(主动)
|
||||
┌──────────────────────────────────────────┐
|
||||
│ daemon(执行引擎,内部状态管理不变) │
|
||||
│ │
|
||||
│ task_state(轻量索引,替代 tasks 表): │
|
||||
│ issue_number → status, branch, retry │
|
||||
│ │
|
||||
│ 执行面表(不变): │
|
||||
│ reviews, checkpoints, decisions, │
|
||||
│ observations, experiences, │
|
||||
│ routing_decisions, task_attempts │
|
||||
│ │
|
||||
│ 调度逻辑(不变): │
|
||||
│ ticker → 扫 task_state → dispatch │
|
||||
│ dispatcher → 读 Gitea Issue → spawn │
|
||||
│ spawner → 读 Gitea Issue → prompt │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 数据映射
|
||||
|
||||
| 黑板 DB | Gitea 对应 | 迁移方式 |
|
||||
|---------|-----------|---------|
|
||||
| tasks.title | Issue.title | 直接对应 |
|
||||
| tasks.description | Issue.body | 直接对应 |
|
||||
| tasks.assignee | Issue.assignee | 直接对应 |
|
||||
| tasks.status (pending/working/review/done) | daemon task_state 内部维护 | Issue open/closed 只表示生命周期 |
|
||||
| tasks.priority | Issue label (priority/P0-P3) | label 模拟 |
|
||||
| tasks.must_haves (JSON) | daemon task_state 内部存储 | daemon 专用元数据 |
|
||||
| tasks.depends_on | Issue blocked_by | Gitea 原生 dependency |
|
||||
| tasks.parent_task | Issue body 引用(如 `Parent: #42`) | 约定 |
|
||||
| tasks.retry_count / dispatch_count | daemon task_state 内部维护 | 执行面数据 |
|
||||
| comments | Issue comment | 直接对应 |
|
||||
| outputs | 分支 commit(代码/文档)+ Issue comment(摘要) | 成果物载体迁移 |
|
||||
| events | webhook | 主动推送替代 SSE |
|
||||
|
||||
### 3.3 数据访问层改造:Repository 模式换底
|
||||
|
||||
**设计原则**:改造现有 Repository(Queries/Blackboard 类)的实现,不新增中间层。
|
||||
|
||||
当前数据访问层已有 Repository 模式的基础——`Blackboard` 类和 `Queries` 类封装了所有数据访问,上层(dispatcher/spawner/ticker)通过方法调用(`queries.pending_dispatchable()`、`blackboard.get_task()`),不直接写 SQL。
|
||||
|
||||
改造做法:**Repository 接口不变,实现从 SQLite-only 改为 Gitea + SQLite。**
|
||||
|
||||
```
|
||||
现在:
|
||||
dispatcher → Queries(SQLite) → 黑板 DB
|
||||
|
||||
改造后:
|
||||
dispatcher → Queries(Gitea-backed) → Gitea API(协作数据)+ SQLite(执行状态)
|
||||
```
|
||||
|
||||
上层代码**基本**不用改。但有一个前置条件:dispatcher.py 中存在约 20 处直接操作 tasks 表的 SQL(绕过 Queries 类,如 `SELECT assignee FROM tasks`、`UPDATE tasks SET status=?`)。这些直接 SQL 需要先迁移到 Queries 方法调用,才能实现 Repository 换底。此项作为 Phase 1 的前置工作。
|
||||
|
||||
Queries 内部决定数据从哪来:
|
||||
|
||||
| 数据类型 | 来源 | 方式 |
|
||||
|---------|------|------|
|
||||
| title / description / assignee / labels | Gitea Issue | API 读取(webhook 触发时缓存) |
|
||||
| comment / 讨论 | Gitea Issue comment | API 读取 |
|
||||
| status / retry_count / dispatch_count | 本地 SQLite | 原有逻辑不变 |
|
||||
| daemon_meta(原 must_haves) | 本地 SQLite | 原有逻辑不变 |
|
||||
|
||||
**本地 SQLite 表(执行状态,Gitea 没有的)**:
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_state (
|
||||
issue_number INTEGER, -- Gitea Issue 编号
|
||||
repo TEXT, -- 仓库名
|
||||
status TEXT DEFAULT 'pending', -- daemon 内部状态机
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
dispatch_count INTEGER DEFAULT 0,
|
||||
max_retries INTEGER DEFAULT 2,
|
||||
daemon_meta TEXT, -- JSON(event_type, steps 等 daemon 元数据)
|
||||
issue_body_cache TEXT, -- 缓存的 Issue body(优化用,可失效重拉)
|
||||
issue_updated_at TEXT, -- Gitea Issue 的 updated_at(缓存失效判断)
|
||||
created_at TEXT,
|
||||
updated_at TEXT,
|
||||
PRIMARY KEY (issue_number, repo)
|
||||
);
|
||||
```
|
||||
|
||||
**为什么不用 TaskAdapter**:Repository 内部已经把 Gitea 数据 + 本地执行状态合并成 Task 对象返回。上层调用 `queries.get_task(issue_number)` 得到的 Task 对象和现在一模一样——有 title、有 description、有 status。不需要额外 adapter 层。
|
||||
|
||||
---
|
||||
|
||||
## §4. 流程设计
|
||||
|
||||
### 4.1 创建 Task
|
||||
|
||||
```
|
||||
① 庞统/主公在 Gitea 创建 Issue + 指派 agent
|
||||
② Gitea webhook: issues/assigned
|
||||
③ daemon toolchain handler 收到 webhook
|
||||
→ 在 task_state 插入一行(issue_number, repo, assignee, status=pending)
|
||||
④ ticker 扫 task_state 发现 pending → dispatch → spawn agent
|
||||
```
|
||||
|
||||
**和现在的区别**: 当前是庞统在黑板 API 创建 task。改造后是庞统在 Gitea 创建 Issue,webhook 自动触发 daemon 建索引。
|
||||
|
||||
### 4.2 执行 Task
|
||||
|
||||
```
|
||||
① dispatcher 扫 task_state 发现 pending task
|
||||
② spawner 从 Gitea API 读 Issue body(需求描述)
|
||||
③ spawner 用 Issue body 构建 prompt(替代从黑板 DB 读 description)
|
||||
→ prompt 结构: Issue body(需求)+ PromptSection 注入(工作流程、约束、API 指引)
|
||||
④ agent 收到 prompt → 执行
|
||||
⑤ agent 在 Gitea Issue comment 汇报进展(替代黑板 comment)
|
||||
⑥ daemon 更新 task_state status=working
|
||||
```
|
||||
|
||||
### 4.3 审查
|
||||
|
||||
```
|
||||
① agent 编码完成 → push 到分支 → 创建 PR
|
||||
② Gitea webhook: pull_request/opened
|
||||
→ daemon 更新 task_state status=review
|
||||
③ Reviewer 在 Gitea 做 PR Review
|
||||
④ Gitea webhook: pull_request_review
|
||||
→ daemon 根据 Review 结果更新 task_state
|
||||
⑤ Review 通过 → PR merge
|
||||
→ Gitea 自动关闭 Issue
|
||||
→ Gitea webhook: issues/closed
|
||||
→ daemon 更新 task_state status=done
|
||||
```
|
||||
|
||||
**commit message 约定**(司马懿 S3):Gitea PR merge 自动关闭 Issue 需要 commit message 包含 `Closes #N` 或 `Fixes #N` 关键词。agent 创建 PR 时在描述中加上此约定,确保 merge 后 Issue 自动关闭。
|
||||
|
||||
**审查统一走 PR Review**——不区分设计审查和代码审查,所有成果物都在分支上,Reviewer 一次性审。
|
||||
|
||||
### 4.4 CI 失败处理
|
||||
|
||||
```
|
||||
① PR 创建 → CI 自动跑
|
||||
② CI 失败 → Gitea webhook: pull_request(CI status)
|
||||
→ daemon toolchain handler 创建 ci_failure toolchain task
|
||||
→ 指派给 PR 作者
|
||||
→ agent 按 ci_failure steps 处理(已有逻辑,不变)
|
||||
③ agent 修复 → push 到同分支 → PR 自动更新 → CI 重跑
|
||||
```
|
||||
|
||||
### 4.5 ticker 兜底
|
||||
|
||||
webhook 可能丢失或延迟。ticker 保留原有逻辑,改为:
|
||||
- 扫 `task_state` 中 status=pending 的记录(替代扫黑板 tasks 表)
|
||||
- 扫 `task_state` 中 status=working 但超时的记录
|
||||
- 如果发现 Gitea Issue 已 closed 但 task_state 还是 working → 更新为 done
|
||||
|
||||
---
|
||||
|
||||
## §5. daemon 需要改的地方
|
||||
|
||||
**原则: daemon 调度逻辑不变,只改数据访问层。**
|
||||
|
||||
### 5.1 数据访问层改造
|
||||
|
||||
| 模块 | 现在 | 改造后 | 影响范围 |
|
||||
|------|------|-------|---------|
|
||||
| `queries.pending_dispatchable()` | `SELECT * FROM tasks WHERE status='pending'` | `SELECT * FROM task_state WHERE status='pending'` | SQL 改表名 |
|
||||
| `Task.from_row(row)` 构建 task 对象 | 从 tasks 表行直接取 title/description | 从 task_state 取 issue_number → 调 Gitea API 读 Issue title/body | 需要新增 Gitea API 调用 |
|
||||
| `UPDATE tasks SET status=?` | 直接更新 SQLite | 更新 task_state(SQLite) | SQL 改表名 |
|
||||
| `INSERT INTO comments` | 写黑板 DB | 改为 Gitea Issue comment API | 需要新增 Gitea API 调用 |
|
||||
|
||||
### 5.2 ⚠️ 需要讨论的改造点
|
||||
|
||||
以下是因为数据源变了,daemon 实现需要调整的地方:
|
||||
|
||||
**P1: spawner 每次 spawn 都要调 Gitea API 读 Issue body?**
|
||||
|
||||
当前 spawner 从 SQLite 读 task description(微秒级)。改为从 Gitea API 读(毫秒级,HTTP 请求)。
|
||||
|
||||
- **方案 A**: 每次 spawn 时实时调 Gitea API。简单但慢
|
||||
- **方案 B**: webhook 触发时缓存 Issue body 到 `task_state.issue_body_cache` + `issue_updated_at`。spawn 时从缓存读
|
||||
- **缓存失效**: spawner 构建 prompt 时比对 `issue_updated_at` 和 Gitea API 的 Issue `updated_at`,不匹配才重新拉取(正常情况 Issue body 创建后不改,开销可忽略)
|
||||
- **推荐**: 方案 B + updated_at 校验
|
||||
|
||||
**P2: Repository 内部数据合并**
|
||||
|
||||
Queries 类内部改造——`get_task()` 方法从 Gitea API(或缓存)读 title/body,从 task_state 表读 status/retry_count,合并成 Task 对象返回。上层(dispatcher/spawner)无感知。
|
||||
|
||||
这是标准的 Repository 模式——接口不变(`get_task(id)` 返回 Task 对象),实现换底(从 SQLite 单源改为 Gitea + SQLite 双源)。不新增 TaskAdapter 类——Queries 类本身就是 Repository,内部合并是职责内的事。
|
||||
|
||||
**P3: agent 的 prompt 中引用黑板 API 的地方需要改**
|
||||
|
||||
PromptSection 中有多处 `POST localhost:8083/api/projects/.../tasks/.../comments`(黑板 API)。这些要改为 Gitea API:
|
||||
- `task_handler.py` TaskApiSection: `POST .../status` 和 `POST .../outputs` → 不需要(daemon 通过 webhook 自动感知状态)
|
||||
- `toolchain_handler.py` ToolchainApiSection: `POST .../comments`(action report)→ 改为 `POST Gitea API .../issues/.../comments`
|
||||
- `toolchain_handler.py` ToolchainApiSection: `POST .../outputs` → 改为"push 到分支"指引
|
||||
|
||||
**P4: comments 表的 @mention 机制**
|
||||
|
||||
当前 @mention 通过黑板 `mention_queue` 表排队。改造后 @mention 通过 Gitea Issue/PR comment(webhook 自然触发)。但 mention_queue 的消费逻辑(ticker 扫描 → 通知 → agent 处理)需要适配。
|
||||
|
||||
- **方案**: mention_queue 保留,但数据来源从黑板 comment 改为 Gitea webhook payload
|
||||
- **⚠️ 适配层**: Gitea webhook payload 中的 comment body 是完整 markdown 文本(不像黑板 comment 是结构化 JSON,有 comment_type、author 等字段)。mention_queue 消费侧需要适配:从 webhook payload 的 `comment.body` 中正则提取 @mention,而非 SQL WHERE 精确查询。这层适配在 Phase 4 mention 迁移时细化
|
||||
- **影响**: ticker 中的 mention 处理逻辑需要适配
|
||||
- **action_report 识别**(司马懿 S2):黑板 comment 有结构化 `comment_type=action_report` 字段,Gitea comment 只有 markdown body。迁移后用 **body 中的固定标记**识别,如 `<!-- action_report -->` 或约定 body 以 `[Action Report]` 开头。具体格式在 Phase 4 实施时确定。
|
||||
|
||||
---
|
||||
|
||||
## §6. prompt 改造
|
||||
|
||||
### 6.1 受影响的 PromptSection
|
||||
|
||||
| Section | 文件 | 当前内容 | 改造后 |
|
||||
|---------|------|---------|-------|
|
||||
| TaskApiSection | task_handler.py | 黑板 API(status 回写、outputs 提交) | 删除 status 回写(daemon 自动管);outputs 改为 git push |
|
||||
| ToolchainApiSection | toolchain_handler.py | 黑板 API(action_report comment、outputs) | action_report 改为 Issue comment;outputs 改为 git push |
|
||||
| TaskConstraintsSection | task_handler.py | "blackboard comment" 引用 | 改为 "Issue/PR comment" |
|
||||
| ToolchainConstraintsSection | toolchain_handler.py | 已禁止 Mail | 同时告知 agent 使用 Gitea 协作 |
|
||||
|
||||
### 6.2 agent prompt 新增指引
|
||||
|
||||
agent 需要知道工作方式变了。新增一个通用 section(或加入现有 constraints section):
|
||||
|
||||
```
|
||||
## 协作方式
|
||||
- 你的任务通过 Gitea Issue 管理
|
||||
- 需求描述在 Issue body 中
|
||||
- 进展汇报通过 Issue comment
|
||||
- 代码产出通过分支 commit + PR
|
||||
- 审查通过 PR Review
|
||||
- 不要使用黑板 API,不要使用 Mail API
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §7. Issue ↔ 分支 ↔ PR 关系
|
||||
|
||||
| 场景 | Issue : 分支 : PR |
|
||||
|------|------------------|
|
||||
| 简单任务(bugfix、小功能) | 1 : 1 : 1 |
|
||||
| 复杂任务(多阶段) | 1 : 1 : N(分阶段提交 PR,同一个分支) |
|
||||
| 极复杂任务(需拆解) | 1 : N : N(Issue body 列出子任务,每个一个分支+PR) |
|
||||
|
||||
**默认 1:1:1**。分支命名规范不变:`fix/{issue_number}-{brief}`。
|
||||
|
||||
---
|
||||
|
||||
## §8. 不做的事
|
||||
|
||||
| 不做 | 理由 |
|
||||
|------|------|
|
||||
| 不做数据迁移 | 主公确认当前无正式使用数据 |
|
||||
| 不做 Issue 状态 label(status/xxx) | 中间状态 daemon 内部管,Issue 只有 open/closed |
|
||||
| 不改 Mail | Mail 职责不变。Issue-centric 模式下 agent 不用 Mail(通过 Issue/PR comment 协作)。Mail 剩余职责:非 Gitea 相关的 agent 间点对点通知(如庞统通知赵云准备数据) |
|
||||
| 不改前端(本阶段) | 前端改造独立于后端,后续设计 |
|
||||
| 不做存量 task 退役 | 原 task 流程和 Issue 流程可共存,原 task 自然退役 |
|
||||
| 不改 experiences/checkpoints/decisions 表 | 执行面表保留在 daemon,不受影响 |
|
||||
|
||||
---
|
||||
|
||||
## §9. 实施路径
|
||||
|
||||
| 阶段 | 内容 | 依赖 |
|
||||
|------|------|------|
|
||||
| 阶段 | 内容 | 依赖 |
|
||||
|------|------|------|
|
||||
| Phase 0 | webhook 权限配置(主公手动配)+ CI status webhook 验证(确认 Gitea CI status 变化触发 webhook) | 设计 Review 通过 |
|
||||
| Phase 1 | dispatcher.py 直接 SQL(约 20 处)迁移到 Queries 方法调用 + task_state 表创建 + Queries/Blackboard 内部改造(Gitea + SQLite 双源)+ must_haves→daemon_meta 重命名(17 文件 80 处,机械替换,需 CI 覆盖) | Phase 0 |
|
||||
| Phase 2 | dispatcher/ticker 数据源从 tasks 表切换到 task_state | Phase 1 |
|
||||
| Phase 3 | spawner 读 Issue body 构建 prompt(替代读黑板 description)+ issue_updated_at 缓存失效机制 | Phase 2 |
|
||||
| Phase 4 | prompt 改造(黑板 API → Gitea API)+ mention_queue 适配(action_report body 标记约定) | Phase 3 |
|
||||
| Phase 5 | 验证 + 清理废弃的黑板协作面表 | Phase 4 |
|
||||
|
||||
每个 Phase 独立可验证,出问题可以回退。
|
||||
|
||||
---
|
||||
|
||||
## §10. 风险评估
|
||||
|
||||
| 风险 | 等级 | 缓解 |
|
||||
|------|------|------|
|
||||
| Gitea API 不可用时 daemon 完全瘫痪 | 中 | webhook 触发时缓存 Issue body + issue_updated_at 校验(P1 方案 B),减少运行时 Gitea API 依赖 |
|
||||
| Gitea webhook 丢失 | 低 | ticker 兜底扫描 |
|
||||
| task_state 和 Gitea Issue 状态不一致 | 中 | ticker 定期校验(发现 Issue closed 但 index 未更新则修复) |
|
||||
| spawner 性能下降(Gitea API 调用) | 低 | 方案 B 缓存 Issue body,spawn 时不调 Gitea API |
|
||||
| 原 task 流程和新 Issue 流程共存期混乱 | 中 | 可以限定只在特定项目中启用 Issue 模式,逐步切换 |
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,405 @@
|
||||
---
|
||||
title: "End-to-End Flow — 端到端任务流程设计"
|
||||
created: 2026-06-22
|
||||
version: v1.1
|
||||
status: draft
|
||||
changelog: v1.1 补充轻量路径设计(§22.6)、数据流澄清(§22.7)、修正设计原则3、统一Phase编号、Phase 1标注
|
||||
v1.0 初版
|
||||
---
|
||||
|
||||
# End-to-End Flow — 端到端任务流程设计
|
||||
|
||||
> 本文档描述一个任务从发起到结束的**完整系统行为链路**。
|
||||
> §21 各章节按功能点分章,本文档把它们串成一条端到端流程。
|
||||
> 每个 Phase 标注:触发源、daemon 函数、prompt 来源、agent 行为、留痕位置。
|
||||
|
||||
---
|
||||
|
||||
## §22.1 流程总览
|
||||
|
||||
```
|
||||
Phase 0: 庞统创建 parent Issue(无 assignee, 有 type/* label)
|
||||
↓ webhook: issues/opened
|
||||
Phase 1: Discussion 广播(所有空闲 agent)
|
||||
↓ agent 在 Gitea Issue comment 讨论 → 创建 sub Issue(assign 自己)
|
||||
Phase 2: sub Issue assigned → executor 分派
|
||||
↓ webhook: issues/assigned
|
||||
Phase 3: 编码 + PR + CI
|
||||
↓ webhook: pull_request/opened → CI 自动触发
|
||||
↓ CI 通过 → 等 Review / CI 失败 → agent 修复 → 重跑
|
||||
Phase 4: Review(司马懿)
|
||||
↓ APPROVED → 通知 agent 合并 / REQUEST_CHANGES → agent 修改 → 回 Phase 3
|
||||
Phase 5: Merge + sub Issue 自动关闭
|
||||
↓ webhook: pull_request/closed(merged)
|
||||
Phase 6: Round Review(庞统三问)
|
||||
↓ GOAL_ACHIEVED → 关闭 parent / 需要新轮 → 创建新 sub → 回 Phase 2
|
||||
Phase 7: parent Issue 关闭
|
||||
↓ webhook: issues/closed
|
||||
```
|
||||
|
||||
**核心设计原则**:
|
||||
|
||||
1. **协作面是 Gitea**(Issue/PR comment),不是黑板 DB
|
||||
2. **每个 Phase 有明确的前置条件和产出**,前一步未完成则后续不触发
|
||||
3. **webhook 和 ticker 协同**——webhook 负责事件感知和 task 创建,ticker 负责广播调度(Phase 1)和聚合检测(Phase 6)
|
||||
4. **agent 在 Gitea 留痕**(Issue comment、PR、Review),黑板 DB 只存 daemon 内部状态
|
||||
|
||||
---
|
||||
|
||||
## §22.2 阶段详解
|
||||
|
||||
### Phase 0: parent Issue 创建
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | 庞统(或用户)在 Gitea 创建 parent Issue |
|
||||
| **条件** | 无 assignee + 有 `type/*` label |
|
||||
| **daemon 函数** | `_handle_issues`(`toolchain_routes.py`),opened 分支 |
|
||||
| **daemon 行为** | 检测无 assignee + 有 type/* label → 创建 toolchain task(`assignee=None`, `action_type=issue_discussion`)写入 `_toolchain` DB |
|
||||
| **agent 行为** | 无(此阶段不 spawn agent) |
|
||||
| **产出** | `_toolchain` DB 中一条 pending task |
|
||||
|
||||
**webhook 流转**:
|
||||
|
||||
```
|
||||
Gitea: Issue created (no assignee, label type/feat)
|
||||
→ webhook: issues/opened
|
||||
→ daemon: _handle_issues → action="opened"
|
||||
→ 非部署失败 + 无 assignee + 有 type/* label
|
||||
→ _send_toolchain_task(to_agent=None, action_type="issue_discussion")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Discussion 广播
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | ticker 30s 扫到 Phase 0 创建的 pending task |
|
||||
| **daemon 函数** | `ticker._dispatch_pending` → `dispatcher.decide` → `_broadcast_claim`。⚠️ 当前 broadcast 使用 claim prompt(黑板 API)而非 discussion prompt,P0 修复后改为 discussion broadcast |
|
||||
| **daemon 行为** | assignee=None → router 返回 mode=delegate → ticker 归入 broadcast_tasks → 广播给所有空闲 agent |
|
||||
| **prompt 来源(设计期望)** | `discussion prompt`(§13.2):你是谁 + 你必须做什么(4 维度)+ Gitea API + Boids 行为准则 |
|
||||
| **agent 行为(设计期望)** | 每个 agent 在 **Gitea Issue** comment(角色名开头,4 维度回应);需要参与的 agent 创建 sub Issue(Gitea API,assign 自己);在 parent Issue comment 注册 sub |
|
||||
| **产出** | parent Issue 上有所有 agent 的讨论 comment;Gitea 上有若干 sub Issue |
|
||||
|
||||
**discussion prompt 核心(§13.2)**:
|
||||
|
||||
```
|
||||
你被 spawn 来参与 Gitea Issue 讨论。
|
||||
|
||||
## 讨论主题
|
||||
{parent Issue body 全文}
|
||||
|
||||
## 你是谁
|
||||
你是 {agent_id}({display_name}),角色是 {role},能力是 {capabilities}
|
||||
|
||||
## 你必须做什么(每个 agent 必须 comment)
|
||||
1.【定位】这个需求和你有什么关系?
|
||||
2.【建议】你对实现方案有什么建议?
|
||||
3.【认领】如果你需要参与,创建 sub Issue 并在 parent comment 注册:
|
||||
POST /repos/{repo}/issues
|
||||
title: "[moz][sub][parent #{N}] 任务名"
|
||||
body: "Parent: #{N}\nDepends: #M\n## 任务\n..."
|
||||
assignees: ["{你的 agent_id}"]
|
||||
4.【风险】如果发现风险或不合理假设,直接提出
|
||||
|
||||
## Comment 格式
|
||||
[角色名] 你的观点
|
||||
|
||||
## API(Gitea)
|
||||
- 读 Issue: GET /repos/{repo}/issues/{N}
|
||||
- 写 comment: POST /repos/{repo}/issues/{N}/comments
|
||||
- 创建 sub Issue: POST /repos/{repo}/issues
|
||||
```
|
||||
|
||||
**Phase 1 的结束条件**(⚠️ 设计目标,检测逻辑待实现):
|
||||
|
||||
- 所有被广播的 agent 都已 comment(或 NO_REPLY)——daemon 需维护"已回复 agent"集合,当前无此机制
|
||||
- 至少有一个 agent 创建了 sub Issue
|
||||
- 如果没有任何 agent 创建 sub Issue → ticker 升级庞统(3 轮无 taker 机制)
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: sub Issue assigned → executor 分派
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent 在 Phase 1 创建 sub Issue 时 assign 自己 → Gitea 发 `issues/assigned` webhook |
|
||||
| **daemon 函数** | `_handle_issues`(`toolchain_routes.py`),assigned 分支 |
|
||||
| **daemon 行为** | 解析 type/* label → 确定 business_type → 从 `toolchain-templates.yaml` 获取 steps → 创建 toolchain task(`action_type=issue_assigned`)→ ticker dispatch |
|
||||
| **prompt 来源** | `ToolchainHandler.build_prompt`(通过 PromptComposer 拼装 sections) |
|
||||
| **agent 行为** | 建分支 `{type}/{sub_issue_number}-{brief}` → 编码 → 写测试 → 创建 PR(body 含 `Closes #N` + `Parent #M`) |
|
||||
| **产出** | Gitea 上有 PR + 分支有代码 |
|
||||
|
||||
**steps 来源(`config/toolchain-templates.yaml`)**:
|
||||
|
||||
```yaml
|
||||
issue_assigned:
|
||||
feat:
|
||||
steps:
|
||||
- "理解需求(Issue body)"
|
||||
- "git checkout -b feat/{issue_number}-{brief}"
|
||||
- "编码 + 写 UT"
|
||||
- "文档同步检查"
|
||||
- "git push + 创建 PR(body 含 Closes #{issue_number})"
|
||||
- "等 Review"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 编码 + PR + CI
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent 创建 PR → webhook `pull_request/opened` → CI 自动触发 |
|
||||
| **daemon 函数(PR opened)** | `_handle_pull_request` → 创建 `review_request` task → 通知司马懿 |
|
||||
| **daemon 函数(CI)** | CI 结果通过 Gitea comment 反馈 → `_handle_issue_comment` CI 路径 |
|
||||
| **CI 通过** | 等待 Review(Phase 5) |
|
||||
| **CI 失败** | 创建 `ci_failure` task → 通知 agent 修复 |
|
||||
| **agent prompt(CI 失败时)** | `ToolchainHandler.build_prompt`,action_type=ci_failure,steps 含 CI 日志查看 + 根因判断 + 修复 |
|
||||
| **产出** | CI 通过的 PR |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Review(司马懿)
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | `review_request` task 被 ticker dispatch 给司马懿 |
|
||||
| **daemon 函数** | ticker spawn 司马懿 → 司马懿通过 Review API 提交 verdict → webhook `pull_request_review` → `_handle_pull_request_review` |
|
||||
| **prompt 来源** | `ToolchainHandler.build_prompt`,action_type=review_request |
|
||||
| **Review APPROVED** | 创建 `review_result_approved` task → 通知 agent 合并 |
|
||||
| **Review REQUEST_CHANGES** | 创建 `review_result_request_changes` task → 通知 agent 修改 → agent push 同分支 → CI 重跑 → 回 Phase 3 |
|
||||
| **产出** | Gitea Review 记录 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Merge + sub Issue 关闭
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | agent merge PR |
|
||||
| **daemon 函数** | Gitea 自动关闭 sub Issue(PR body 含 `Closes #N`)→ webhook `pull_request/closed(merged)` → `_handle_pull_request` closed 分支 |
|
||||
| **daemon 行为** | 创建 `review_merged` task → 通知 agent(纯通知);ToolchainHandler verify: auto-pass |
|
||||
| **产出** | sub Issue closed + 分支自动删除 |
|
||||
|
||||
---
|
||||
|
||||
### Phase 6: Round Review(庞统三问)
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | daemon 检测 parent Issue 下所有 sub Issue 终态(done/closed) |
|
||||
| **daemon 函数** | `ticker._check_round_complete` 扫描 parent/sub 映射 → 所有 sub 终态 → spawn 庞统 |
|
||||
| **prompt 来源** | `ticker._build_review_prompt`(三问框架) |
|
||||
| **agent 行为** | 庞统通过 Gitea API 读 parent Issue body + 所有 sub Issue comments/outputs → 三问评估 |
|
||||
| **GOAL_ACHIEVED** | 庞统关闭 parent Issue → Phase 7 |
|
||||
| **需要新轮** | 庞统创建新 sub Issues → 回 Phase 2 |
|
||||
| **产出** | parent Issue closed 或新一轮 sub Issues |
|
||||
|
||||
**三问框架**:
|
||||
|
||||
```
|
||||
1. Goal 还清晰吗?(是否有 goal drift)
|
||||
2. 成果物覆盖 goal 了吗?(逐条检查验收标准 + docs/design 同步确认)
|
||||
3. 下一轮需要做什么?(创建新 sub / 标记完成 / 调整方向)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 7: parent Issue 关闭
|
||||
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | 庞统关闭 parent Issue |
|
||||
| **daemon 函数** | webhook `issues/closed` → `_handle_issues` closed 分支 |
|
||||
| **daemon 行为** | 创建 `issue_closed` task → 通知(纯通知,auto-pass) |
|
||||
| **产出** | parent Issue closed,全流程结束 |
|
||||
|
||||
---
|
||||
|
||||
## §22.3 Prompt 模板对照表
|
||||
|
||||
| Phase | prompt 用途 | 设计指定的模板来源 | 当前实际来源 | 一致? |
|
||||
|-------|-----------|----------------|------------|-------|
|
||||
| 1 Discussion | 广播讨论 | discussion prompt(§13.2,Gitea API) | claim prompt(`_build_claim_prompt`,黑板 API) | ❌ |
|
||||
| 2 Executor | 编码执行 | `ToolchainHandler.build_prompt` + YAML steps | 同设计 | ✅ |
|
||||
| 3 CI 失败 | 修复 CI | `ToolchainHandler.build_prompt` ci_failure | 同设计 | ✅ |
|
||||
| 4 Review | 审查 PR | `ToolchainHandler.build_prompt` review_request | 同设计 | ✅ |
|
||||
| 5 Merge | 合并通知 | `ToolchainHandler.build_prompt` review_merged | 同设计 | ✅ |
|
||||
| 6 Round Review | 庞统三问 | `ticker._build_review_prompt` | 同设计 | ✅ |
|
||||
| 7 Issue closed | 关闭通知 | `ToolchainHandler.build_prompt` issue_closed | 同设计 | ✅ |
|
||||
|
||||
**唯一偏差在 Phase 1**:discussion 设计了完整的 prompt(§13.2),但 ticker 从未调用它。
|
||||
|
||||
---
|
||||
|
||||
## §22.4 当前实现差距
|
||||
|
||||
| Phase | 实现状态 | 差距描述 |
|
||||
|-------|---------|---------|
|
||||
| 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task |
|
||||
| 1 Discussion 广播 | ❌ **未实现** | ticker broadcast 只有 `_build_claim_prompt`(黑板 API,认领模式),没有 discussion broadcast(Gitea API,讨论模式)。`_build_discussion_prompt` 存在于 spawner 中但 ticker 从未调用。agent 收到的是"认领并执行"而不是"讨论后创建 sub" |
|
||||
| 2 sub Issue → executor | ⚠️ 部分 | assigned 路径 + YAML steps 已实现(PR #107),但因为 Phase 1 断裂,agent 不会创建 sub Issue,走不到此阶段 |
|
||||
| 3 PR + CI | ✅ 已实现 | toolchain handler 正常处理 PR opened + CI 失败 |
|
||||
| 4 Review | ✅ 已实现 | Review 请求 + Review 结果通知正常 |
|
||||
| 5 Merge + sub 关闭 | ⚠️ 部分 | merge 通知正常。但 agent 创建的 PR body 不含 `Closes #N`(因为走的是 claim prompt 不是 executor prompt,prompt 中没有 Closes #N 约束) |
|
||||
| 6 Round Review | ❌ 未适配 | `_check_round_complete` 扫黑板 `parent_task` 字段,不扫 Gitea parent/sub Issue 映射。没有 sub Issue 就检测不到 |
|
||||
| 7 parent Issue 关闭 | ✅ 已实现 | PR #113 issue_closed auto-pass |
|
||||
|
||||
---
|
||||
|
||||
## §22.5 差距优先级排序
|
||||
|
||||
| 优先级 | 差距 | 影响范围 | 修复建议 |
|
||||
|--------|------|---------|---------|
|
||||
| **P0** | Phase 1 discussion broadcast | **核心断裂**。整个 Gitea Issue 流程退化成黑板 claim 模式 | ticker `_broadcast_claim` 中判断 `action_type=issue_discussion` → 调用 discussion prompt(Gitea API 版本)而非 claim prompt |
|
||||
| **P1** | Phase 5 PR body Closes #N | sub Issue 不自动关闭 | Phase 2 executor prompt 中已有 Closes #N 约束(YAML steps),Phase 1 修好后自然解决 |
|
||||
| **P2** | Phase 6 Round Review Gitea 适配 | 无法触发庞统三问 | `_check_round_complete` 改为扫 Gitea parent/sub Issue 映射,详见 §22.7 数据流设计 |
|
||||
|
||||
**关键结论**:P0 是唯一阻断点。修好 Phase 1 discussion broadcast 后,Phase 2-5 自然走通(已实现),Phase 6-7 后续跟进。
|
||||
|
||||
---
|
||||
|
||||
## §22.6 轻量路径设计(Direct Assignment)
|
||||
|
||||
> 不是所有任务都需要 Discussion 广播。需求明确、单一执行者的任务应跳过 Phase 0/1 直接进入 Phase 2。
|
||||
|
||||
### 路径决策矩阵
|
||||
|
||||
| 条件 | 路径 | 流程 |
|
||||
|------|------|------|
|
||||
| Issue 无 assignee + type/* label | **Discussion 路径**(默认) | Phase 0 → 1 → 2 → … |
|
||||
| Issue 有 assignee | **Direct 路径** | → Phase 2(跳过 Phase 0/1) |
|
||||
| Issue 有 `flow/direct` label | **强制 Direct** | → Phase 2(即使无 assignee,daemon 自动 assign) |
|
||||
| Issue 有 `flow/discuss` label | **强制 Discussion** | Phase 0 → 1 → 2 → …(即使有 assignee) |
|
||||
|
||||
### 判断逻辑
|
||||
|
||||
```
|
||||
parent Issue 创建
|
||||
├─ 有 flow/discuss label?→ Discussion 路径(Phase 0 → 1)
|
||||
├─ 有 flow/direct label?→ Direct 路径(daemon 自动 assign → Phase 2)
|
||||
├─ 有 assignee?→ Direct 路径(→ Phase 2)
|
||||
└─ 无 assignee(默认)→ Discussion 路径(Phase 0 → 1)
|
||||
```
|
||||
|
||||
优先级:`flow/*` label > assignee > 默认行为。
|
||||
|
||||
### 适用场景
|
||||
|
||||
| 路径 | 适用场景 | 示例 |
|
||||
|------|---------|------|
|
||||
| Discussion | 需求不明确、需要多角色讨论、跨模块协作 | 新功能设计、架构变更、涉及 3+ agent 的任务 |
|
||||
| Direct | 需求明确、单一执行者、小范围改动 | Bug 修复、文档更新、配置调整、单人任务 |
|
||||
|
||||
### Discussion 路径中的降级
|
||||
|
||||
Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及一个角色,任何 agent 可以在 comment 中建议:
|
||||
|
||||
```
|
||||
@pangtong-fujunshi 建议直接指派 @agent-id,理由:...
|
||||
```
|
||||
|
||||
庞统判断后创建 sub Issue 直接 assign → 该 agent 跳过讨论直接进入 Phase 2。
|
||||
|
||||
### Direct 路径跳过的 Phase
|
||||
|
||||
| Phase | 是否跳过 | 原因 |
|
||||
|-------|---------|------|
|
||||
| Phase 0(parent Issue 讨论 task) | ✅ 跳过 | 不创建 discussion task |
|
||||
| Phase 1(Discussion 广播) | ✅ 跳过 | 不广播,不打扰其他 agent |
|
||||
| Phase 2(assigned → executor) | ❌ 直接进入 | 起点 |
|
||||
|
||||
### 对现有代码的影响
|
||||
|
||||
当前 `_handle_issues` 已有两个分支:
|
||||
|
||||
- `opened`(无 assignee + type/* label)→ discussion task ← Discussion 路径 ✅
|
||||
- `assigned`(有 assignee)→ executor task ← Direct 路径 ✅
|
||||
|
||||
**轻量路径的后端已实现**。需要补充的是:
|
||||
|
||||
1. `flow/direct` 和 `flow/discuss` label 的识别逻辑(`toolchain_routes.py` `_handle_issues` opened 分支)
|
||||
2. Discussion prompt 中告知 agent 可以建议 direct assignment(降级机制)
|
||||
|
||||
---
|
||||
|
||||
## §22.7 数据流设计澄清
|
||||
|
||||
> 本文档涉及两个数据源(黑板 DB 和 Gitea Issue),parent/sub 映射的数据流必须明确。
|
||||
|
||||
### 当前状态 vs 设计目标
|
||||
|
||||
| 数据 | 当前存储 | §20/§21 设计目标 | 差距 |
|
||||
|------|---------|-----------------|------|
|
||||
| 协作面(title/body/comment) | 黑板 DB tasks/comments 表 | Gitea Issue/PR | ❌ Phase 1 断裂导致 agent 用黑板 API |
|
||||
| parent/sub 映射 | `tasks.parent_task`(黑板字段) | `task_state.parent_issue`(新表) | ❌ task_state 表不存在 |
|
||||
| 执行状态 | 黑板 DB tasks.status | task_state.status | ❌ task_state 表不存在 |
|
||||
| 成果物 | 黑板 DB outputs 表 | git commit + PR | ⚠️ 需 prompt 引导 |
|
||||
|
||||
### parent/sub Issue 映射数据流(设计)
|
||||
|
||||
```
|
||||
Agent 在 Phase 1 创建 sub Issue:
|
||||
POST /repos/{repo}/issues
|
||||
title: "[moz][sub][parent #{N}] 任务名"
|
||||
assignees: ["{agent_id}"]
|
||||
↓
|
||||
Gitea webhook: issues/opened (with assignee)
|
||||
↓
|
||||
daemon: _handle_issues → assigned 分支
|
||||
→ 解析标题 [parent #{N}] → 提取 parent_issue_number
|
||||
→ 写入 task_state (issue_number, parent_issue=N, status='pending')
|
||||
↓
|
||||
ticker: _check_round_complete
|
||||
→ SELECT DISTINCT parent_issue FROM task_state WHERE parent_issue IS NOT NULL
|
||||
→ 对每个 parent_issue: SELECT status FROM task_state WHERE parent_issue=?
|
||||
→ 全部终态 → spawn 庞统 review
|
||||
```
|
||||
|
||||
### 前置条件
|
||||
|
||||
1. **task_state 表创建**:§20 设计了 DDL 但未实现。需要先建表:
|
||||
|
||||
```sql
|
||||
CREATE TABLE task_state (
|
||||
issue_number INTEGER PRIMARY KEY,
|
||||
repo TEXT,
|
||||
parent_issue INTEGER,
|
||||
status TEXT DEFAULT 'pending',
|
||||
action_type TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
dispatch_count INTEGER DEFAULT 0,
|
||||
round_count INTEGER DEFAULT 0,
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
CREATE INDEX idx_task_state_parent ON task_state(parent_issue);
|
||||
```
|
||||
|
||||
2. **parent_issue 解析**:daemon 在 `_handle_issues` 中解析标题 `[parent #{N}]` 模式,写入 `task_state.parent_issue`(§21 §14b L920 已有此设计)。
|
||||
|
||||
3. **_check_round_complete 改造**:从扫 `tasks.parent_task`(黑板)改为扫 `task_state.parent_issue`(toolchain DB)(§21 §16.2 L1148 已有此设计)。
|
||||
|
||||
### 混合期处理
|
||||
|
||||
task_state 表创建前,`_check_round_complete` 仍扫 `tasks.parent_task`。两种数据可以共存:
|
||||
|
||||
- **黑板路径**(Phase 1 断裂时):agent 通过黑板 API 认领 → `tasks.parent_task` 有值
|
||||
- **Gitea 路径**(Phase 1 修复后):agent 创建 sub Issue → `task_state.parent_issue` 有值
|
||||
|
||||
`_check_round_complete` 需要同时扫两个源,直到黑板路径完全废弃。
|
||||
|
||||
### 各 Phase 数据读写汇总
|
||||
|
||||
| Phase | daemon 写 | daemon 读 | agent 读 | agent 写 |
|
||||
|-------|----------|----------|----------|----------|
|
||||
| 0 | toolchain DB: task(pending, action_type=issue_discussion) | Gitea webhook payload | — | — |
|
||||
| 1 | toolchain DB: broadcast log | toolchain DB: pending tasks | Gitea Issue body | Gitea Issue comment + sub Issue |
|
||||
| 2 | toolchain DB: task(pending, action_type=issue_assigned) | Gitea webhook payload, toolchain-templates.yaml | Gitea Issue body | git branch + PR |
|
||||
| 3 | toolchain DB: task(review_request/ci_failure) | Gitea webhook (PR opened, CI status) | Gitea PR + CI logs | git push (修复) |
|
||||
| 4 | toolchain DB: task(review_result_*) | Gitea webhook (pull_request_review) | Gitea PR diff + files | Gitea Review API |
|
||||
| 5 | toolchain DB: task(review_merged) | Gitea webhook (PR closed/merged) | — | — |
|
||||
| 6 | task_state: round_count++ | task_state: parent_issue + sub status | Gitea parent Issue body + sub Issue comments | Gitea Issue comment(三问结论)+ 关闭/创建 Issue |
|
||||
| 7 | toolchain DB: task(issue_closed) | Gitea webhook (issues/closed) | — | — |
|
||||
@@ -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**:产物只有 Skill(skill_workshop 管理)和 Memory(MEMORY.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)— 每次启动注入
|
||||
- L2:Agent `read` SKILL.md — 按需加载
|
||||
- L3:SKILL.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
|
||||
# 案例 1:PromptContext event_type 未知
|
||||
# 案例 2:PromptContext 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 的确定性注入)
|
||||
@@ -0,0 +1 @@
|
||||
# algorithms package
|
||||
@@ -0,0 +1,16 @@
|
||||
"""find_max_bubble — 冒泡排序法找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def find_max_bubble(nums: list[int | float]) -> int | float | None:
|
||||
"""冒泡排序后取最后一个元素,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
arr = list(nums) # 不修改原列表
|
||||
n = len(arr)
|
||||
for i in range(n - 1):
|
||||
for j in range(n - 1 - i):
|
||||
if arr[j] > arr[j + 1]:
|
||||
arr[j], arr[j + 1] = arr[j + 1], arr[j]
|
||||
return arr[-1]
|
||||
@@ -0,0 +1,18 @@
|
||||
"""find_max_linear — 线性扫描法找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def find_max_linear(nums: list[int | float]) -> int | float | None:
|
||||
"""返回列表中的最大值,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
result = nums[0]
|
||||
for num in nums[1:]:
|
||||
if num > result:
|
||||
result = num
|
||||
return result
|
||||
|
||||
|
||||
# 兼容别名
|
||||
find_max = find_max_linear
|
||||
@@ -0,0 +1,41 @@
|
||||
"""find_max_quickselect — 快速选择 partition 思路找最大值"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
|
||||
def find_max_quickselect(nums: list[int | float]) -> int | float | None:
|
||||
"""使用快速选择的 partition 思路直接找最大值,空列表返回 None。"""
|
||||
if not nums:
|
||||
return None
|
||||
arr = list(nums) # 不修改原列表
|
||||
return _quickselect_max(arr, 0, len(arr) - 1)
|
||||
|
||||
|
||||
def _quickselect_max(arr: list[int | float], lo: int, hi: int) -> int | float:
|
||||
"""在 arr[lo..hi] 中找最大值。
|
||||
|
||||
Lomuto partition: < pivot 左,>= pivot 右。
|
||||
pivot 落在 store 位置,最大值在 [store, hi]。
|
||||
"""
|
||||
if lo == hi:
|
||||
return arr[lo]
|
||||
|
||||
pivot_idx = random.randint(lo, hi)
|
||||
arr[pivot_idx], arr[hi] = arr[hi], arr[pivot_idx]
|
||||
|
||||
pivot = arr[hi]
|
||||
store = lo
|
||||
for i in range(lo, hi):
|
||||
if arr[i] < pivot:
|
||||
arr[store], arr[i] = arr[i], arr[store]
|
||||
store += 1
|
||||
arr[store], arr[hi] = arr[hi], arr[store]
|
||||
|
||||
# store 是 pivot 最终位置
|
||||
# 如果 store == hi,pivot 是当前区间最大值
|
||||
if store == hi:
|
||||
return arr[store]
|
||||
# 否则在 store+1..hi 中继续找(右侧都 >= pivot)
|
||||
return _quickselect_max(arr, store + 1, hi)
|
||||
+155
-35
@@ -219,7 +219,7 @@ def _toolchain_db_path() -> Path:
|
||||
|
||||
|
||||
def _send_toolchain_task(
|
||||
to_agent: str,
|
||||
to_agent: str | None,
|
||||
title: str,
|
||||
description: str,
|
||||
event_type: str,
|
||||
@@ -231,7 +231,7 @@ def _send_toolchain_task(
|
||||
"""创建 Toolchain Task 并写入 _toolchain DB。
|
||||
|
||||
Args:
|
||||
to_agent: 收件人 Agent ID
|
||||
to_agent: 收件人 Agent ID,None 表示无指派(待路由/delegate)
|
||||
title: 任务标题
|
||||
description: 任务描述(模板渲染后的事件信息)
|
||||
event_type: 事件类型(review_result / ci_failure / ...)
|
||||
@@ -243,7 +243,7 @@ def _send_toolchain_task(
|
||||
Returns:
|
||||
创建的 Task ID
|
||||
"""
|
||||
if to_agent not in AGENT_IDS:
|
||||
if to_agent is not None and to_agent not in AGENT_IDS:
|
||||
logger.warning("Unknown agent: %s, skipping toolchain task", to_agent)
|
||||
return ""
|
||||
|
||||
@@ -650,9 +650,10 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None:
|
||||
else: # REQUEST_CHANGES
|
||||
tc_steps = [
|
||||
"按审查意见逐条修改代码",
|
||||
"文档同步:如审查涉及设计/接口变更,同步更新 docs/design/ 对应文档",
|
||||
"push 到原分支 → CI 自动跑",
|
||||
"CI 通过后等重新 Review",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果",
|
||||
]
|
||||
_send_toolchain_task(
|
||||
to_agent=pr_author,
|
||||
@@ -778,9 +779,8 @@ def _send_deploy_failure_task(repo: str, pr_number: int, pr_title: str, reason:
|
||||
action_type="deploy_failure",
|
||||
steps=[
|
||||
"检查 deploy 日志",
|
||||
"排查失败原因",
|
||||
"修复并重新部署",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)",
|
||||
"根据 deploy 日志判断失败原因类型:\n a. 代码/配置问题(rsync 路径错、依赖缺失、启动失败)→ 修复 → 重新部署\n b. 基础设施问题(Gitea 不可用、网络不通、磁盘满、SSH 故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见下方「需要创建 Issue 时」),label 必须包含 type/infrastructure",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中说明判断的原因类型和执行的操作",
|
||||
],
|
||||
context_data={
|
||||
"repo": repo,
|
||||
@@ -997,32 +997,153 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
"brief": brief,
|
||||
})
|
||||
|
||||
title = f"Issue 指派: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee,
|
||||
title=title,
|
||||
description=text,
|
||||
event_type="issue_assigned",
|
||||
action_type="issue_assigned",
|
||||
steps=[
|
||||
f"创建分支 fix/{issue_number}-{brief}",
|
||||
"编码 + 写 UT",
|
||||
"push → 等 CI",
|
||||
f"CI 通过后创建 PR(Gitea API: POST /repos/{repo}/pulls)",
|
||||
"等 Review",
|
||||
# 检查是否是基础设施 Issue(按 label 分流)
|
||||
is_infrastructure = any("infrastructure" in lbl.lower() for lbl in labels_list)
|
||||
|
||||
if is_infrastructure:
|
||||
infra_steps = [
|
||||
"根据 Issue body 中的错误来源和日志片段排查问题",
|
||||
"修复基础设施问题(如修复 CI runner 环境、恢复网络、重启服务等)",
|
||||
"修复后在 Issue 上 comment 说明修复方式和结果",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)",
|
||||
],
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"labels": labels,
|
||||
"issue_body": issue_body or "(无描述)",
|
||||
"brief": brief,
|
||||
},
|
||||
]
|
||||
title = f"基础设施 Issue: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee,
|
||||
title=title,
|
||||
description=text,
|
||||
event_type="infrastructure_failure",
|
||||
action_type="infrastructure_failure",
|
||||
steps=infra_steps,
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"labels": labels,
|
||||
"issue_body": issue_body or "(无描述)",
|
||||
"brief": brief,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# §21 §5 按 type/* label 解析 business_type
|
||||
business_type = "feat" # default
|
||||
for lbl in labels_list:
|
||||
lbl_lower = lbl.lower()
|
||||
if "bug" in lbl_lower:
|
||||
business_type = "bug"
|
||||
elif "impl" in lbl_lower:
|
||||
business_type = "impl"
|
||||
elif "feat" in lbl_lower:
|
||||
business_type = "feat"
|
||||
elif "docs" in lbl_lower or "documentation" in lbl_lower:
|
||||
business_type = "docs"
|
||||
elif "refactor" in lbl_lower:
|
||||
business_type = "refactor"
|
||||
elif "test" in lbl_lower:
|
||||
business_type = "test"
|
||||
|
||||
# §21 §4 从 YAML 模板获取 steps(fallback 到硬编码)
|
||||
from src.daemon.toolchain_templates import get_steps
|
||||
yaml_steps = get_steps("issue_assigned", business_type)
|
||||
if yaml_steps:
|
||||
# 渲染占位符
|
||||
rendered_steps = []
|
||||
for s in yaml_steps:
|
||||
s = s.replace("{issue_number}", str(issue_number))
|
||||
s = s.replace("{brief}", brief)
|
||||
s = s.replace("{title}", issue_title[:30])
|
||||
rendered_steps.append(s)
|
||||
steps_to_use = rendered_steps
|
||||
else:
|
||||
# fallback: 硬编码 steps(向后兼容)
|
||||
steps_to_use = [
|
||||
f"在开发目录执行 git 操作:\n a. git checkout main && git pull origin main\n b. git checkout -b fix/{issue_number}-{brief}",
|
||||
"编码 + 写 UT",
|
||||
"文档同步:如果本次改动涉及设计变更或接口变更,在同一分支更新 docs/design/ 对应文档。如无需更新,在 action report 中说明「文档无需更新」",
|
||||
f"git add -A && git commit -m \"[moz] fix: {issue_title[:30]}\" && git push origin fix/{issue_number}-{brief}",
|
||||
f"CI 通过后创建 PR(Gitea API: POST /repos/{repo}/pulls,head: fix/{issue_number}-{brief}, base: main)— PR body 必须含 Closes #{issue_number}",
|
||||
"等 Review",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果",
|
||||
]
|
||||
|
||||
title = f"Issue 指派: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee,
|
||||
title=title,
|
||||
description=text,
|
||||
event_type="issue_assigned",
|
||||
action_type="issue_assigned",
|
||||
steps=steps_to_use,
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"labels": labels,
|
||||
"issue_body": issue_body or "(无描述)",
|
||||
"brief": brief,
|
||||
"business_type": business_type,
|
||||
},
|
||||
)
|
||||
|
||||
elif action == "closed":
|
||||
# §21 §11 Issue closed 纯通知(auto-pass)
|
||||
assignee_login = ""
|
||||
issue_assignees = issue.get("assignees") or []
|
||||
if issue_assignees:
|
||||
assignee_login = issue_assignees[-1].get("login", "")
|
||||
if not assignee_login or assignee_login not in AGENT_IDS:
|
||||
logger.debug("Issue closed but no valid assignee, skipping")
|
||||
return
|
||||
title = f"Issue 已关闭: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=assignee_login,
|
||||
title=title,
|
||||
description=f"## Issue 已关闭\n\n**{repo}#{issue_number}**: {issue_title}\n\nIssue 已被关闭。",
|
||||
event_type="issue_closed",
|
||||
action_type="issue_closed",
|
||||
steps=[], # 纯通知,无步骤
|
||||
context_data={"repo": repo, "issue_number": issue_number},
|
||||
)
|
||||
|
||||
elif action == "opened":
|
||||
# §11b: 无 assignee 的普通 Issue → discussion task
|
||||
assignees = issue.get("assignees") or []
|
||||
single_assignee = issue.get("assignee")
|
||||
if single_assignee and isinstance(single_assignee, dict) and single_assignee not in assignees:
|
||||
assignees = list(assignees) + [single_assignee]
|
||||
|
||||
if not assignees and not ("部署失败" in issue_title):
|
||||
# 无 assignee + 非部署失败 → 检查是否有 type/* label
|
||||
labels_list_opened = [
|
||||
lbl.get("name", "") for lbl in (issue.get("labels") or [])
|
||||
]
|
||||
has_type_label = any(
|
||||
lbl.lower().startswith("type/") for lbl in labels_list_opened
|
||||
)
|
||||
if has_type_label:
|
||||
issue_body = issue.get("body", "") or "(无描述)"
|
||||
title_discussion = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
to_agent=None, # 无指派 → router delegate 庞统
|
||||
title=title_discussion,
|
||||
description=f"## Issue 需要讨论\n\n"
|
||||
f"**{repo}#{issue_number}**: {issue_title}\n\n"
|
||||
f"{issue_body}",
|
||||
event_type="issue_discussion",
|
||||
action_type="issue_discussion",
|
||||
steps=[], # discussion 不需要结构化步骤
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"issue_body": issue_body,
|
||||
},
|
||||
)
|
||||
logger.info(
|
||||
"Issue #%s: no assignee + type label → discussion task",
|
||||
issue_number)
|
||||
# discussion task 创建后继续检查 @mention(不 return)
|
||||
|
||||
if "部署失败" in issue_title:
|
||||
# 从 Issue body 提取 commit hash(Gitea deploy workflow 格式)
|
||||
sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", ""))
|
||||
@@ -1043,9 +1164,8 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
action_type="deploy_failure",
|
||||
steps=[
|
||||
"检查 deploy 日志",
|
||||
"排查失败原因",
|
||||
"修复并重新部署",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)",
|
||||
"根据 deploy 日志判断失败原因类型:\n a. 代码/配置问题(rsync 路径错、依赖缺失、启动失败)→ 修复 → 重新部署\n b. 基础设施问题(Gitea 不可用、网络不通、磁盘满、SSH 故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见下方「需要创建 Issue 时」),label 必须包含 type/infrastructure",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中说明判断的原因类型和执行的操作",
|
||||
],
|
||||
context_data={
|
||||
"repo": repo,
|
||||
@@ -1126,9 +1246,9 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None:
|
||||
action_type="ci_failure",
|
||||
steps=[
|
||||
"查看完整 CI 日志(PR 页面或 Gitea Actions 页面)",
|
||||
"修复失败的测试",
|
||||
"push → CI 自动重跑",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)",
|
||||
"根据 CI 日志判断失败原因类型:\n a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑\n b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见下方「需要创建 Issue 时」),label 必须包含 type/infrastructure",
|
||||
"文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档",
|
||||
"提交 action report(POST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/comments,comment_type=action_report)— 报告中说明判断的原因类型和执行的操作,以及文档是否需要更新",
|
||||
],
|
||||
context_data={
|
||||
"pr_number": issue_number,
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -152,6 +152,7 @@ class BootstrapBuilder:
|
||||
constraints.extend([
|
||||
"- 审查结果必须明确 pass/fail",
|
||||
"- 评审意见须附证据(文件:行号)",
|
||||
"- 需求-设计-编码一致性:PR 改动是否和 Issue/设计文档描述一致?如改了实现但 docs/design 未同步更新,在 Review 中指出",
|
||||
])
|
||||
elif role == "planner":
|
||||
constraints.extend([
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# DEPRECATED per §19 重设计 — 经验蒸馏改为双层 daily cron(L1 各 agent + L2 庞统)
|
||||
# 保留代码供参考,后续 P3 清理时物理删除
|
||||
"""Experience Distillation — 经验蒸馏
|
||||
|
||||
从已完成的任务产出中提取经验:
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -174,3 +174,28 @@ 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/ 对应设计文档是否需要更新?需要则在同一 PR 中更新\n"
|
||||
"- 改了实现 → tests/ 是否有对应测试脚本需要更新?需要则在同一 PR 中更新\n"
|
||||
"- action report 中必须逐项说明上述检查结果(如「文档无需更新」「测试已补充」)\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
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# DEPRECATED per §19 重设计 — 不再参与 skill 发现/加载
|
||||
# 实际 skill 发现走 openclaw 原生 <available_skills> 机制
|
||||
# 保留代码供参考,后续 P3 清理时物理删除
|
||||
"""Skill System — 技能注册、加载、匹配、执行
|
||||
|
||||
三层自由度:
|
||||
|
||||
+95
-33
@@ -103,9 +103,9 @@ SPAWN_PROMPT_TEMPLATE = """{identity_section}
|
||||
"""
|
||||
|
||||
|
||||
DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一个 v2.9 四相循环的讨论环节。
|
||||
DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与 Gitea Issue 讨论。
|
||||
|
||||
## 你的任务
|
||||
## 讨论主题
|
||||
|
||||
{goal_snapshot}
|
||||
|
||||
@@ -113,35 +113,59 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一
|
||||
|
||||
{constraints}
|
||||
|
||||
## 黑板 API
|
||||
## 你是谁
|
||||
|
||||
你可以随时:
|
||||
- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all(含 comments、outputs)
|
||||
- 写 comment:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/comments
|
||||
body: {{"author": "{agent_id}", "body": "内容(@agent-id 自动路由)"}}
|
||||
- 创建 sub task:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks
|
||||
body: {{"title": "...", "description": "...", "task_type": "...", "parent_task": "{task_id}", "must_haves": "{{\"capability\": \"...\"}}"}}
|
||||
- 认领任务:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{{sub_task_id}}/claim
|
||||
{agent_identity}
|
||||
|
||||
## 你必须做什么
|
||||
|
||||
读完需求后,在 Gitea Issue comment 回应(必须,不是可选):
|
||||
|
||||
1.【定位】这个需求和你有什么关系?你的专业能力能贡献什么?
|
||||
2.【建议】你对实现方案有什么建议?(技术选型、数据来源、实现路径)
|
||||
3.【认领】如果你需要参与,创建 sub Issue 并在 parent Issue comment 注册:
|
||||
- 创建 sub Issue:
|
||||
```bash
|
||||
curl -X POST http://192.168.2.154:3000/api/v1/repos/{repo}/issues \
|
||||
-H "Authorization: token <your-token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{{"title": "[moz][sub][parent #{issue_number}] 任务名", "body": "Parent: #{issue_number}\n## 任务\n...", "assignees": ["{agent_id}"]}}'
|
||||
```
|
||||
- 创建后在 parent Issue comment: "[角色名] 我创建了 sub Issue #N: 任务名,我负责 简述"
|
||||
4.【风险】如果你发现风险、不合理的假设、或遗漏的环节,直接提出
|
||||
|
||||
⚠️ 每个 agent 必须 comment。即使你认为和自己无关,也要说明原因——这证明你读过并思考过了。
|
||||
|
||||
## Comment 格式
|
||||
|
||||
你的 comment 必须以角色名开头,让其他人知道你是谁:
|
||||
[角色名] 你的观点
|
||||
例:[张飞] 我来负责策略编码,用 vnpy CtaTemplate 实现。
|
||||
例:[关羽] 这个策略需要风控,连亏 3 天应暂停。
|
||||
例:[赵云] 数据已就绪,2024-08 缺失已补齐。
|
||||
|
||||
## Gitea API
|
||||
|
||||
- 读 Issue: GET http://192.168.2.154:3000/api/v1/repos/{repo}/issues/{issue_number}
|
||||
- 读 Issue comments: GET http://192.168.2.154:3000/api/v1/repos/{repo}/issues/{issue_number}/comments
|
||||
- 写 comment: POST http://192.168.2.154:3000/api/v1/repos/{repo}/issues/{issue_number}/comments
|
||||
body: {{"body": "内容"}}
|
||||
- 创建 sub Issue: POST http://192.168.2.154:3000/api/v1/repos/{repo}/issues
|
||||
|
||||
## 行为准则
|
||||
|
||||
1. **你是自主的。**读黑板、思考、行动,不要等指令。
|
||||
2. **不重复别人的工作。**动手前先读黑板看谁在做什么(Separation)。
|
||||
3. **保持方向对齐。**你的产出方向和 parent goal 对齐,不确定时 @pangtong-fujunshi(Alignment)。
|
||||
4. **产出可共享。**产出写入黑板,让其他人能看到你的成果(Cohesion)。
|
||||
5. **不越界。**安全红线不要碰,超出能力的 @ 庞统升级(Boundary)。
|
||||
6. **随时讨论。**执行过程中需要协作时 @ 对应 Agent,讨论是灵活的不是固定阶段的。
|
||||
1. **你是自主的。** 读 Issue、思考、行动,不要等指令。
|
||||
2. **不重复别人的工作。** 动手前先读 Issue comment 看谁在做什么(Separation)。
|
||||
3. **保持方向对齐。** 你的产出方向和 parent goal 对齐,不确定时 @pangtong-fujunshi(Alignment)。
|
||||
4. **产出可共享。** 产出写入 Gitea Issue/PR,让其他人能看到你的成果(Cohesion)。
|
||||
5. **不越界。** 安全红线不要碰,超出能力的 @ 庞统升级(Boundary)。
|
||||
6. **随时讨论。** 执行过程中需要协作时 @ 对应 Agent,讨论是灵活的不是固定阶段的。
|
||||
|
||||
## 讨论完成后
|
||||
|
||||
- 如果讨论收敛到可执行的任务,直接创建 sub task
|
||||
- 如果有分歧或不确定,在黑板上写 comment @ 庞统裁决
|
||||
- 标记完成:
|
||||
```bash
|
||||
curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/status \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{{"status": "done", "agent": "{agent_id}"}}'
|
||||
```
|
||||
- 如果讨论收敛到可执行的任务,直接创建 sub Issue(assign 自己)
|
||||
- 如果有分歧或不确定,在 Issue comment @pangtong-fujunshi 裁决
|
||||
- 标记完成(在 parent Issue comment 写总结)
|
||||
"""
|
||||
|
||||
|
||||
@@ -288,6 +312,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 +321,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 +332,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)
|
||||
|
||||
@@ -378,18 +407,33 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta
|
||||
def _build_discussion_prompt(self, task_id: str, title: str,
|
||||
description: str, must_haves: str,
|
||||
project_id: str, agent_id: str) -> str:
|
||||
"""构建讨论类 spawn prompt(§3.3 框架 + Boids)"""
|
||||
"""构建讨论类 spawn prompt(§22.2 Gitea API 版本)"""
|
||||
goal_snapshot = description or title
|
||||
constraints = must_haves or "(无特殊约束)"
|
||||
|
||||
# 从 must_haves 解析 issue 上下文
|
||||
repo = "sanguo/sanguo_moziplus_v2"
|
||||
issue_number = ""
|
||||
constraints = "(无特殊约束)"
|
||||
try:
|
||||
meta = json.loads(must_haves) if must_haves else {}
|
||||
ctx = meta.get("context", {})
|
||||
repo = ctx.get("repo", repo)
|
||||
issue_number = str(ctx.get("issue_number", ""))
|
||||
if meta.get("steps"):
|
||||
constraints = "\n".join(meta["steps"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
|
||||
agent_identity = self._inject_agent_identity(agent_id)
|
||||
return DISCUSSION_PROMPT_TEMPLATE.format(
|
||||
goal_snapshot=goal_snapshot,
|
||||
constraints=constraints,
|
||||
project_id=project_id,
|
||||
task_id=task_id,
|
||||
agent_id=agent_id,
|
||||
api_host=self.api_host,
|
||||
api_port=self.api_port,
|
||||
agent_identity=agent_identity,
|
||||
repo=repo,
|
||||
issue_number=issue_number,
|
||||
)
|
||||
|
||||
def _inject_agent_identity(self, agent_id: str) -> str:
|
||||
@@ -625,19 +669,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
|
||||
@@ -1251,7 +1300,7 @@ curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_
|
||||
conn = get_connection(db_path)
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT id, title, status FROM tasks WHERE id=?", (
|
||||
"SELECT id, title, status, must_haves FROM tasks WHERE id=?", (
|
||||
task_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
@@ -1949,6 +1998,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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
+98
-21
@@ -332,25 +332,10 @@ class Ticker:
|
||||
except Exception as e:
|
||||
logger.warning("HealthChecker error for %s: %s", project_id, e)
|
||||
|
||||
# 9. 经验蒸馏(完成的 task 自动触发)
|
||||
# 9. 经验蒸馏 — DEPRECATED per §19, 双层 daily cron 替代
|
||||
# 保留参数向后兼容,不再执行逐任务蒸馏
|
||||
if self.experience_distiller:
|
||||
try:
|
||||
conn2 = get_connection(db_path)
|
||||
try:
|
||||
done_tasks = conn2.execute(
|
||||
"SELECT id FROM tasks WHERE status='done' AND updated_at > datetime('now', '-60 seconds')"
|
||||
).fetchall()
|
||||
finally:
|
||||
conn2.close()
|
||||
for row in done_tasks:
|
||||
t = Blackboard(db_path).get_task(row[0])
|
||||
if t:
|
||||
self.experience_distiller.distill_from_task(
|
||||
task_id=t.id, task_title=t.title, task_type=t.task_type
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"ExperienceDistiller error for %s: %s", project_id, e)
|
||||
logger.debug("ExperienceDistiller deprecated per §19, skipping (use L1/L2 daily cron)")
|
||||
|
||||
# 10. 扫描后状态
|
||||
result["summary_after"] = queries.task_summary()
|
||||
@@ -528,7 +513,7 @@ class Ticker:
|
||||
|
||||
### 三问
|
||||
1. Goal 还清晰吗?(是否有 goal drift)
|
||||
2. 成果物覆盖 goal 了吗?(逐条检查验收标准)
|
||||
2. 成果物覆盖 goal 了吗?(逐条检查验收标准 + 确认 docs/design 是否同步更新)
|
||||
3. 下一轮需要做什么?(创建新 sub tasks / 标记完成 / 调整方向)
|
||||
|
||||
### 失败处理
|
||||
@@ -1084,6 +1069,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,
|
||||
@@ -1163,10 +1161,43 @@ Parent Task ID: {parent_task.id}
|
||||
if t.id not in self._broadcast_tracker:
|
||||
self._broadcast_tracker[t.id] = BroadcastRound(task_id=t.id)
|
||||
|
||||
# 分离 discussion tasks 和普通 claim tasks
|
||||
discussion_tasks = []
|
||||
claim_tasks = []
|
||||
for t in broadcastable:
|
||||
action_type = self._get_task_action_type(t)
|
||||
if action_type == "issue_discussion":
|
||||
discussion_tasks.append(t)
|
||||
else:
|
||||
claim_tasks.append(t)
|
||||
|
||||
spawned = []
|
||||
for agent_id in idle_agents:
|
||||
prompt = self._build_claim_prompt(
|
||||
agent_id, broadcastable, project_id)
|
||||
prompts_to_send = []
|
||||
|
||||
# discussion tasks: 每个 task 独立构建 discussion prompt
|
||||
for t in discussion_tasks:
|
||||
disc_prompt = self.spawner._build_discussion_prompt(
|
||||
task_id=t.id,
|
||||
title=t.title,
|
||||
description=t.description or "",
|
||||
must_haves=t.must_haves or "",
|
||||
project_id=project_id,
|
||||
agent_id=agent_id,
|
||||
)
|
||||
prompts_to_send.append(disc_prompt)
|
||||
|
||||
# claim tasks: 按原逻辑批量构建 claim prompt
|
||||
if claim_tasks:
|
||||
claim_prompt = self._build_claim_prompt(
|
||||
agent_id, claim_tasks, project_id)
|
||||
prompts_to_send.append(claim_prompt)
|
||||
|
||||
# 合并 prompt(如果有多种类型)
|
||||
if not prompts_to_send:
|
||||
continue
|
||||
prompt = "\n\n---\n\n".join(prompts_to_send)
|
||||
|
||||
try:
|
||||
session_id = await self.spawner.spawn_full_agent(
|
||||
agent_id=agent_id,
|
||||
@@ -1189,6 +1220,14 @@ Parent Task ID: {parent_task.id}
|
||||
|
||||
return spawned
|
||||
|
||||
def _get_task_action_type(self, task) -> str:
|
||||
"""从 task.must_haves JSON 中提取 action_type"""
|
||||
try:
|
||||
meta = json.loads(task.must_haves) if task.must_haves else {}
|
||||
return meta.get("action_type", "")
|
||||
except (json.JSONDecodeError, TypeError, AttributeError):
|
||||
return ""
|
||||
|
||||
def _build_claim_prompt(self, agent_id: str, tasks: list,
|
||||
project_id: str) -> str:
|
||||
"""#03: 广播认领 prompt(身份+专长注入)"""
|
||||
@@ -1376,6 +1415,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 +1440,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,9 +31,11 @@ _ACTION_HINTS: Dict[str, str] = {
|
||||
"review_result": "你收到一个 Review 结果通知,这是一个需要你执行动作的事件(不是纯通知)。",
|
||||
"review_request": "你收到一个 Review 请求,这是一个需要你审查并提交 Review 的事件。",
|
||||
"review_updated": "你收到一个 PR 更新通知,这是一个需要你重新审查修改部分的事件。",
|
||||
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。",
|
||||
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。回复时 @评论者。",
|
||||
"ci_failure": "你收到一个 CI 失败通知,这是一个需要你修复失败测试的事件。",
|
||||
"issue_assigned": "你收到一个 Issue 指派,这是一个需要你编码实现的事件。",
|
||||
"issue_closed": "你收到一个 Issue 关闭通知。这是一条纯通知,阅读即可。",
|
||||
"issue_discussion": "你收到一个需要讨论的 Issue。请阅读 Issue 内容,发起讨论并引导其他 agent 参与。",
|
||||
"deploy_failure": "你收到一个部署失败通知,这是一个需要你排查并修复的事件。",
|
||||
"mention": "你收到一个 @mention 通知,这是一个需要你按指引响应的事件。",
|
||||
"review_merged": "你收到一个 PR 合并通知。这是一条纯通知,阅读即可。",
|
||||
@@ -51,17 +53,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():
|
||||
@@ -142,6 +168,46 @@ class ToolchainApiSection:
|
||||
"",
|
||||
"⚠️ 不要使用 Mail API(飞鸽传书)。所有协作通过 Gitea 留痕。",
|
||||
"",
|
||||
"### 提交 PR Review",
|
||||
"",
|
||||
"如果步骤中要求提交 Review(审查 PR):",
|
||||
"```bash",
|
||||
f'curl -s -X POST "{_GITEA_BASE}/repos/{{repo}}/pulls/{{pr_number}}/reviews" \\',
|
||||
' -H "Authorization: token <your-token>" \\',
|
||||
' -H "Content-Type: application/json" \\',
|
||||
' -d \'{"event": "approved", "body": "审查结论"}\'',
|
||||
"```",
|
||||
"",
|
||||
"event 可选:approved(通过)/ request_changes(驳回)",
|
||||
"⚠️ 必须用 Review API 提交,不要在 PR comment 中写 Review。",
|
||||
"",
|
||||
"### 需要创建 Issue 时",
|
||||
"",
|
||||
"如果步骤中要求创建 Issue 指派他人(如 jiangwei-infra):",
|
||||
"```bash",
|
||||
f'curl -s -X POST "{_GITEA_BASE}/repos/{{repo}}/issues" \\',
|
||||
' -H "Authorization: token <your-token>" \\',
|
||||
' -H "Content-Type: application/json" \\',
|
||||
' -d \'{"title": "[moz] infra: 简述问题", "body": "## 问题描述\\n\\n<简要描述问题现象>\\n\\n## 错误来源\\n\\n- 仓库: <repo>\\n- PR/Commit: <链接>\\n- CI/Deploy run: <Gitea Actions 页面链接>\\n\\n## 日志关键片段\\n\\n```<错误日志摘要>```\\n\\n## 判断依据\\n\\n<为什么判断为基础设施问题>", "assignees": ["jiangwei-infra"], "labels": [<label_id>]}\'',
|
||||
"```",
|
||||
"",
|
||||
"⚠️ Issue body 必须包含错误来源链接(PR/Commit + CI run),让排查者能直接看到全貌。",
|
||||
"⚠️ label 数字 ID 先 GET /repos/{repo}/labels 查询 type/infrastructure 对应的 ID。",
|
||||
"",
|
||||
"### Git 操作说明",
|
||||
"",
|
||||
"你的工作目录是开发目录(如 ~/.openclaw/sanguo_projects/sanguo_moziplus_v2/)。",
|
||||
"标准分支操作流程:",
|
||||
"```bash",
|
||||
"git checkout main && git pull origin main # 从最新主干开始",
|
||||
"git checkout -b fix/{branch_name} # 创建功能分支",
|
||||
"# ... 写代码 ...",
|
||||
"git add -A && git commit -m 'message' # 提交改动",
|
||||
"git push origin {branch_name} # 推送到远程",
|
||||
"```",
|
||||
"",
|
||||
"⚠️ 不要在 main 分支上直接 commit。",
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
@@ -183,6 +249,12 @@ class ToolchainConstraintsSection:
|
||||
'- 如果遇到问题需要其他角色支持,在关联的 PR/Issue 上创建 comment @对方',
|
||||
'- 不要使用 Mail API(飞鸽传书)发送消息',
|
||||
'- 你的所有操作都在 toolchain 流程内,通过 Gitea 留痕',
|
||||
'- ⚠️ 在 Issue/PR 上写 comment 时,如果内容需要 Issue 的 assignee 或创建者知晓,必须在 comment 中 @对方。纯确认性回复(如"收到")不需要 @。',
|
||||
"",
|
||||
"### 6. 文档同步(涉及代码改动时)",
|
||||
'- 改了实现 → 检查 docs/design/ 对应设计文档是否需要更新',
|
||||
'- 改了实现 → 检查 tests/ 是否有对应测试脚本需要更新',
|
||||
'- action report 中必须说明文档是否需要更新及处理结果(如「文档无需更新」)',
|
||||
"",
|
||||
"### Red Flags(如果脑海中出现以下想法,说明你错了)",
|
||||
"",
|
||||
@@ -194,6 +266,8 @@ class ToolchainConstraintsSection:
|
||||
'| “我已经知道了” | ❌ 知道不等于执行。执行步骤 + 提交 action report 才算完成 |',
|
||||
'| “步骤太多了,选几个做就行” | ❌ 错!必须逐条执行,不可跳过 |',
|
||||
'| “这个步骤不适用于当前情况” | ❌ 如果确实不适用,在 action report 中说明原因,但其他步骤必须执行 |',
|
||||
'| “CI/部署失败不是我代码的问题,我什么也不用做” | ❌ 错!即使是基础设施问题,你也必须创建 Issue 指派 jiangwei-infra(body 含错误来源链接 + 日志 + 判断依据),并在 action report 中说明。不能只报告“不是我的问题”就完事 |',
|
||||
'| “文档以后再说” | ❌ 错!文档同步和代码改动在同一 PR 中完成,action report 中必须说明文档处理情况 |',
|
||||
"",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
@@ -228,6 +302,7 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
ToolchainConstraintsSection(),
|
||||
GiteaConventionSection(),
|
||||
WikiGuideSection(),
|
||||
DeliveryChecklistSection(),
|
||||
]
|
||||
|
||||
def build_prompt(self, context: PromptContext) -> str:
|
||||
@@ -259,6 +334,16 @@ class ToolchainHandler(BaseTaskHandler):
|
||||
return VerifyResult(True, "merged_passthrough",
|
||||
"review_merged auto-pass")
|
||||
|
||||
# 特殊处理:issue_closed 始终通过(纯通知, §21 §11)
|
||||
if meta.get("action_type") == "issue_closed":
|
||||
return VerifyResult(True, "issue_closed_passthrough",
|
||||
"issue_closed auto-pass")
|
||||
|
||||
# 特殊处理:issue_discussion 始终通过(触发 delegate 庞统 §21 §11b)
|
||||
if meta.get("action_type") == "issue_discussion":
|
||||
return VerifyResult(True, "discussion_passthrough",
|
||||
"issue_discussion auto-pass")
|
||||
|
||||
# 1. 优先检查 action_report comment
|
||||
report_row = conn.execute(
|
||||
"SELECT id FROM comments WHERE task_id=? "
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -87,3 +87,65 @@ def render_template(name: str, variables: Dict[str, str]) -> str:
|
||||
def clear_cache() -> None:
|
||||
"""清空模板缓存(用于测试或热更新)"""
|
||||
_template_cache.clear()
|
||||
global _steps_cache
|
||||
_steps_cache = None # 重置为 None,强制下次 reload
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §21 §4.2 YAML steps 模板加载
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
STEPS_YAML_PATH = Path(__file__).parent.parent.parent / "config" / "toolchain-templates.yaml"
|
||||
|
||||
_steps_cache: Optional[dict] = None
|
||||
|
||||
|
||||
def _load_steps_yaml() -> dict:
|
||||
"""加载并缓存 toolchain-templates.yaml。"""
|
||||
global _steps_cache
|
||||
if _steps_cache is not None:
|
||||
return _steps_cache
|
||||
try:
|
||||
import yaml
|
||||
with open(STEPS_YAML_PATH, encoding="utf-8") as f:
|
||||
_steps_cache = yaml.safe_load(f) or {}
|
||||
logger.debug("Loaded steps YAML: %d action_types", len(_steps_cache))
|
||||
except FileNotFoundError:
|
||||
logger.warning("Steps YAML not found: %s", STEPS_YAML_PATH)
|
||||
_steps_cache = {}
|
||||
except Exception as e:
|
||||
logger.error("Failed to load steps YAML: %s", e)
|
||||
_steps_cache = {}
|
||||
return _steps_cache
|
||||
|
||||
|
||||
def get_steps(action_type: str, business_type: str = "") -> List[str]:
|
||||
"""从 YAML 模板配置获取 steps。
|
||||
|
||||
Args:
|
||||
action_type: 动作类型(issue_assigned / ci_failure / ...)
|
||||
business_type: 业务子类型(feature/impl/bug/docs/refactor/test/infrastructure)
|
||||
|
||||
Returns:
|
||||
steps 列表,找不到返回空列表
|
||||
"""
|
||||
templates = _load_steps_yaml()
|
||||
section = templates.get(action_type, {})
|
||||
if isinstance(section, dict) and business_type:
|
||||
subsection = section.get(business_type, {})
|
||||
return subsection.get("steps", [])
|
||||
if isinstance(section, dict):
|
||||
return section.get("steps", [])
|
||||
return []
|
||||
|
||||
|
||||
def get_output_template(action_type: str, business_type: str = "") -> str:
|
||||
"""从 YAML 模板配置获取 output_template。"""
|
||||
templates = _load_steps_yaml()
|
||||
section = templates.get(action_type, {})
|
||||
if isinstance(section, dict) and business_type:
|
||||
subsection = section.get(business_type, {})
|
||||
return subsection.get("output_template", "")
|
||||
if isinstance(section, dict):
|
||||
return section.get("output_template", "")
|
||||
return ""
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '⚙️' },
|
||||
];
|
||||
|
||||
|
||||
@@ -6,8 +6,14 @@
|
||||
需要 RUN_INTEGRATION=1 + 生产 daemon 运行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import re
|
||||
import sqlite3
|
||||
import sys
|
||||
@@ -17,7 +23,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# 指向部署目录
|
||||
|
||||
@@ -4,15 +4,20 @@
|
||||
需要 RUN_INTEGRATION=1 + 生产 daemon 运行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# 指向部署目录
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
skip_no_integration = pytest.mark.skipif(
|
||||
not __import__("os").environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon",
|
||||
)
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = [pytest.mark.e2e, skip_no_integration]
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
"""v2.7 端到端测试 — 全链路真实环境
|
||||
|
||||
@@ -14,7 +13,6 @@ pytestmark = [pytest.mark.e2e, skip_no_integration]
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
@@ -22,7 +20,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import pytest
|
||||
import os
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
skip_no_integration = pytest.mark.skipif(
|
||||
not __import__("os").environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon",
|
||||
)
|
||||
|
||||
"""v3.1 端到端测试 — 新增场景覆盖
|
||||
|
||||
覆盖 v3.1 新增功能:
|
||||
@@ -22,7 +21,6 @@ skip_no_integration = pytest.mark.skipif(
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import sys
|
||||
import time
|
||||
@@ -31,7 +29,6 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import pytest
|
||||
import requests as http_requests
|
||||
|
||||
# ── 路径设置 ──
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import os
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.skipif(
|
||||
not os.environ.get("RUN_INTEGRATION"),
|
||||
reason="Set RUN_INTEGRATION=1 to run E2E tests",
|
||||
)]
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
pytestmark = [pytest.mark.e2e]
|
||||
|
||||
"""#01 四相循环 单元测试
|
||||
|
||||
@@ -23,13 +24,10 @@ pytestmark = [pytest.mark.e2e, pytest.mark.skipif(
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# ── 路径设置 ──
|
||||
DEPLOY_DIR = Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2"
|
||||
SRC_DIR = DEPLOY_DIR / "src"
|
||||
|
||||
@@ -6,14 +6,19 @@
|
||||
覆盖:项目管理 → Task CRUD → SubTask → Stage 进度 → 父状态聚合 → 依赖链 → 超时 → Mail
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
if not os.environ.get("RUN_INTEGRATION"):
|
||||
pytest.skip("Integration tests require RUN_INTEGRATION=1", allow_module_level=True)
|
||||
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""find_max 单元测试"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.algorithms.find_max_linear import find_max
|
||||
|
||||
|
||||
class TestFindMax:
|
||||
def test_normal_list(self):
|
||||
assert find_max([3, 1, 4, 1, 5, 9, 2, 6]) == 9
|
||||
|
||||
def test_empty_list(self):
|
||||
assert find_max([]) is None
|
||||
|
||||
def test_single_element(self):
|
||||
assert find_max([42]) == 42
|
||||
|
||||
def test_negative_numbers(self):
|
||||
assert find_max([-5, -1, -10, -3]) == -1
|
||||
|
||||
def test_floats(self):
|
||||
assert find_max([1.5, 2.7, 0.3, 3.14]) == 3.14
|
||||
|
||||
def test_mixed_int_float(self):
|
||||
assert find_max([1, 2.5, 3, 0.1]) == 3
|
||||
|
||||
def test_duplicate_max(self):
|
||||
assert find_max([7, 7, 7]) == 7
|
||||
@@ -0,0 +1,63 @@
|
||||
"""三种排序算法找最大值 — 单元测试"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.algorithms.find_max_linear import find_max_linear
|
||||
from src.algorithms.find_max_bubble import find_max_bubble
|
||||
from src.algorithms.find_max_quickselect import find_max_quickselect
|
||||
|
||||
ALL_ALGOS = [find_max_linear, find_max_bubble, find_max_quickselect]
|
||||
|
||||
|
||||
# ── 通用测试(每种算法都跑) ──
|
||||
|
||||
@pytest.mark.parametrize("algo", ALL_ALGOS, ids=["linear", "bubble", "quickselect"])
|
||||
class TestAllAlgorithms:
|
||||
def test_normal_list(self, algo):
|
||||
assert algo([3, 1, 4, 1, 5, 9, 2, 6]) == 9
|
||||
|
||||
def test_empty_list(self, algo):
|
||||
assert algo([]) is None
|
||||
|
||||
def test_single_element(self, algo):
|
||||
assert algo([42]) == 42
|
||||
|
||||
def test_negative_numbers(self, algo):
|
||||
assert algo([-5, -1, -10, -3]) == -1
|
||||
|
||||
def test_floats(self, algo):
|
||||
assert algo([1.5, 2.7, 0.3, 3.14]) == 3.14
|
||||
|
||||
def test_mixed_int_float(self, algo):
|
||||
assert algo([1, 2.5, 3, 0.1]) == 3
|
||||
|
||||
def test_duplicate_max(self, algo):
|
||||
assert algo([7, 7, 7]) == 7
|
||||
|
||||
|
||||
# ── 一致性测试(同输入三法结果相同) ──
|
||||
|
||||
class TestConsistency:
|
||||
@pytest.mark.parametrize("nums", [
|
||||
[3, 1, 4, 1, 5, 9, 2, 6],
|
||||
[-5, -1, -10, -3],
|
||||
[1.5, 2.7, 0.3, 3.14],
|
||||
[42],
|
||||
[7, 7, 7],
|
||||
[0, -0.0, 0.0],
|
||||
[100, 200, 50, 150, 75],
|
||||
])
|
||||
def test_three_algorithms_same_result(self, nums):
|
||||
results = [algo(nums) for algo in ALL_ALGOS]
|
||||
assert all(r == results[0] for r in results), f"Inconsistent: {results}"
|
||||
|
||||
def test_empty_consistency(self):
|
||||
results = [algo([]) for algo in ALL_ALGOS]
|
||||
assert all(r is None for r in results)
|
||||
|
||||
def test_does_not_mutate_input(self):
|
||||
original = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
for algo in ALL_ALGOS:
|
||||
nums = list(original)
|
||||
algo(nums)
|
||||
assert nums == original, f"{algo.__name__} mutated input"
|
||||
@@ -523,3 +523,98 @@ class TestFullPromptBuild:
|
||||
# Must have constraints with Red Flags
|
||||
assert "Red Flags" in prompt
|
||||
assert "强制要求" in prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# §17 v2: CI/deploy failure branching + issue label routing + Issue API guidance
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCiFailureBranching:
|
||||
"""ci_failure steps should include a/b branching guidance."""
|
||||
|
||||
def test_ci_failure_steps_contain_branching(self):
|
||||
source_file = PROJECT_ROOT / "src" / "api" / "toolchain_routes.py"
|
||||
source = source_file.read_text()
|
||||
assert '基础设施问题' in source
|
||||
assert 'type/infrastructure' in source
|
||||
assert 'jiangwei-infra' in source
|
||||
|
||||
|
||||
class TestDeployFailureBranching:
|
||||
"""deploy_failure steps should include a/b branching guidance."""
|
||||
|
||||
def test_deploy_failure_steps_contain_branching(self):
|
||||
source_file = PROJECT_ROOT / "src" / "api" / "toolchain_routes.py"
|
||||
source = source_file.read_text()
|
||||
count = source.count('基础设施问题(Gitea 不可用')
|
||||
assert count >= 2, f'Expected >=2 deploy_failure branching, found {count}'
|
||||
|
||||
|
||||
class TestIssueAssignedLabelRouting:
|
||||
"""issue_assigned handler should route by type/infrastructure label."""
|
||||
|
||||
def test_label_check_in_source(self):
|
||||
source_file = PROJECT_ROOT / "src" / "api" / "toolchain_routes.py"
|
||||
source = source_file.read_text()
|
||||
assert 'is_infrastructure' in source
|
||||
assert 'infrastructure_failure' in source
|
||||
assert '基础设施 Issue' in source
|
||||
|
||||
def test_normal_issue_keeps_coding_steps(self):
|
||||
source_file = PROJECT_ROOT / "src" / "api" / "toolchain_routes.py"
|
||||
source = source_file.read_text()
|
||||
assert 'git checkout -b fix/' in source
|
||||
assert 'issue_assigned' in source
|
||||
|
||||
|
||||
class TestToolchainApiIssueGuidance:
|
||||
"""ToolchainApiSection should include Issue creation guidance."""
|
||||
|
||||
def test_has_issue_creation_section(self):
|
||||
source_file = PROJECT_ROOT / "src" / "daemon" / "toolchain_handler.py"
|
||||
source = source_file.read_text()
|
||||
assert "需要创建 Issue 时" in source
|
||||
assert "/issues" in source
|
||||
assert "jiangwei-infra" in source
|
||||
assert "type/infrastructure" in source
|
||||
|
||||
def test_issue_body_template_mentions_required_fields(self):
|
||||
source_file = PROJECT_ROOT / "src" / "daemon" / "toolchain_handler.py"
|
||||
source = source_file.read_text()
|
||||
assert "错误来源" in source
|
||||
assert "判断依据" in source
|
||||
|
||||
|
||||
class TestRedFlagsInfrastructure:
|
||||
"""Red Flags should include the 'not my code' entry."""
|
||||
|
||||
def test_has_infrastructure_red_flag(self):
|
||||
source_file = PROJECT_ROOT / "src" / "daemon" / "toolchain_handler.py"
|
||||
source = source_file.read_text()
|
||||
assert "不是我代码的问题" in source
|
||||
assert "基础设施问题" in source
|
||||
|
||||
|
||||
class TestGitOperationGuidance:
|
||||
"""ToolchainApiSection should include Git operation guidance."""
|
||||
|
||||
def test_has_git_operation_section(self):
|
||||
source_file = PROJECT_ROOT / "src" / "daemon" / "toolchain_handler.py"
|
||||
source = source_file.read_text()
|
||||
assert "Git 操作说明" in source
|
||||
assert "git checkout main" in source
|
||||
assert "git pull origin main" in source
|
||||
assert "git checkout -b" in source
|
||||
|
||||
def test_has_no_main_commit_warning(self):
|
||||
source_file = PROJECT_ROOT / "src" / "daemon" / "toolchain_handler.py"
|
||||
source = source_file.read_text()
|
||||
assert "不要在 main 分支上直接 commit" in source
|
||||
|
||||
def test_issue_assigned_steps_have_git_commands(self):
|
||||
source_file = PROJECT_ROOT / "src" / "api" / "toolchain_routes.py"
|
||||
source = source_file.read_text()
|
||||
assert 'git checkout main && git pull origin main' in source
|
||||
assert 'git checkout -b fix/' in source
|
||||
assert 'git add -A && git commit' in source
|
||||
assert 'git push origin fix/' in source
|
||||
|
||||
Reference in New Issue
Block a user