diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 726bff2..aebf9a6 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: 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<2' 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: | diff --git a/docs/design/21-unified-toolchain-design.md b/docs/design/21-unified-toolchain-design.md new file mode 100644 index 0000000..0d789ae --- /dev/null +++ b/docs/design/21-unified-toolchain-design.md @@ -0,0 +1,580 @@ +--- +title: "Unified Toolchain Design — 统一工具链工作流设计" +created: 2026-06-20 +version: v1.0 draft +status: draft +changelog: v1.0 初版 +--- + +# Unified Toolchain Design + +> **范围**: 仅 toolchain 流程。task 和 mail 不变。 +> **前置**: §17 ToolchainHandler、§20 Issue-Centric Orchestration +> **目标**: 把割裂的单点优化整合为统一的 AI native 工作流 + +--- + +## §1. 背景与问题 + +### 1.1 当前割裂点 + +| # | 割裂 | 现状 | 本文解决 | +|---|------|------|---------| +| 1 | agent 同时引用两套 API | 黑板 API(action report/outputs)+ Gitea API(Issue/PR comment) | §3 统一到 Gitea | +| 2 | steps 硬编码 | 9 处 `steps=[...]` 写死在 toolchain_routes.py | §4 模板化 | +| 3 | issue_assigned 只分 2 路 | 只按 infrastructure label 分流,不分 type/feat vs type/docs | §5 按 type/* 6 路分流 | +| 4 | action_type 和 business_type 混在一起 | issue_assigned 内部混入业务场景 | §6 分离两个维度 | +| 5 | verify 依赖黑板 comment_type | 迁移到 Gitea 后失效 | §7 重新定义完成检测 + 输出约束 | + +### 1.2 黑板中的"无缝接续"设计(不能丢弃) + +黑板 DB 中有多个机制确保后续 agent 可以无缝接续: + +| 机制 | 当前实现 | 用途 | 迁移到 Issue 后 | +|------|---------|------|----------------| +| **任务描述** | tasks.title + tasks.description | agent 知道"做什么" | Issue title + body ✅ 直接对应 | +| **验收标准** | tasks.must_haves | agent 知道"怎样算完成" | Issue body 中结构化字段(模板定义)✅ | +| **前序产出** | outputs 表 + depends_on | agent 知道"之前做了什么" | Issue body 引用前序 Issue/PR(如 `Depends: #42`)⚠️ 需约定 | +| **handoff comment** | comment_type=handoff | agent 之间交接上下文(≥50 字符) | Issue/PR comment ✅ 天然支持 | +| **讨论历史** | comments 表 | agent 知道"讨论了什么" | Issue/PR comment 全部可读 ✅ | +| **审查结果** | reviews 表(verdict/round/consensus) | agent 知道"审查结论" | PR Review(APPROVE/REQUEST_CHANGES)+ Issue comment 记录 | +| **agent 声明式交接** | next_capability 字段 | agent 声明"我搞不定,需要 XX 能力的人" | Issue comment @对方(已有 mention 机制)✅ | +| **retry 历史** | task_attempts 表 | daemon 知道"试了几次" | daemon 内部 task_state 表 ✅ 不变 | +| **风险观察** | observations 表 | agent 标记"发现风险" | Issue comment(约定标记)⚠️ 需约定 | + +**结论**:大部分机制可以自然迁移到 Issue。两个需要约定: +1. **前序产出引用**:Issue body 中用 `Depends: #N` 或 `Parent: #N` 引用前序 Issue +2. **风险观察**:Issue comment 中用约定标记(如 `⚠️ [观察]`) + +--- + +## §2. 设计原则 + +1. **Gitea only**:toolchain agent 只操作 Gitea,不引用黑板 API、不用 Mail +2. **模板驱动**:steps 从硬编码改为模板,不同 Issue type 对应不同流程 +3. **action_type 和 business_type 分离**:事件类型决定 action_hint,业务类型决定 steps +4. **完成检测:终态事件 + 输出约束**:用 Gitea 终态事件检测完成 + 结构化输出约束 agent 汇报 +5. **无缝接续不丢弃**:handoff/前序产出/审查结果等机制迁移到 Issue 语义 + +--- + +## §3. 统一到 Gitea(割裂 1 解决) + +### 3.1 agent API 引用变更 + +| 操作 | 现在(黑板 API) | 改造后(Gitea API) | +|------|----------------|-------------------| +| 提交 action report | `POST localhost:8083/.../comments` (comment_type=action_report) | `POST Gitea .../issues/{N}/comments`(结构化 body,§7 定义) | +| 提交产出 | `POST localhost:8083/.../outputs` | git push 到分支(代码/文档/测试) | +| 讨论 / @mention | `POST localhost:8083/.../comments` | `POST Gitea .../issues/{N}/comments` | +| 创建 PR | 已是 Gitea API(不变) | 不变 | +| 创建 Issue | 已是 Gitea API(不变) | 不变 | + +### 3.2 agent prompt 中的 API 指引 + +ToolchainApiSection 改造——去掉所有 `localhost:8083` 引用,只保留 Gitea API: + +``` +## 操作指令 + +### 汇报执行结果 +执行完步骤后,在关联的 Issue 上 comment 汇报: +```bash +curl -X POST "{GITEA}/repos/{repo}/issues/{N}/comments" \ + -H "Authorization: token " \ + -d '{"body": "[Action Report]\n\n**操作**:...\n**结果**:...\n**CI**:..."}' +``` + +### 需要其他角色支持 +在关联的 Issue/PR 上 comment @对方(已有机制) + +### 代码产出 +git push 到功能分支 → 创建 PR +``` + +--- + +## §4. steps 模板化(割裂 2 解决) + +### 4.1 模板存储 + +steps 模板存放在 daemon 配置文件 `config/toolchain-templates.yaml`: + +```yaml +# 每种 business_type 对应一套 steps + output_template +issue_assigned: + feature: + steps: + - "理解需求(Issue body)→ 如有不明确在 Issue comment 追问" + - "git checkout main && git pull origin main" + - "git checkout -b fix/{issue_number}-{brief}" + - "编码实现 + 写 UT" + - "git add -A && git commit -m '[moz] feat: {title}' && git push" + - "创建 PR(body 引用 Issue:Closes #{issue_number})" + - "等 CI + Review" + output_template: | + [Action Report] + **分支**:fix/{issue_number}-{brief} + **PR**:#{pr_number} + **改动文件**:{files} + **CI**:{ci_status} + + impl: + steps: + - "读设计文档(Issue body 中的路径)→ 理解实现范围" + - "git checkout main && git pull origin main" + - "git checkout -b impl/{issue_number}-{brief}" + - "按设计编码实现 + 写 UT" + - "git add -A && git commit -m '[moz] impl: {title}' && git push" + - "创建 PR(body 引用 Issue + 设计文档路径)" + - "等 CI + Review" + output_template: | + [Action Report] + **设计文档**:{design_doc} + **分支**:impl/{issue_number}-{brief} + **PR**:#{pr_number} + **改动文件**:{files} + **CI**:{ci_status} + + bug: + steps: + - "读 Bug 描述 + 复现步骤(Issue body)" + - "定位根因(读代码/日志,不要猜测)" + - "git checkout main && git pull origin main" + - "git checkout -b fix/{issue_number}-{brief}" + - "修复 + 写回归测试" + - "git add -A && git commit -m '[moz] fix: {title}' && git push" + - "创建 PR(body 说明根因和修复方式)" + - "等 CI + Review" + output_template: | + [Action Report] + **根因**:{root_cause} + **修复方式**:{fix_approach} + **分支**:fix/{issue_number}-{brief} + **PR**:#{pr_number} + **CI**:{ci_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" + - "创建 PR" + - "等 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(python -m pytest tests/unit/ -q)" + - "git add -A && git commit -m '[moz] refactor: {title}' && git push" + - "创建 PR(body 说明重构内容和影响范围)" + - "等 CI + Review" + output_template: | + [Action Report] + **重构范围**:{scope} + **测试结果**:{test_result} passed + **分支**:refactor/{issue_number}-{brief} + **PR**:#{pr_number} + **CI**:{ci_status} + + test: + steps: + - "读测试目标(Issue body)" + - "git checkout main && git pull origin main" + - "git checkout -b test/{issue_number}-{brief}" + - "编写测试脚本到 tests/ 对应目录" + - "运行测试验证(python -m pytest {test_file} -v)" + - "git add -A && git commit -m '[moz] test: {title}' && git push" + - "创建 PR" + - "等 CI + 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 说明修复方式和结果" + - "汇报执行结果" + output_template: | + [Action Report] + **问题**:{problem} + **根因**:{root_cause} + **修复方式**:{fix} + **验证**:{verification} + +# toolchain 事件(非 issue_assigned)的模板 +ci_failure: + steps: + - "查看 CI 日志(PR 页面或 Gitea Actions)" + - "判断失败原因:a.代码问题→修复→push b.基础设施→创建 Issue 指派 jiangwei-infra" + - "汇报结果" + output_template: | + [Action Report] + **原因类型**:{cause_type} + **操作**:{action} + **CI 重跑**:{ci_status} + +# ... 其他 toolchain 事件(review_result/review_request/...)各自定义 +``` + +### 4.2 模板加载 + +daemon 启动时加载 YAML 配置,运行时按 `action_type + business_type` 查找模板: + +```python +def get_steps(action_type: str, business_type: str = "") -> list[str]: + """从模板配置获取 steps""" + section = TEMPLATES.get(action_type, {}) + if isinstance(section, dict) and business_type: + return section.get(business_type, {}).get("steps", section.get("default", {}).get("steps", [])) + return section.get("steps", []) +``` + +--- + +## §5. 按 type/* 6 路分流(割裂 3 解决) + +### 5.1 issue_assigned handler 改造 + +当前只按 `infrastructure` label 分 2 路。改为按 `type/*` label 分流: + +```python +# 伪代码 +labels_list = [lbl.get("name", "") for lbl in (issue.get("labels") or [])] + +# 1. 基础设施(不变) +if any("infrastructure" in lbl.lower() for lbl in labels_list): + business_type = "infrastructure" +# 2. 按 type/* 确定 business_type +elif "type/feat" in labels_list: + business_type = "feature" +elif "type/impl" in labels_list: + business_type = "impl" +elif "type/bug" in labels_list: + business_type = "bug" +elif "type/docs" in labels_list: + business_type = "docs" +elif "type/refactor" in labels_list: + business_type = "refactor" +elif "type/test" in labels_list: + business_type = "test" +else: + business_type = "feature" # 默认走编码流程 + +# 从模板获取 steps + output_template +template = get_template("issue_assigned", business_type) +steps = template["steps"] +output_template = template["output_template"] +``` + +### 5.2 action_hint 差异化 + +当前 action_hint 按 action_type 固定("你收到一个 Issue 指派...")。改为同时体现 business_type: + +```python +_ACTION_HINTS = { + "issue_assigned": { + "feature": "你收到一个功能需求,理解需求后编码实现。", + "impl": "你收到一个实现任务,按设计文档编码实现。", + "bug": "你收到一个 Bug 报告,定位根因后修复。", + "docs": "你收到一个文档任务,编写文档。", + "refactor": "你收到一个重构任务,重构并确保测试通过。", + "test": "你收到一个测试任务,编写测试脚本。", + "infrastructure": "你收到一个基础设施问题报告,请排查并修复。", + }, + "ci_failure": "你收到一个 CI 失败通知,这是一个需要你修复失败测试的事件。", + "review_result": "你收到一个 Review 结果通知,这是一个需要你执行动作的事件。", + # ... 其他 action_type 不分 business_type +} +``` + +--- + +## §6. action_type 和 business_type 分离(割裂 4 解决) + +### 6.1 两个维度 + +| 维度 | 来源 | 决定什么 | 示例 | +|------|------|---------|------| +| **action_type** | webhook 事件类型 | action_hint("你收到一个 XX 通知") | ci_failure / review_result / issue_assigned | +| **business_type** | Issue label type/* | steps + output_template | feature / impl / bug / docs / refactor / test | + +### 6.2 组合规则 + +- issue_assigned:action_type=issue_assigned + business_type 从 label 确定 +- ci_failure:action_type=ci_failure(无 business_type,CI 失败就是 CI 失败) +- review_result:action_type=review_result(无 business_type,Review 就是 Review) +- 只有 issue_assigned 需要 business_type 维度(因为同一个 action_type 下不同业务的流程不同) + +--- + +## §7. 完成检测 + 输出约束(割裂 5 解决) + +### 7.1 设计原则 + +| 关注点 | 方案 | +|--------|------| +| 完成检测 | Gitea 终态事件优先 + Issue comment 兜底 | +| 输出约束 | 按 business_type 定义的 output_template(不是空泛的"简要描述") | + +### 7.2 完成检测:按 action_type 分类 + +| action_type | 终态信号 | 检测方式 | 兜底 | +|-------------|---------|---------|------| +| issue_assigned | PR merged 或 Issue closed | webhook: pull_request/closed(merged=true) 或 issues/closed | — | +| ci_failure | agent Issue comment 汇报 | Issue comment 检测([Action Report] 标记) | ⚠️ Gitea 1.26.2 不触发 CI status webhook,只能靠 comment 兜底。ticker 可选轮询 Gitea commit status API 作为补充 | +| review_result(APPROVED) | PR merged | webhook: pull_request/closed(merged=true) | — | +| review_result(CHANGES) | agent push 到分支 | webhook: pull_request/synchronize | — | +| review_request | Review 提交 | webhook: pull_request_review | — | +| review_updated | Review 提交 | webhook: pull_request_review | — | +| review_comment | agent comment | webhook: issue_comment/created | — | +| mention | agent comment | webhook: issue_comment/created | — | +| deploy_failure | agent Issue comment 汇报 | Issue comment 检测 | ✅ | +| infrastructure_failure | agent Issue comment 汇报 | Issue comment 检测 | ✅ | +| review_merged | — | auto-pass | — | + +### 7.3 状态流转:单一终态保证 + +一个 task 只有一个终态触发。daemon 内部状态机保证: + +``` +pending → working → done(终态事件触发,只触发一次) + → failed(超时/异常) +``` + +终态事件到来时检查 task 当前状态: +- 如果已经 done/failed → 忽略(幂等) +- 如果 working → 标 done +- 如果 pending → 异常,记日志 + +**中间事件**(push/comment/Review submitted)**不改变 task 状态**——它们是过程中的信号,不是终态。 + +### 7.4 输出约束:output_template + +每种 business_type 有自己的 output_template(§4.1 定义)。agent 完成后在 Issue comment 中按模板汇报。 + +daemon 的 verify 通过 webhook 事件检测终态(不需要检查 comment 内容)。但 output_template 的价值是**约束 agent 的汇报质量**——不是检测完成用的,而是给后续 agent/审查者提供结构化信息。 + +output_template 作为 steps 的最后一步注入 prompt: + +### 7.5 action report 识别规范 + +daemon 通过 webhook `issue_comment/created` 感知到新 comment 后,需要判断是否为 action report。 + +**匹配规则**: +- 精确匹配:comment body 以 `[Action Report]` 开头(允许前导空白) +- 容错策略:如果 body 包含 `[Action Report]`(不要求开头),也接受 +- 大小写不敏感 + +**匹配失败处理**: +- 不匹配的 comment 不触发完成检测 +- 作为普通讨论 comment 处理(agent 之间的 handoff/讨论) + +``` +最后一步:汇报执行结果,在 Issue 上 comment,格式: +[Action Report] +**根因**:<根因描述> +**修复方式**:<做了什么> +**分支**:fix/42-xxx +**PR**:#43 +**CI**:✅ 通过 +``` + +--- + +## §8. 无缝接续机制迁移 + +### 8.1 前序产出引用 + +当前黑板用 `depends_on` 字段 + `PriorOutputsSection` 注入前序产出摘要。 + +迁移到 Issue 后,在 Issue body 中用约定引用: + +```markdown +## 依赖 +Depends: #42(前序任务) +Parent: #40(父 Issue) + +## 前序产出摘要 +- #42 完成了数据获取模块(分支 fix/42-data,PR #43) +- 数据路径:/Volumes/stock/xxx +``` + +agent 读 Issue body 自然获得前序上下文。daemon 的 spawner 在构建 prompt 时,可以解析 Issue body 中的 `Depends: #N`,调 Gitea API 读取前序 Issue 的 comment(包含 action report)作为上下文注入。 + +### 8.2 handoff comment + +当前 handoff 通过黑板 `comment_type=handoff` + ≥50 字符约束。 + +迁移到 Issue 后,agent 的 handoff 就是**Issue/PR comment**。不需要 comment_type 字段——所有有实质内容的 comment 都是为后续 agent 提供上下文的"handoff"。 + +### 8.3 审查结果 + +当前黑板 reviews 表存 verdict/round/consensus。 + +迁移后: +- **代码审查**:PR Review(Gitea 原生,APPROVE/REQUEST_CHANGES) +- **方案审查**(设计 PR):同上 +- **庞统 round review**:保留在 daemon 内部(不迁移,这是编排逻辑) + +--- + +## §9. prompt 层级(L0-L4 不变,L2 重组) + +| 层 | 内容 | 不变? | +|---|------|-------| +| L0 铁律 | 安全底线 | ✅ 不变 | +| L1 角色 | SOUL.md / IDENTITY.md | ✅ 不变 | +| L2 引擎注入 | **本文重组** | 改造 | +| L3 被参考 | Skill 列表 | ✅ 不变 | + +L2 重组后的 section 列表: + +| priority | Section | 内容 | 来源 | +|----------|---------|------|------| +| 10 | ToolchainContextSection | action_hint + Issue body(需求)+ steps | 改造:从模板加载 steps | +| 20 | PriorContextSection | 前序产出(解析 Issue body 中的 Depends) | 改造现有 PriorOutputsSection | +| 30 | RoleSkillSection | 角色 Skill | 不变 | +| 35 | GitOperationSection | Git 操作说明(PR #95 已有) | 不变 | +| 40 | GiteaApiSection | Gitea API 指引(Issue comment + PR 创建) | 改造:去掉黑板 API | +| 50 | ToolchainConstraintsSection | 约束 + Red Flags | 不变 | +| 55 | GiteaConventionSection | Gitea 标题规范 | 不变 | +| 60 | WikiGuideSection | 知识查询引导 | 不变 | +| 65 | DeliveryChecklistSection | 交付检查 | 改造:output_template 替代空泛的"简要描述" | + +--- + +## §10. 涉及改动 + +| 文件 | 改动 | 工作量 | +|------|------|-------| +| `config/toolchain-templates.yaml` | 新建:6 种 business_type steps + output_template | 新文件 | +| `src/daemon/toolchain_handler.py` | ToolchainApiSection 改为 GiteaApiSection(去黑板 API);action_hint 支持 business_type | 改造 | +| `src/daemon/toolchain_handler.py` | verify_completion 改为终态事件检测 + Issue comment 兜底 | 改造 | +| `src/api/toolchain_routes.py` | issue_assigned handler 按 type/* 6 路分流 | 改造 | +| `src/api/toolchain_routes.py` | steps 从模板加载(替代硬编码) | 改造 | +| `src/daemon/toolchain_handler.py` | webhook handler 增加终态事件检测 | 新增 | +| `.gitea/ISSUE_TEMPLATE/` | 新增 impl.yml / docs.yml / refactor.yml | 新文件 | +| `tests/` | 更新测试 | 改造 | + +--- + +## §11. Issue closed 事件处理 + +### 11.1 问题 + +当前 `_handle_issues` 只处理 `action == "assigned"`,不处理 `action == "closed"`。Issue 被关闭时: +- daemon 不感知(webhook `issues/closed` 被忽略) +- 创建者 / 关注者收不到通知 +- 如果该 Issue 对应一个活跃的 task,daemon 不知道 Issue 已关闭 + +### 11.2 设计 + +`_handle_issues` 增加 `action == "closed"` 分支: + +**谁被通知**:Issue 创建者(`issue.user.login`)。 + +**通知内容**(通过 toolchain task 发给创建者): +- Issue 标题 + 编号 +- 关闭者(`payload.sender.login`) +- 关闭时间 +- Issue 上最后一个 comment 的摘要(修复说明) + +**通知类型**:纯通知(event_type=issue_closed,verify auto-pass,和 review_merged 一样)。 + +**特殊情况**: +- 如果关闭者是创建者自己(自己关自己创建的),不通知(避免自环) +- 如果 Issue 没有创建者信息或创建者不是已知 agent,跳过 + +### 11.3 实现伪代码 + +```python +# _handle_issues 中新增 +if action == "closed": + issue_creator = issue.get("user", {}).get("login", "") + closed_by = payload.get("sender", {}).get("login", "") + + # 自己关自己创建的,不通知 + if issue_creator == closed_by: + return + + # 只通知已注册的 agent + if issue_creator not in AGENT_IDS: + return + + # 读取最后一个 comment 作为修复摘要 + comments = issue.get("comments", 0) + last_comment_summary = "(无 comment)" + # 可选:调 Gitea API 读最后一个 comment + + title = f"Issue 已关闭: {issue_title} ({repo}#{issue_number})" + description = f"Issue {repo}#{issue_number} 已被 {closed_by} 关闭。\n\n{last_comment_summary}" + + _send_toolchain_task( + to_agent=issue_creator, + title=title, + description=description, + event_type="issue_closed", + action_type="issue_closed", + steps=[], # 纯通知,无步骤 + context_data={ + "issue_number": issue_number, + "repo": repo, + "issue_title": issue_title, + "closed_by": closed_by, + }, + ) +``` + +### 11.4 _ACTION_HINTS 新增 + +```python +"issue_closed": "你创建的 Issue 已被关闭。这是一条纯通知,阅读即可。", +``` + +### 11.5 EVENT_LABELS_ZH 新增 + +```python +"issue_closed": "Issue 已关闭", +``` + +### 11.6 verify_completion + +issue_closed 走 auto-pass(和 review_merged 一样),纯通知不需要 agent 动作。 + +### 11.7 涉及改动 + +| 文件 | 改动 | +|------|------| +| `src/api/toolchain_routes.py` `_handle_issues` | 新增 `action == "closed"` 分支 | +| `src/daemon/toolchain_handler.py` `_ACTION_HINTS` | 新增 issue_closed | +| `src/daemon/toolchain_handler.py` `EVENT_LABELS_ZH` | 新增 issue_closed | +| `src/daemon/toolchain_handler.py` `verify_completion` | issue_closed auto-pass | +| `templates/toolchain/issue_closed.md` | 新建通知模板 | +| `tests/` | 新增 closed 事件测试 | + +--- + +## §12. 不做的事 + +| 不做 | 理由 | +|------|------| +| 不改 task handler | task 流程不变(§20 设计中的 task 逐步迁移到 Issue 是后续工作) | +| 不改 mail | mail 职责不变 | +| 不改 dispatcher/ticker 核心逻辑 | 调度逻辑不变(§20 Phase 1 的 dispatcher SQL 迁移是前置工作) | +| 不做前端改造 | 后续独立设计 | +| 不改 experiences/checkpoints/decisions 表 | 执行面表保留在 daemon |