diff --git a/.gitea/ISSUE_TEMPLATE/impl.yml b/.gitea/ISSUE_TEMPLATE/impl.yml index f985e10..cfed43c 100644 --- a/.gitea/ISSUE_TEMPLATE/impl.yml +++ b/.gitea/ISSUE_TEMPLATE/impl.yml @@ -2,7 +2,7 @@ name: 实现任务 about: 按设计文档实现功能 title: "[moz] impl: " labels: - - type/feature + - type/feat body: - type: textarea id: description diff --git a/config/toolchain-templates.yaml b/config/toolchain-templates.yaml index 8798382..5bc407d 100644 --- a/config/toolchain-templates.yaml +++ b/config/toolchain-templates.yaml @@ -6,7 +6,7 @@ # issue_assigned(按 Issue label 分 6 路 + infrastructure) # --------------------------------------------------------------------------- issue_assigned: - feature: + feat: steps: - "理解需求(Issue body)→ 如有不明确在 Issue comment 追问" - "git checkout main && git pull origin main" diff --git a/src/api/toolchain_routes.py b/src/api/toolchain_routes.py index e727ef4..f56586b 100644 --- a/src/api/toolchain_routes.py +++ b/src/api/toolchain_routes.py @@ -1025,14 +1025,38 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: }, ) else: - 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=[ + # §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 中说明「文档无需更新」", @@ -1040,7 +1064,16 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: 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//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, @@ -1048,9 +1081,30 @@ async def _handle_issues(payload: Dict[str, Any]) -> None: "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": if "部署失败" in issue_title: # 从 Issue body 提取 commit hash(Gitea deploy workflow 格式) diff --git a/src/daemon/spawner.py b/src/daemon/spawner.py index 9cfbbaf..239c3a2 100644 --- a/src/daemon/spawner.py +++ b/src/daemon/spawner.py @@ -103,9 +103,9 @@ SPAWN_PROMPT_TEMPLATE = """{identity_section} """ -DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一个 v2.9 四相循环的讨论环节。 +DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与讨论。这是一个四相循环的讨论环节。 -## 你的任务 +## 讨论主题 {goal_snapshot} @@ -113,14 +113,38 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一 {constraints} +## 你是谁 + +{agent_identity} + +## 你必须做什么 + +读完需求后,在黑板 comment 回应(必须,不是可选): + +1.【定位】这个需求和你有什么关系?你的专业能力能贡献什么? +2.【建议】你对实现方案有什么建议?(技术选型、数据来源、实现路径) +3.【认领】如果你需要参与,创建 sub task 并在 parent task comment 注册: + - 创建 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\": \"...\"}}"}} + - 创建后在 parent task comment: "[你的角色名] 我创建了 sub: 任务名,我负责 简述" +4.【风险】如果你发现风险、不合理的假设、或遗漏的环节,直接提出 + +⚠️ 每个 agent 必须 comment。即使你认为和自己无关,也要说明原因——这证明你读过并思考过了。 + +## Comment 格式 + +你的 comment 必须以角色名开头,让其他人知道你是谁: +[角色名] 你的观点 +例:[张飞] 我来负责策略编码,用 vnpy CtaTemplate 实现。 +例:[关羽] 这个策略需要风控,连亏 3 天应暂停。 +例:[赵云] 数据已就绪,2024-08 缺失已补齐。 + ## 黑板 API -你可以随时: -- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all(含 comments、outputs) +- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all - 写 comment:POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/comments - body: {{"author": "{agent_id}", "body": "内容(@agent-id 自动路由)"}} + body: {{"author": "{agent_id}", "body": "内容"}} - 创建 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 ## 行为准则 @@ -387,12 +411,14 @@ curl -X POST http://{self.api_host}:{self.api_port}/api/projects/{project_id}/ta goal_snapshot = description or title constraints = must_haves or "(无特殊约束)" + 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, + agent_identity=agent_identity, api_host=self.api_host, api_port=self.api_port, ) diff --git a/src/daemon/toolchain_handler.py b/src/daemon/toolchain_handler.py index 368ed15..24a0057 100644 --- a/src/daemon/toolchain_handler.py +++ b/src/daemon/toolchain_handler.py @@ -331,6 +331,11 @@ 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") + # 1. 优先检查 action_report comment report_row = conn.execute( "SELECT id FROM comments WHERE task_id=? " diff --git a/src/daemon/toolchain_templates.py b/src/daemon/toolchain_templates.py index e767188..9dc5b81 100644 --- a/src/daemon/toolchain_templates.py +++ b/src/daemon/toolchain_templates.py @@ -87,7 +87,8 @@ def render_template(name: str, variables: Dict[str, str]) -> str: def clear_cache() -> None: """清空模板缓存(用于测试或热更新)""" _template_cache.clear() - _steps_cache.clear() + global _steps_cache + _steps_cache = None # 重置为 None,强制下次 reload # ---------------------------------------------------------------------------