56 Commits

Author SHA1 Message Date
jiangwei-infra 89bbfeb54a Merge PR #126: docs(§22): v1.3 TC Discuss 重构 + Gitea Round Review 适配设计 2026-06-25 10:18:49 +00:00
claude_dev 0e8e9d0c24 Merge branch 'main' into docs/22-tc-discuss-and-gitea-round-review
CI / lint (pull_request) Successful in 21s
CI / test (pull_request) Successful in 26s
CI / frontend (pull_request) Successful in 13s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-25 18:18:38 +08:00
claude_dev 24600c38f9 chore: retrigger CI via synchronize 2026-06-25 18:15:11 +08:00
claude_dev 37a6257689 Merge branch 'main' into docs/22-tc-discuss-and-gitea-round-review 2026-06-25 08:42:13 +08:00
claude_dev 3c58baa00f chore: retrigger CI (paths-ignore fix #129 merged) 2026-06-25 08:39:44 +08:00
jiangwei-infra ddd2b016f8 Merge PR #129: fix(ci): 移除 paths-ignore
Deploy / ci (push) Successful in 30s
Deploy / deploy (push) Successful in 13s
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 2s
2026-06-25 00:39:25 +00:00
claude_dev 1fd9e9c7de fix(ci): 移除 paths-ignore,修复分支保护 status check 冲突
CI / lint (pull_request) Successful in 22s
CI / test (pull_request) Successful in 34s
CI / frontend (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Successful in 1s
根因:CI paths-ignore 跳过纯文档 PR → 无 status → 分支保护拒绝合并
修复:移除 paths-ignore,所有 PR 均触发 CI(lint 对 src/ 检查,文档改动秒级通过)

Closes #128
2026-06-25 08:37:37 +08:00
claude_dev c19286eb5b [moz] docs(§22): v1.5 采纳司马懿 S1-S4+P1-P3 审查建议 2026-06-25 07:45:26 +08:00
claude_dev a376fdadbc [moz] docs(§22): v1.4 设计补足 — 6 个问题修复(基础设施排除/单spawn自主模式/迁移策略/TC数据源分流通透) 2026-06-25 07:41:17 +08:00
jiangwei-infra 1d539f4f58 Merge PR #127: [moz] docs(§98): CloudCLI 外网部署与 VPS 服务暴露规范 2026-06-24 23:27:53 +00:00
claude_dev cafb3f991d [moz] docs(§98): CloudCLI 外网部署与 VPS 服务暴露规范 2026-06-25 07:24:54 +08:00
cfdaily 1ec950a7ce [moz] docs(§22): v1.3 §22.6 TC Discuss 重构 + §22.8 TC Round Review Gitea 适配设计 2026-06-24 08:46:46 +08:00
pangtong-fujunshi f99c4c38ae Merge PR #125: [moz] impl(§22): P1+P2 轻量路径+数据流+Round Review Gitea 适配
Deploy / ci (push) Failing after 14m0s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-23 23:29:16 +00:00
cfdaily 35959e19fa [moz] impl(§22): P1+P2 轻量路径+数据流+Round Review Gitea 适配
CI / lint (pull_request) Successful in 47s
CI / test (pull_request) Successful in 1m0s
CI / frontend (pull_request) Failing after 46s
CI / notify-on-failure (pull_request) Successful in 2s
§22 全部未完成项一次性补齐:

A. 文档更新:
- §22.3/§22.4 Phase 1 状态更新为  已实现
- §22.5 P0/P1/P2 标记全部完成
- §22.6/§22.7 更新实现状态

B2. flow/* label 识别(toolchain_routes.py):
- opened 分支识别 flow/direct → 创建 executor task(跳过 discussion)
- flow/discuss 无 assignee 时走默认 discussion 路径

B3. Discussion prompt 降级机制(spawner.py):
- DISCUSSION_PROMPT_TEMPLATE 加降级引导

B4. task_state 表 + parent_issue 解析(toolchain_routes.py):
- _init_task_state_table: CREATE TABLE IF NOT EXISTS task_state
- _ensure_task_state: 解析 [parent #N] 写入 parent_issue
- _handle_issues assigned 分支调用

B5. _check_round_complete 双源扫描(ticker.py):
- 扫 tasks.parent_task(黑板路径)+ task_state.parent_issue(Gitea 路径)
- 合并两个来源的 parent IDs
2026-06-23 23:46:20 +08:00
pangtong-fujunshi 493d0f017b Merge PR #124: [moz] impl(§22): P0 Phase 1 Discussion Broadcast
Deploy / ci (push) Successful in 36s
Deploy / deploy (push) Successful in 18s
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-22 23:36:12 +00:00
cfdaily dc1d444fed [moz] impl(§22): P0 Phase 1 Discussion Broadcast
CI / lint (pull_request) Successful in 25s
CI / test (pull_request) Successful in 34s
CI / frontend (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Successful in 0s
- DISCUSSION_PROMPT_TEMPLATE 改为 Gitea API(Issue comment + sub Issue 创建)
- _build_discussion_prompt 从 must_haves 解析 repo/issue_number 注入模板
- _broadcast_claim 判断 action_type=issue_discussion → 用 discussion prompt
- 新增 _get_task_action_type 辅助方法
- Gitea 创建 flow/direct 和 flow/discuss label
2026-06-23 07:32:46 +08:00
jiangwei-infra ab59723b1e Merge PR #123
Deploy / ci (push) Successful in 3m24s
Deploy / deploy (push) Successful in 18s
Deploy / notify-deploy-failure (push) Successful in 3s
Deploy / notify-deploy-success (push) Successful in 2s
2026-06-22 23:20:09 +00:00
cfdaily 1f7e7f69ad fix(ci): deploy.yml Setup Python 清理旧 venv 防止 pip 缺失
CI / lint (pull_request) Successful in 19s
CI / test (pull_request) Successful in 35s
CI / frontend (pull_request) Successful in 22s
CI / notify-on-failure (pull_request) Successful in 0s
根因:deploy.yml 的 venv 创建未清理旧目录,导致 /tmp/ci-venv-deploy
复用损坏的 venv(pip 缺失)。ci.yml 已有 rm -rf 但 deploy.yml 遗漏。

修复:对齐 ci.yml 模式,在 venv 创建前 rm -rf + pip upgrade
2026-06-23 07:18:06 +08:00
jiangwei-infra 474db1263b Merge PR #122
Deploy / ci (push) Failing after 14s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 2s
2026-06-22 23:00:51 +00:00
cfdaily 09c763341a Merge branch 'main' of http://192.168.2.154:3000/sanguo/sanguo_moziplus_v2 into fix/ci-skip-docs-only-changes
CI / lint (pull_request) Successful in 25s
CI / test (pull_request) Successful in 44s
CI / frontend (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-23 06:57:10 +08:00
pangtong-fujunshi 11f1c28565 Merge PR #120: [moz] docs(§22): v1.1 轻量路径设计 + 数据流澄清 + 编号修正
Deploy / ci (push) Failing after 14s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-22 22:57:03 +00:00
cfdaily a584bb0abf [moz] ci: 文档改动跳过 CI/CD,仅代码改动触发
CI / lint (pull_request) Successful in 17s
CI / test (pull_request) Successful in 29s
CI / frontend (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Successful in 1s
2026-06-23 06:54:40 +08:00
cfdaily 7a2505b137 [moz] ci: re-trigger (lint job Gitea status bug)
CI / lint (pull_request) Successful in 21s
CI / test (pull_request) Successful in 45s
CI / frontend (pull_request) Successful in 26s
CI / notify-on-failure (pull_request) Successful in 1s
2026-06-22 21:25:44 +08:00
cfdaily c2e710bf67 chore: retrigger CI (runner was stuck)
CI / lint (pull_request) Successful in 23s
CI / test (pull_request) Successful in 51s
CI / frontend (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-22 20:43:07 +08:00
cfdaily d2e449bca8 [moz] docs(§22): v1.1 轻量路径设计 + 数据流澄清 + 编号修正
CI / lint (pull_request) Failing after 32s
CI / test (pull_request) Has been skipped
CI / frontend (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 2s
2026-06-22 20:28:44 +08:00
pangtong-fujunshi 91116520d2 Merge PR #119: [moz] docs(§22): 端到端任务流程设计 v1.0
Deploy / ci (push) Failing after 13m41s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-22 11:39:32 +00:00
cfdaily 6536d99e7f [moz] docs(§22): 端到端任务流程设计 v1.0
CI / lint (pull_request) Successful in 26s
CI / test (pull_request) Successful in 47s
CI / frontend (pull_request) Successful in 30s
CI / notify-on-failure (pull_request) Successful in 0s
把 §21 散落在各章节的设计点串成一条完整链路:
- Phase 0-8 覆盖从 parent Issue 创建到关闭
- 每个 Phase 标注 4 维度(触发/daemon/prompt/agent)
- prompt 模板对照表(设计 vs 实际)
- 差距分析:Phase 1 discussion broadcast 是核心断裂点(P0)

Closes #119
2026-06-22 19:35:19 +08:00
zhangfei-dev cbb58c3092 Merge PR #118: [moz] feat(algorithms): 三种排序算法实现最大值查找
Deploy / ci (push) Failing after 14s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-21 22:34:04 +00:00
cfdaily a402bb2a2d [moz] fix(test): 补修 test_e2e_v31/test_four_phase 的 sys.path 污染
CI / lint (pull_request) Successful in 21s
CI / test (pull_request) Successful in 41s
CI / frontend (pull_request) Successful in 14s
CI / notify-on-failure (pull_request) Successful in 0s
PR #110 遗漏的两个文件,同样在模块级执行 sys.path.insert(DEPLOY_DIR)
导致 CI 中 src 模块从安装目录加载。统一加 allow_module_level skip guard。

- tests/e2e/test_e2e_v31.py
- tests/e2e/test_four_phase.py
2026-06-22 06:30:46 +08:00
cfdaily ac8444bb41 [moz] feat(algorithms): 三种排序算法实现最大值查找
CI / lint (pull_request) Successful in 19s
CI / test (pull_request) Failing after 42s
CI / frontend (pull_request) Successful in 19s
CI / notify-on-failure (pull_request) Successful in 1s
- find_max_linear: 线性扫描 O(n)(原 find_max 重命名+alias兼容)
- find_max_bubble: 冒泡排序 O(n²)
- find_max_quickselect: 快速选择 partition O(n) avg
- 37 个测试(7×3 参数化 + 9 一致性 + 7 原有)
2026-06-22 06:25:22 +08:00
pangtong-fujunshi d39f2ba1df Merge PR #116
Deploy / ci (push) Failing after 14s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-21 15:55:54 +00:00
cfdaily 548e39d3b8 [moz] fix: S1 _ACTION_HINTS review_comment 补充 @提示(司马懿 Review)
CI / lint (pull_request) Successful in 27s
CI / test (pull_request) Successful in 54s
CI / frontend (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-21 23:53:06 +08:00
cfdaily 1a536e84c6 [moz] impl(§15b): Comment @assignee 行为约束 prompt 约束
CI / lint (pull_request) Successful in 24s
CI / test (pull_request) Successful in 1m4s
CI / frontend (pull_request) Successful in 31s
CI / notify-on-failure (pull_request) Successful in 0s
ToolchainConstraintsSection §5 加一条:
在 Issue/PR 上写 comment 时,如果内容需要 assignee/创建者知晓,必须 @对方。

Closes #116
2026-06-21 23:46:42 +08:00
pangtong-fujunshi d25eea4e82 Merge PR #115: [moz] docs(§21): v1.2 §15b Comment @assignee 行为约束
Deploy / ci (push) Failing after 14s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-21 15:30:10 +00:00
cfdaily 38c676c627 [moz] docs(§21): v1.2 §15b Comment @assignee 行为约束
CI / lint (pull_request) Successful in 31s
CI / test (pull_request) Successful in 53s
CI / frontend (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Successful in 0s
问题:agent 在 Issue/PR 上写 comment 不 @ 人时,assignee 收不到通知。
方案:prompt 约束(不走代码自动通知),让 agent 自主判断何时 @assignee。
不做:代码路径 3(自动通知 assignee,噪音问题)、subscribe 机制。

Closes #115
2026-06-21 23:27:10 +08:00
pangtong-fujunshi 0c1acbd073 Merge PR #113
Deploy / ci (push) Failing after 18s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-21 15:04:35 +00:00
cfdaily b921b094ca [moz] impl(§21): §11b Issue opened 无 assignee → discussion task delegate 庞统
CI / lint (pull_request) Successful in 33s
CI / test (pull_request) Successful in 47s
CI / frontend (pull_request) Successful in 29s
CI / notify-on-failure (pull_request) Successful in 0s
设计文档 PR #112 已合并。本 PR 实现代码改动:

1. _send_toolchain_task to_agent 允许 None(None 时跳过 AGENT_IDS 校验)
2. _handle_issues opened 分支:无 assignee + 有 type/* label → 创建 issue_discussion task(assignee=None)
3. router 对 assignee=None → delegate 庞统(FALLBACK_AGENT,现有机制)
4. toolchain_handler verify: issue_discussion auto-pass
5. _ACTION_HINTS + EVENT_LABELS_ZH 加 issue_closed/issue_discussion

司马懿 Review 建议项处理:
- S1: router FALLBACK_AGENT=pangtong-fujunshi(router.py:71),assignee=None 必走 delegate
- S2: _send_toolchain_task 校验已改为 to_agent is not None and ...(line 246)

Closes #113
2026-06-21 21:18:29 +08:00
pangtong-fujunshi 7bb7637d24 Merge PR #112
Deploy / ci (push) Failing after 17s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-21 12:57:01 +00:00
cfdaily 02883750f5 [moz] docs(§21): v1.1 §11b Issue opened 无 assignee 处理 + 修正 §13.1 触发路径
CI / lint (pull_request) Successful in 42s
CI / test (pull_request) Successful in 43s
CI / frontend (pull_request) Successful in 21s
CI / notify-on-failure (pull_request) Successful in 1s
- 新增 §11b: 无 assignee 的 Issue opened → toolchain task(assigee=None) → delegate 庞统 discussion
- 修正 §13.1: 原设计写 issues/assigned,实际 Gitea 无 assignee 的 Issue 发的是 issues/opened
- §11b.3: _send_toolchain_task to_agent 允许 None
- §11b.6: 为什么用 delegate 而非 broadcast 的设计决策

Closes #112
2026-06-21 20:54:16 +08:00
zhangfei-dev 60925c3395 Merge PR #110: [moz] fix(test): 修复 integration/e2e 测试 sys.path 污染导致 CI 失败
Deploy / ci (push) Failing after 8s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-20 23:30:46 +00:00
cfdaily 7335cafa90 [moz] fix(test): 补回 test_e2e_v27.py 的 pytest.mark.e2e 标记 (Review G1)
CI / lint (pull_request) Successful in 25s
CI / test (pull_request) Successful in 1m1s
CI / frontend (pull_request) Successful in 16s
CI / notify-on-failure (pull_request) Successful in 1s
2026-06-21 07:27:47 +08:00
cfdaily 1362cc5d64 [moz] fix(test): 修复 integration/e2e 测试 sys.path 污染导致 CI 失败
CI / lint (pull_request) Successful in 19s
CI / test (pull_request) Successful in 43s
CI / frontend (pull_request) Successful in 16s
CI / notify-on-failure (pull_request) Successful in 0s
根因:4 个测试文件在模块级别执行 sys.path.insert(0, DEPLOY_DIR),
即使测试被 deselect/conftest 跳过,模块仍被 import 触发 sys.path 污染。
后续测试 import src 时从安装目录加载(缺少新增模块如 algorithms/)。

修复:在 sys.path 操作前加 allow_module_level skip guard,
未设置 RUN_INTEGRATION 时跳过整个模块,不执行任何模块级代码。

影响文件:
- tests/integration/test_e2e_api_s1_s8.py
- tests/e2e/test_e2e_stress.py
- tests/e2e/test_e2e_scenarios.py
- tests/e2e/test_e2e_v27.py
2026-06-21 07:23:41 +08:00
zhangfei-dev 6df7563070 Merge PR #109: [moz] feat(algorithms): 排序算法实现最大值查找
Deploy / ci (push) Failing after 7s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 1s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-20 23:15:02 +00:00
cfdaily da137ba193 [moz] feat: 排序算法实现最大值查找
CI / lint (pull_request) Successful in 14s
CI / test (pull_request) Failing after 34s
CI / frontend (pull_request) Successful in 15s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-21 07:03:51 +08:00
pangtong-fujunshi ac4e28a57c Merge PR #107
Deploy / ci (push) Failing after 11m50s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-20 15:46:20 +00:00
cfdaily 41d60cca2c [moz] fix: M1 label type/feature→type/feat 对齐仓库实际 label(姜维 Review)
CI / lint (pull_request) Successful in 45s
CI / test (pull_request) Successful in 1m0s
CI / frontend (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Successful in 1s
- impl.yml: type/feature → type/feat
- toolchain_routes.py: business_type feature→feat(匹配 Gitea label)
- toolchain-templates.yaml: feature→feat
2026-06-20 23:40:15 +08:00
cfdaily bae3244e24 [moz] fix: S2 _steps_cache None 崩溃修复(司马懿 Review 反馈)
clear_cache() 中 _steps_cache.clear() 改为 _steps_cache = None,
避免在 get_steps() 之前调用时 AttributeError。
2026-06-20 23:40:15 +08:00
cfdaily e238eaec31 [moz] fix: CI lint E303 + F401 修复
- toolchain_handler.py: 删除多余空行 (E303)
- toolchain_templates.py: 删除 unused json import (F401)
2026-06-20 23:40:15 +08:00
cfdaily b0bca0df5c [moz] fix(§21): GAP-1 issue_assigned 6路分流 + GAP-2 issue_closed auto-pass + GAP-3 Discussion Prompt v3
第一轮背靠背一致性检查发现的 3 个 GAP:

GAP-1: §21 §5 issue_assigned 按 type label 分流
- 从 labels 解析 business_type (feature/impl/bug/docs/refactor/test)
- 从 toolchain-templates.yaml get_steps() 获取对应 steps
- fallback 到硬编码 steps(向后兼容)

GAP-2: §21 §11 Issue closed 事件处理
- toolchain_handler verify 加 issue_closed auto-pass
- toolchain_routes 加 issue closed webhook → 纯通知 task

GAP-3: §21 §13.2 Discussion Prompt v3
- 新增 你是谁 段(agent_identity 注入)
- 新增 你必须做什么 4 维度(定位/建议/认领/风险)
- 新增 Comment 格式(角色名开头)
- Boids 行为准则不变
- 黑板 API 不变(§21 范围仅 toolchain)
2026-06-20 23:39:56 +08:00
pangtong-fujunshi 6d1d906551 Merge PR #106
Deploy / ci (push) Failing after 12m13s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-20 15:35:47 +00:00
cfdaily 37b8115485 [moz] fix(lint): remove unused json import in toolchain_templates.py
CI / lint (pull_request) Successful in 34s
CI / test (pull_request) Successful in 1m45s
CI / frontend (pull_request) Successful in 20s
CI / notify-on-failure (pull_request) Successful in 0s
2026-06-20 23:29:00 +08:00
cfdaily 7c11c6b9aa [moz] impl(§21): toolchain-templates.yaml + Issue 模板补全 + G1 修复 + §20 superseded
CI / lint (pull_request) Failing after 1m7s
CI / test (pull_request) Has been skipped
CI / frontend (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 2s
- config/toolchain-templates.yaml: §4 steps 模板化(7 种 business_type + ci_failure + review_result)
- src/daemon/toolchain_templates.py: 加 get_steps()/get_output_template() YAML 加载
- .gitea/ISSUE_TEMPLATE/: 补 impl.yml + docs.yml + refactor.yml
- src/daemon/toolchain_handler.py: G1 弯引号修复
- docs/design/20-issue-centric-orchestration.md: status 改为 superseded by §21

Closes #106
2026-06-20 23:10:00 +08:00
pangtong-fujunshi 027ae59d40 Merge PR #105
Deploy / ci (push) Failing after 9s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 2s
Deploy / notify-deploy-success (push) Successful in 0s
2026-06-20 14:49:33 +00:00
cfdaily 12f6ac3b1f [moz] feat(prompt): toolchain steps 加文档同步 + 司马懿/庞统 review 加需求-设计-编码一致性检查
CI / lint (pull_request) Successful in 49s
CI / test (pull_request) Successful in 47s
CI / frontend (pull_request) Successful in 19s
CI / notify-on-failure (pull_request) Successful in 0s
改动:
- toolchain_routes.py: issue_assigned/review_result/ci_failure steps 加文档同步 step
- toolchain_handler.py: ToolchainConstraintsSection 加 §6 文档同步约束 + Red Flag
- bootstrap.py: reviewer 硬约束加需求-设计-编码一致性检查
- ticker.py: 庞统 round review 三问第 2 问加 docs/design 同步确认
- prompt_composer.py: DeliveryChecklistSection 从弱提醒改为强制步骤

Closes #105
2026-06-20 22:47:11 +08:00
pangtong-fujunshi 623942bd15 Merge PR #104: [moz] docs(§21): v3 discussion prompt 重构 + 分支创建时机 + L2 约束
Deploy / ci (push) Failing after 15s
Deploy / deploy (push) Has been skipped
Deploy / notify-deploy-failure (push) Successful in 0s
Deploy / notify-deploy-success (push) Successful in 1s
2026-06-20 13:31:02 +00:00
cfdaily 280435119e [moz] docs(§21): 新增 §14b 分支与 PR 生命周期管理
CI / lint (pull_request) Successful in 24s
CI / test (pull_request) Successful in 1m7s
CI / frontend (pull_request) Successful in 23s
CI / notify-on-failure (pull_request) Successful in 0s
§14b.1 Sub Issue 完整生命周期(9 步:创建→分支→编码→PR→CI→Review→修改→Merge→清理)
§14b.2 Parent Issue 完整生命周期(5 步:创建→讨论→执行→Round Review→关闭)
§14b.3 核心规则(分支:Issue=1:1, 分支:PR=1:1, Closes #N, Parent #M)
§14b.4 多分支并行场景
§14b.5 PR body 标准模板
2026-06-20 21:28:46 +08:00
30 changed files with 2053 additions and 87 deletions
+27
View File
@@ -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
+33
View File
@@ -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
+27
View File
@@ -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
+3
View File
@@ -2,6 +2,9 @@
# #
# 触发条件: # 触发条件:
# - pull_requestopened, synchronize # - pull_requestopened, synchronize
# - 所有 PR 均触发 CI,确保 status check 始终产出
# (移除 paths-ignore:与分支保护 enable_status_check 冲突,
# 纯文档 PR 无 CI status 导致无法合并)
# #
# 注意:只保留 pull_request 触发,避免 push + pull_request 双倍触发 # 注意:只保留 pull_request 触发,避免 push + pull_request 双倍触发
# #
+9
View File
@@ -2,6 +2,7 @@
# #
# 触发条件: # 触发条件:
# - push 到 main 分支 # - push 到 main 分支
# - 仅代码改动触发部署,纯文档改动跳过(paths-ignore
# #
# Gitea v1.23.4 限制注意: # Gitea v1.23.4 限制注意:
# - 不支持 failure() 表达式 # - 不支持 failure() 表达式
@@ -13,6 +14,12 @@ name: Deploy
on: on:
push: push:
branches: [main] branches: [main]
paths-ignore:
- 'docs/**'
- 'mockups/**'
- 'prompt_templates/**'
- '*.md'
- 'README.md'
jobs: jobs:
# ── Job 1: CI(main 分支跑完整测试)───────────────── # ── Job 1: CI(main 分支跑完整测试)─────────────────
@@ -25,7 +32,9 @@ jobs:
env: env:
no_proxy: "*" no_proxy: "*"
run: | run: |
rm -rf /tmp/ci-venv-deploy
python3 -m venv /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 /tmp/ci-venv-deploy/bin/pip install --quiet flake8 fastapi pydantic pyyaml uvicorn requests pytest pytest-asyncio httpx
- name: Lint - name: Lint
+1
View File
@@ -35,3 +35,4 @@ inbox/*.jsonl
# E2E test data # E2E test data
data/e2e-*/ data/e2e-*/
data/_mail/
+171
View File
@@ -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 通过后创建 PRbody 含 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 通过后创建 PRbody 含 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 通过后创建 PRbody 含 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}"
- "创建 PRbody 含 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}"
- "重构 + 确保现有测试不 breakpython3 -m pytest tests/unit/ -q"
- "文档同步:如重构涉及架构变更,同步更新 docs/design/ 对应文档"
- "git add -A && git commit -m '[moz] refactor: {title}' && git push origin refactor/{issue_number}-{brief}"
- "CI 通过后创建 PRbody 含 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}"
- "创建 PRbody 含 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 reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_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-infralabel 必须包含 type/infrastructure"
- "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档"
- "提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report)— 报告中说明判断的原因类型和执行的操作,以及文档是否需要更新"
output_template: |
[Action Report]
**原因类型**{cause_type}
**操作**{action}
**CI 重跑**{ci_status}
**文档同步**{doc_status}
# ---------------------------------------------------------------------------
# review_result — APPROVED
# ---------------------------------------------------------------------------
review_result_approved:
steps:
- "合并 PRGitea API: POST /repos/{repo}/pulls/{pr_number}/merge"
- "提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_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 reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果"
output_template: |
[Action Report]
**修改内容**{changes}
**CI**{ci_status}
**文档同步**{doc_status}
@@ -2,7 +2,7 @@
title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面" title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面"
created: 2026-06-19 created: 2026-06-19
version: v2.1 draft version: v2.1 draft
status: draft status: superseded by §21 (toolchain 部分)
changelog: v2.1 修正 M1dispatcher 直接 SQL 声明)+ M2Phase 格式)+ S1/S2TaskAdapter 残留清理) changelog: v2.1 修正 M1dispatcher 直接 SQL 声明)+ M2Phase 格式)+ S1/S2TaskAdapter 残留清理)
v2.0 纳入姜维+司马懿 Review 反馈 + 庞统 Repository 模式修正 v2.0 纳入姜维+司马懿 Review 反馈 + 庞统 Repository 模式修正
v1.1 纳入姜维 Review 反馈 v1.1 纳入姜维 Review 反馈
+317 -6
View File
@@ -1,9 +1,11 @@
--- ---
title: "Unified Toolchain Design — 统一工具链工作流设计" title: "Unified Toolchain Design — 统一工具链工作流设计"
created: 2026-06-20 created: 2026-06-20
version: v1.0 draft version: v1.2 draft
status: draft status: draft
changelog: v1.0 初版 changelog: v1.2 补充 §15b Comment @assignee 行为约束
v1.1 补充 §11b Issue opened 无 assignee 处理 + 修正 §13.1 触发路径
v1.0 初版
--- ---
# Unified Toolchain Design # Unified Toolchain Design
@@ -569,6 +571,125 @@ issue_closed 走 auto-pass(和 review_merged 一样),纯通知不需要 ag
--- ---
## §11b. Issue opened 无 assignee 处理
### 11b.1 问题
`_handle_issues``action == "opened"` 分支只处理"部署失败"关键词和 @mention。无 assignee 的普通 Issue(如庞统创建的 parent Issue)被静默忽略,无法触发 discussion 流程。
### 11b.2 设计
`_handle_issues``opened` 分支新增无 assignee 处理路径:
```
Gitea Issue 创建(无 assignee, 有 type/* label
→ webhook: issues/opened
→ _handle_issues:
action == "opened"
→ 非"部署失败"
→ 无 assignee + 有 type/* label
→ 创建 toolchain taskassignee=None, action_type=issue_discussion
→ ticker 扫到 pending task
→ router.route: assignee=None → 不走快速路径 4 → delegate 庞统
→ 庞统收到 discussion promptspawner._build_discussion_prompt
→ 庞统在 Issue 上 comment 发起讨论,引导其他 agent 参与
```
### 11b.3 `_send_toolchain_task` 改动
`to_agent` 参数允许 `None`
```python
def _send_toolchain_task(
to_agent: str | None, # None = 无指派,待路由
...
) -> str:
# None 不校验 AGENT_IDS(非 None 时仍然校验)
if to_agent is not None and to_agent not in AGENT_IDS:
logger.warning("Unknown agent: %s, skipping toolchain task", to_agent)
return ""
task = Task(
...
assignee=to_agent, # None → 待路由
...
)
```
### 11b.4 `_handle_issues` opened 分支改动
```python
elif action == "opened":
if "部署失败" in issue_title:
# 部署失败处理(不变)
...
elif not (issue.get("assignees") or issue.get("assignee")):
# §11b: 无 assignee 的普通 Issue
labels_list = [lbl.get("name", "")
for lbl in (issue.get("labels") or [])]
has_type_label = any(lbl.lower().startswith("type/") for lbl in labels_list)
if not has_type_label:
return # 无 type label 不处理(避免噪音)
title = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
_send_toolchain_task(
to_agent=None, # 无指派 → router delegate 庞统
title=title,
description=f"## Issue 需要讨论\n\n{issue.get('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.get("body", ""),
},
)
return
# @mention 检查(不变)
...
```
### 11b.5 ToolchainHandler verify 改动
`issue_discussion` 走 auto-pass(纯触发,不需要 agent 执行步骤):
```python
if meta.get("action_type") == "issue_discussion":
return VerifyResult(True, "discussion_passthrough",
"issue_discussion auto-pass")
```
### 11b.6 为什么用 delegate 而非 broadcast
现有 ticker 有两种调度路径:
- **deterministic**:有 assignee → 直接 spawn 给该 agent
- **broadcast**:无 assignee → 所有空闲 agent 收到 claim prompt
无 assignee 的 Issue 如果走 broadcast,每个 agent 收到的是 claim prompt(认领 task),不是 discussion prompt(讨论 Issue)。而 §13.2 设计的 discussion prompt 才是正确行为——agent 应该讨论 Issue 内容,自主决定是否参与,而不是竞争认领一个 task。
**delegate 庞统**是更合理的路径:
- 庞统收到 task → 读取 Issue body → 按副军师职责发起讨论
- 庞统在 Issue 上 comment 引导其他 agent 参与
- 其他 agent 看到 @mention 后自主创建 sub Issue
如果未来需要多 agent 并行讨论,可以在 ticker 中新增 discussion broadcast 路径(spawn_type=discussion)。但 MVP 阶段 delegate 庞统已足够。
### 11b.7 涉及改动
| 文件 | 改动 |
|------|------|
| `src/api/toolchain_routes.py` `_send_toolchain_task` | `to_agent` 允许 None |
| `src/api/toolchain_routes.py` `_handle_issues` | opened 分支加无 assignee 路径 |
| `src/daemon/toolchain_handler.py` `verify_completion` | issue_discussion auto-pass |
| `src/daemon/toolchain_handler.py` `_ACTION_HINTS` | 新增 issue_discussion |
| `src/daemon/toolchain_handler.py` `EVENT_LABELS_ZH` | 新增 issue_discussion |
| `tests/` | 新增 issue_discussion 测试 |
---
## §12. AI Native 能力完整性(v2 补充) ## §12. AI Native 能力完整性(v2 补充)
> 本节确保 Gitea 替代黑板后,PRD-v3.0 的 AI native 能力不降级。 > 本节确保 Gitea 替代黑板后,PRD-v3.0 的 AI native 能力不降级。
@@ -606,12 +727,17 @@ issue_closed 走 auto-pass(和 review_merged 一样),纯通知不需要 ag
庞统创建 parent Issue(无 assignee)后,触发 discussion 庞统创建 parent Issue(无 assignee)后,触发 discussion
``` ```
庞统创建 parent Issue → webhook: issues/assigned(或 ticker 发现 pending 无 assignee 庞统创建 parent Issue无 assignee
daemon 检测:无 assignee = 广播讨论 Gitea webhook: issues/opened(注意:不是 assigned,无 assignee 的 Issue 只触发 opened
ticker 广播 spawn 所有 agentspawn_type=discussion _handle_issues opened 分支检测:无 assignee + 有 type/* label
每个 agent 收到 DISCUSSION_PROMPT_TEMPLATE 创建 toolchain taskassignee=None, action_type=issue_discussion
→ ticker 扫到 pending task → router delegate 庞统
→ 庞统收到 discussion prompt → 在 Issue 上 comment 发起讨论
→ 其他 agent 看到讨论 → 自主创建 sub Issue 认领
``` ```
**关键修正(v1.1**:原设计写 "webhook: issues/assigned",实际 Gitea 对无 assignee 的 Issue 只发 `issues/opened` 事件。`_handle_issues``opened` 分支需要新增无 assignee 处理路径(见 §11b)。
### 13.2 Discussion Prompt 设计(v3 重构) ### 13.2 Discussion Prompt 设计(v3 重构)
**设计参考**:Edict(角色驱动主动发言)+ APM(自包含 Task Prompt+ PAV 循环(输入/输出/验证)。 **设计参考**:Edict(角色驱动主动发言)+ APM(自包含 Task Prompt+ PAV 循环(输入/输出/验证)。
@@ -797,6 +923,138 @@ daemon 监听 Issue 创建 webhook → 解析标题中的 `[parent #N]` → 记
--- ---
## §14b. 分支与 PR 生命周期管理
> 解决分支、PR、Issue 之间的割裂问题,统一定义完整生命周期。
### 14b.1 Sub Issue 的分支/PR 生命周期
```
① 创建 Sub Issue
Agent 在 discussion 阶段创建 sub Issueassign 自己)
→ 分支:还不存在
→ daemon task_state: status=pending, parent_issue=N
② 分支创建(executor 阶段)
Agent 收到 executor prompt → git checkout -b {type}/{sub_issue_number}-{brief}
→ 分支名从 sub Issue number 派生,一一对应
→ daemon task_state: status=working(通过 dispatch 触发)
③ 编码
Agent 在分支上编码 → commit → push
→ 分支远程存在,Gitea 可见
④ PR 创建
Agent 创建 PRhead: {type}/{sub_issue_number}-{brief} → base: main
PR body 必须包含:
- `Closes #{sub_issue_number}`(让 Gitea merge 时自动关 sub Issue
- `Parent: #{parent_issue_number}`(关联到 parent
- 改动说明(改了什么、为什么)
→ webhook: pull_request/opened → daemon task_state: status=review
⑤ CI
PR 创建 → CI 自动触发(lint + test + frontend
→ 通过:等 Review
→ 失败:toolchain handler 创建 ci_failure task → agent 修复 → push 到同分支 → CI 重跑
⑥ Review
司马懿收到 Review 请求 → 读 PR diff → 提交 ReviewReview API
→ APPROVED:通知 agent 合并
→ REQUEST_CHANGES:通知 agent 修改
⑦ 修改(如果有)
Agent 在同分支上修改 → commit → push
→ PR 自动更新(不新建 PR)
→ CI 重跑 → 司马懿重新 Review
→ 循环回到 ⑥
⑧ Merge
Agent 或 Reviewer merge PR
→ Gitea 自动关闭 sub Issue(因为 PR body 有 Closes #N
→ webhook: pull_request/closed(merged=true)
→ daemon 终态信号:task_state status=done
⑨ 分支清理
Gitea 配置自动删除已合并分支(推荐)
Issue 保持 closed 状态(完整历史保留)
```
### 14b.2 Parent Issue 的生命周期
```
① 创建(庞统)
庞统创建 parent Issue(无 assignee
→ 触发 discussion 广播
→ parent Issue 不创建分支(parent 是讨论和编排层)
② Discussion
Agents 讨论、创建 sub Issues、在 parent comment 注册
③ 执行
所有 sub Issues 走 §14b.1 生命周期
④ Round Review
所有 sub Issues 终态 → 庞统三问
→ GOAL_ACHIEVED → 庞统关闭 parent Issue
→ 需要新轮 → 庞统创建新 sub Issues → 回到 ③
⑤ 关闭
庞统关闭 parent Issue
→ webhook: issues/closed → daemon 终态
```
### 14b.3 核心规则
| 规则 | 定义 | 原因 |
|------|------|------|
| 分支 : sub Issue = 1:1 | 每个 sub Issue 一个分支 | 分支名从 Issue number 派生 |
| 分支 : PR = 1:1 | 每个分支一个 PR | Review 驳回不新建 PRpush 到同分支更新 |
| sub Issue : PR = 1:1 | 一个 sub Issue 一个 PR | 简单场景。复杂 sub 按需拆多个 sub Issue |
| PR body 必须含 Closes #N | N = sub Issue 编号 | merge 后自动关 Issue |
| PR body 必须含 Parent #M | M = parent Issue 编号 | 关联到 parent,方便追溯 |
| parent Issue 不创建分支 | parent 是讨论编排层 | 代码产出在 sub Issue 的分支 |
| discussion 不碰 git | 只读 Issue + comment + 创建 sub | 避免讨论阶段产生无意义 commit |
| 分支名格式 | {type}/{sub_issue_number}-{brief} | 按 Issue 类型:fix/feat/impl/docs/refactor/test |
| merge 后自动删分支 | Gitea 配置 | 避免分支堆积 |
### 14b.4 多分支并行场景
一个 parent Issue 下多个 sub Issue 同时执行:
```
parent Issue #100(双均线策略)
├── sub #101 策略编码 → 分支 feat/101-dual-ma → PR #104
│ └── CI 跑 + Review → merge → Closes #101
├── sub #102 风控规则 → 分支 feat/102-risk-control → PR #105
│ └── CI 跑 + Review → merge → Closes #102
└── sub #103 策略测试 → 分支 test/103-strategy-test → PR #106
└── CI 跑 + Review → merge → Closes #103
所有 sub 终态 → 庞统 round review → parent Issue #100 关闭
```
每个 sub Issue 的分支、PR、CI、Review 独立流转,互不干扰。
### 14b.5 PR body 模板
```markdown
Closes #{sub_issue_number}
Parent: #{parent_issue_number}
## 改动说明
<改了什么、为什么>
## 验证
- [ ] CI 通过(lint + test
- [ ] 本地测试通过(如有)
## 改动文件
- `src/xxx.py`: <改了什么>
- `tests/test_xxx.py`: <新增什么测试>
```
---
## §15. @mention 通知迁移 ## §15. @mention 通知迁移
### 15.1 当前实现 ### 15.1 当前实现
@@ -822,6 +1080,59 @@ agent 写 Issue/PR comment → webhook: issue_comment/created → daemon 解析
--- ---
## §15b. Comment @assignee 行为约束
### 15b.1 问题
`_handle_issue_comment` 只有两条通知路径(CI 关键词 + @mention)。当 agent 在 Issue/PR 上写了 comment 但没有 @ 任何人时,Issue 的 assignee/创建者收不到通知。
例:姜维在 Issue #114 上写了排查结论,没有 @ 庞统。庞统不知道排查已完成。
### 15b.2 设计决策:prompt 约束 vs 代码路径
| 方案 | 优点 | 缺点 |
|------|------|------|
| 代码路径 3(自动通知 assignee) | 不依赖 agent 行为 | 噪音——每条 comment 都通知,review 来回几轮就炸 |
| prompt 约束(agent @assignee | agent 自主判断,语义精确 | 依赖 agent 遵守 |
**选择 prompt 约束**。理由:
1. 与 §21 设计哲学一致(agent 自主决策,不过度自动化)
2. 噪音问题是真实的——不是每条 comment 都需要通知 assignee
3. agent 可以根据场景判断:纯确认不 @,有结论/需关注才 @
4. 兜底机制:round review + 庞统人工检查
### 15b.3 约束内容
在 ToolchainConstraintsSection §5「所有协作通过 Gitea 完成」中新增一条:
```
### 5. 所有协作通过 Gitea 完成
...(现有内容不变)
- ⚠️ 在 Issue/PR 上写 comment 时,如果内容需要 Issue 的 assignee 或创建者知晓,必须在 comment 中 @对方。纯确认性回复(如"收到")不需要 @。
```
同时在 `_ACTION_HINTS` 中 review_comment 的 hint 补充提示:
```
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。回复时 @评论者。",
```
### 15b.4 不做的事
- 不在 `_handle_issue_comment` 中加代码路径 3(自动通知 assignee)
- 不做 subscribe/unsubscribe 机制
- 不通知 Issue 创建者(只通知 assigneeassignee 是责任人)
### 15b.5 涉及改动
| 文件 | 改动 |
|------|------|
| `src/daemon/toolchain_handler.py` ToolchainConstraintsSection §5 | 加 @assignee 约束 |
| `src/daemon/toolchain_handler.py` `_ACTION_HINTS` | review_comment hint 补充 @提示 |
约 +5 行,1 文件。
---
## §16. Round Review 迁移 ## §16. Round Review 迁移
### 16.1 当前实现 ### 16.1 当前实现
+711
View File
@@ -0,0 +1,711 @@
---
title: "End-to-End Flow — 端到端任务流程设计"
created: 2026-06-22
version: v1.5
status: draft
changelog: v1.5 采纳司马懿 S1-S4+P1-P3:判断逻辑补 infrastructure 分支、flow/discuss deprecated、定向讨论 fallback、task_state.status 完整生命周期、parent_status 查表不硬编码
v1.4 §22.6/§22.8 设计补足:基础设施排除、定向讨论简化为单spawn自主模式、迁移策略、TC数据源/round_count分流通透
v1.3 §22.6 重构为「所有 TC 流程第一步走 Discuss」+ §22.8 TC Round Review Gitea 适配设计
v1.2 §22.3/§22.4 更新 Phase 1 为 ✅ 已实现(PR #124
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 Issueassign 自己)
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`。action_type=issue_discussion 时调用 `_build_discussion_prompt`Gitea API),否则用 `_build_claim_prompt`(黑板 API |
| **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 IssueGitea APIassign 自己);在 parent Issue comment 注册 sub |
| **产出** | parent Issue 上有所有 agent 的讨论 commentGitea 上有若干 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 格式
[角色名] 你的观点
## APIGitea
- 读 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
- 至少有一个 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 + 创建 PRbody 含 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 通过** | 等待 ReviewPhase 5 |
| **CI 失败** | 创建 `ci_failure` task → 通知 agent 修复 |
| **agent promptCI 失败时)** | `ToolchainHandler.build_prompt`action_type=ci_failuresteps 含 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 IssuePR 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.2Gitea API | discussion prompt`_build_discussion_prompt`Gitea 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 | 同设计 | ✅ |
**唯一偏差已修复**PR #124 将 ticker broadcast 改为根据 action_type 选择 discussion promptGitea API)或 claim prompt(黑板 API)。
---
## §22.4 当前实现差距
| Phase | 实现状态 | 差距描述 |
|-------|---------|---------|
| 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task |
| 1 Discussion 广播 | ✅ **已实现** | PR #124 修复:ticker `_broadcast_claim` 判断 `action_type=issue_discussion` → 调用 `_build_discussion_prompt`Gitea API)。`_build_discussion_prompt``must_haves.context` 解析 `repo` / `issue_number` 注入模板 |
| 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 通知正常。executor promptYAML steps)中已包含 `Closes #{issue_number}` |
| 6 Round Review | ✅ **已实现** | `_check_round_complete` 支持双源扫描:黑板 `tasks.parent_task` + toolchain `task_state.parent_issue` |
| 7 parent Issue 关闭 | ✅ 已实现 | PR #113 issue_closed auto-pass |
---
## §22.5 差距优先级排序
| 优先级 | 差距 | 影响范围 | 修复建议 |
|--------|------|---------|---------|
| **P0** | Phase 1 discussion broadcast | ✅ **已完成**PR #124 | ~~核心断裂~~ 已修复:ticker 判断 action_type=issue_discussion → discussion prompt |
| **P1** | Phase 5 PR body Closes #N | ✅ **已完成** | YAML steps 已包含 Closes #NP0 修复后自然走通 |
| **P2** | Phase 6 Round Review Gitea 适配 | ✅ **已完成** | `_check_round_complete` 双源扫描 task_state.parent_issue + tasks.parent_task |
**关键结论**P0/P1/P2 全部完成。§22 端到端流程设计已全部实现。
---
## §22.6 轻量路径设计(Direct Assignment
> **核心原则:所有 TC 流程第一步都走 Discuss(基础设施和 flow/direct 除外)。**
> Discussion 不是可选项,而是 TC 流程的必经阶段——确保方案对齐、风险暴露后再进入 exec。
> 区别只在于「谁参与讨论」。
### 路径决策矩阵
| 条件 | 讨论范围 | 流程 |
|------|---------|------|
| Issue 无 assignee + type/* label | **广播所有空闲 agent** | Phase 0 → 1(广播讨论)→ 2 → … |
| Issue 有 assignee(非 infrastructure | **assignee 自主定向讨论** | assignee 写方案 → @司马懿 review → 创建 sub Issue 进 exec |
| Issue 有 `type/infrastructure` label | **跳过讨论** | → executor(运维排障,无需方案审查) |
| Issue 有 `flow/direct` label | **跳过讨论** | → Phase 2(极小改动逃生舱) |
**设计原则**
1. 基础设施 Issue`type/infrastructure`)→ 直接到 executor(运维排障任务,不需要写方案+审查)
2. `flow/direct` label → 直接到 executor(明确不需要讨论的小改动)
3. 有 assignee(非 infrastructure)→ assignee 自主走「方案→review→exec」流程,daemon 不编排
4. 没有 assignee → 广播所有 agent 讨论,有 agent 认领后创建 sub Issue
> **v1.4 补充**v1.3 原设计将 infrastructure 也纳入 Discuss,但基础设施排障任务(Gitea 挂了、磁盘满等)不需要写实现方案和司马懿审查,应排除。
### 判断逻辑
```
parent Issue 创建
├─ 有 flow/direct label?→ Direct 路径(→ Phase 2,跳过讨论)
├─ 有 type/infrastructure label?→ Direct 路径(→ executor,运维排障)
├─ 有 assignee(非 infrastructure)?→ 定向 Discussionassignee 自主模式)
└─ 无 assignee(默认)→ 广播 Discussion(所有空闲 agent
```
### 定向讨论流程(有 assignee,非 infrastructure
> **v1.4 修正**v1.3 原设计「ticker 同时 spawn assignee + 司马懿」过于复杂,需要 daemon 协调两个 agent 生命周期。
> v1.4 简化为**单 spawn 自主模式**——daemon 只创建 discussion task 给 assigneeassignee 在一个 session 内自主完成全部流程。
```
Issue assigned webhook
→ daemon 创建 issue_discussion taskassignee=该 agentcontext_data 带 issue/repo
→ ticker dispatch:有 assignee → 确定性路由 spawn assignee 单人
→ discussion prompt 引导 assignee
1. 读 Issue 全文,在 Gitea Issue comment 写实现方案(技术选型、实现路径、影响范围)
2. @simayi-challenger 请求方案审查(利用现有 @mention 机制自动创建 review task
3. 等司马懿 review 结果(通过 @mention 回传)
4. review 通过 → 创建 sub Issueassign 自己)→ 进入 exec
5. review 驳回 → 修改方案重新提交(重新 comment + @司马懿)
```
**为什么不双 spawn**
1. assignee 写方案时间不确定,双 spawn 后司马懿可能干等
2. 两阶段串行更简单,assignee 完成后 @mention 自动触发司马懿
3. 和广播讨论一致——daemon 只创建初始 task,后续靠 agent 自主 + @mention
**与广播讨论的区别**
- 广播讨论:assignee=None → ticker 归入 broadcast_tasks → 广播所有空闲 agent
- 定向讨论:assignee=该 agent → ticker 归入 deterministic_tasks → 确定性路由 spawn 单人
### 定向讨论 fallbackassignee 无响应)
> **v1.4 补充(司马懿 S4)**:广播讨论有「3 轮无 taker → 升级庞统」机制,定向讨论也需要 fallback。
单 spawn assignee 后,如果 assignee NO_REPLY 或 session 超时:
```
ticker 定期 check(复用现有 broadcast _broadcast_tracker 机制)
→ discussion task 仍 pending/working 且超过 3 轮 check 未终态
→ 升级庞统(escalated 状态)
→ 庞统判断:重新分配 / 转为广播讨论 / 直接关闭
```
复用现有 `_broadcast_tracker``round_number >= 3 → escalated` 逻辑,不引入新机制。
### 广播讨论流程(无 assignee
```
Issue opened webhook
→ daemon 创建 issue_discussion taskassignee=None
→ ticker broadcast:广播所有空闲 agent
→ discussion prompt 引导每个 agent
1. 在 Gitea Issue comment 回应(定位/建议/认领/风险)
2. 需要参与的 agent 创建 sub Issue assign 自己
```
### 适用场景
| 路径 | 适用场景 | 示例 |
|------|---------|------|
| 广播讨论 | 需求不明确、跨模块协作、涉及 3+ agent、无人认领 | 新功能设计、架构变更 |
| 定向讨论 | 需求明确但需要方案确认 | 有明确 assignee 的 Issue |
| Direct | 改 typo、改配置值等极小改动 | `flow/direct` label |
### Discussion 路径中的降级
Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及一个角色,任何 agent 可以在 comment 中建议:
```
@pangtong-fujunshi 建议直接指派 @agent-id,理由:...
```
庞统判断后创建 sub Issue 直接 assign → 该 agent 跳过讨论直接进入 Phase 2。
### 迁移策略
> **v1.4 补充**v1.3 未说明现有 `issue_assigned` 路径如何处理。
**保留 `issue_assigned` 给 infrastructure / flow-direct,新增定向讨论路径**
| 条件 | 旧路径(v1.2) | 新路径(v1.4) | 变化 |
|------|----------------|----------------|------|
| 有 assignee + 非 infrastructure | issue_assigned → executor | issue_discussion → discussion(定向) | **改** |
| 有 assignee + `type/infrastructure` | issue_assigned → executor | issue_assigned → executor(不变) | 无 |
| 有 `flow/direct` label | issue_assigned → executor | issue_assigned → executor(不变) | 无 |
| 无 assignee + type/* label | issue_discussion → broadcast | issue_discussion → broadcast(不变) | 无 |
`_ACTION_HINTS` / YAML steps / `ToolchainContextSection` 中的 `issue_assigned` 相关逻辑**保留**infrastructure/flow-direct 还用),新增 `issue_review_discussion` action_type。
> **`flow/discuss` label deprecated**v1.2 曾设计 `flow/discuss` label(强制 Discussion 即使有 assignee),v1.4 废弃。所有有 assignee 的 Issue(非 infrastructure)默认走定向讨论,无需显式 label。Gitea 上 `flow/discuss` labelid=101)保留但不影响路由。
### 实现方案
**toolchain_routes.py**
- `opened` 分支(无 assignee)→ discussion taskassignee=None)— 现有逻辑 ✅
- `assigned` 分支改造:
- `is_infrastructure=True` → executor task(现有逻辑保留 ✅)
- `has_flow_direct=True` → executor task(现有逻辑保留 ✅)
- **其他** → discussion taskassignee=该 agentcontext_data 带 issue/repo)— **需要改动**
**ticker.py**
- 定向讨论 task 有 assignee → `_dispatch_pending` 确定性路由直接 spawn assignee 单人
- 广播讨论 task 无 assignee → `_broadcast_claim` 广播所有空闲 agent(现有逻辑 ✅)
- `_broadcast_claim` 中的 action_type 判断逻辑保留(discussion task 走 `_build_discussion_prompt`
**讨论 prompt 差异化**
- 定向讨论 prompt 额外指引:你是 assignee,需要写方案并主动 @司马懿 review
- 广播讨论 prompt 保持现有模板(4 维度回应 + 自主认领)
**不影响**mail / task 流程(它们不走 toolchain_routes.py
---
## §22.7 数据流设计澄清
> 本文档涉及两个数据源(黑板 DB 和 Gitea Issue),parent/sub 映射的数据流必须明确。
### 当前状态 vs 设计目标
| 数据 | 当前存储 | §20/§21 设计目标 | 状态 |
|------|---------|-----------------|------|
| 协作面(title/body/comment | Gitea Issue/PRPhase 1 已修复) | Gitea Issue/PR | ✅ |
| parent/sub 映射 | `task_state.parent_issue`(已实现) + `tasks.parent_task`(兼容) | `task_state.parent_issue`(新表) | ✅ |
| 执行状态 | toolchain DB tasks.status + task_state.status | task_state.status | ✅ |
| 成果物 | git commit + PR | git commit + PR | ✅ |
### 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 表创建**:已在 PR #125 中实现(`_init_task_state_table`CREATE IF NOT EXISTS)。
2. **parent_issue 解析**:已实现(`_ensure_task_state``_handle_issues` assigned 分支解析 `[parent #N]`)。
3. **_check_round_complete 双源扫描**:已实现(`task_state.parent_issue` + `tasks.parent_task`)。
### 混合期处理
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) | — | — |
---
## §22.8 TC Round Review Gitea 适配
> **设计原则:三个流程已通过 Handler 架构分离(§14)。TC Round Review 的改造通过 ToolchainHandler 分流,不影响黑板流程。**
### 问题(v1.4 修正:不只是 prompt 不同,数据查询链路也不同)
`_check_round_complete` 中调用 `bb.get_subtasks_summary(parent_id)` / `bb.get_aggregate_outputs()` / `bb.get_round_comments()` 全部查黑板 DB。但 TC 的子任务信息在 `task_state` 表 + Gitea Issue/PR**根本不在黑板 tasks/outputs/comments 表**。
因此 handler 分流不只是 prompt 层面,需要贯穿**数据查询 → prompt 构建 → round_count 递增**三个环节。
### TC 数据源 vs 黑板数据源对照
| 环节 | 黑板路径 | TC 路径 |
|------|---------|---------|
| 子任务状态 | `bb.get_subtasks_summary()` → tasks 表 | 新增 `get_tc_subtask_summary()` → task_state 表 |
| 成果物 | `bb.get_aggregate_outputs()` → outputs 表 | 庞统自己读 Gitea APIPR/commit |
| 讨论历史 | `bb.get_round_comments()` → comments 表 | 庞统自己读 Gitea Issue comments |
| round_count | `bb.increment_round_count()` → tasks.round_count | `UPDATE task_state SET round_count+1` |
| parent status | tasks.status | task_state.status(需补充更新逻辑) |
### task_state.status 更新机制
> **v1.4 补充(司马懿 S3**:当前 `_ensure_task_state` 只做 `INSERT OR IGNORE`status 初始为 `pending`,后续没有任何代码更新。需补充。
**状态生命周期**
| 状态 | 触发来源 | 更新位置 |
|------|---------|----------|
| `pending` | `_ensure_task_state` 初始插入 | `toolchain_routes.py` assigned 分支 |
| `done` | Gitea Issue closed webhook | `toolchain_routes.py` closed 分支 |
| `failed` | executor task 验证失败且未重试 | `toolchain_handler.py` `post_complete``_update_task_state_status()` |
**关键路径说明**
1. **sub Issue 创建**`_ensure_task_state` 写入 `status='pending'`
2. **sub Issue 对应的 executor task 完成** → ToolchainHandler 通过 @mention 指引 agent 创建 PR(含 `Closes #N`)→ PR 合并时 Gitea 自动关闭 Issue → webhook `issues/closed` → 更新 `status='done'`
3. **executor task 失败**`ToolchainHandler.post_complete()` 中检测 verify 失败且 retry_count 达上限 → 调用 `_update_task_state_status(issue_number, 'failed')`
**`post_complete` 何时触发**spawner 完成 session 后(agent exit),ticker 在下个 tick 检测到 session 终态 → 调用 `handler.post_complete(task_id, agent_id, outcome, db_path)`。此时 handler 可以:
- 检查 verify 结果
- 如果 failed 且不可 retry → 查 task_state 找到对应 issue_number → 更新 status='failed'
**新增方法**
```python
# toolchain_handler.py
def _update_task_state_status(self, issue_number: int, status: str):
"""更新 task_state.statusexecutor 失败时调用)"""
tc_db = _toolchain_db_path()
conn = get_connection(tc_db)
try:
conn.execute(
"UPDATE task_state SET status=?, updated_at=datetime('now') "
"WHERE issue_number=?",
(status, issue_number)
)
conn.commit()
finally:
conn.close()
```
**toolchain_routes.py closed 分支补充**
```python
elif action == "closed":
# 更新 task_state status='done'
tc_db = _toolchain_db_path()
conn = get_connection(tc_db)
try:
conn.execute(
"UPDATE task_state SET status='done', updated_at=datetime('now') "
"WHERE issue_number=?",
(issue_number,)
)
conn.commit()
finally:
conn.close()
# ... 现有通知逻辑
```
> **注意**`get_tc_subtask_summary` 中的 `all_terminal` 只依赖 `done` 和 `failed` 两种终态。`pending`/`working` 计入 `other`,确保未完成的 sub 不会误触发 round review。
### 分流方案(全链路)
```
_check_round_complete:
handler = TaskTypeRegistry.get_by_project(project_id)
is_tc = handler and handler.virtual_project == "_toolchain"
# 1. 数据查询分流
if is_tc and parent_id in task_state_parents:
summary = get_tc_subtask_summary(tc_db, int(parent_id))
# 不查 outputs/comments — 庞统 spawn 后自己读 Gitea API
else:
summary = bb.get_subtasks_summary(parent_id)
outputs = bb.get_aggregate_outputs(parent_id)
comments = bb.get_round_comments(parent_id)
# 2. prompt 构建分流
if is_tc:
review_prompt = handler.build_round_review_prompt(parent_id, summary, ...)
else:
review_prompt = self._build_review_prompt(parent_task, summary, outputs, comments, ...)
# 3. round_count 递增分流
if spawned:
if is_tc:
UPDATE task_state SET round_count = round_count + 1 WHERE issue_number = ?
else:
bb.increment_round_count(parent_id)
```
### BaseTaskHandler 接口扩展
`BaseTaskHandler``task_type_registry.py`)中新增默认方法:
```python
def build_round_review_prompt(self, parent_id: str, summary: dict,
outputs: list, comments: list,
round_num: int, **kwargs) -> str:
"""构建 Round Review prompt。默认 raise NotImplementedError。"""
raise NotImplementedError(
f"{self.task_type} handler does not support round review")
```
普通 task 和 mail 不调用此方法(走 ticker 原有 `_build_review_prompt`),只有 ToolchainHandler override。
### ToolchainHandler.build_round_review_prompt
TC 版本的 Round Review prompt。核心区别:
| 维度 | 黑板版(原) | Gitea 版(新) |
|------|-----------|-------------|
| Goal 来源 | `parent_task.description`(黑板 DB | parent Issue body(庞统自己读 Gitea API |
| 成果物 | `bb.get_aggregate_outputs()`(黑板 DB | PR / commit(庞统自己读 Gitea API |
| 讨论历史 | `bb.get_round_comments()`(黑板 DB | Issue comments(庞统自己读 Gitea API |
| 创建新 sub | `POST localhost:8083/api/.../tasks`(黑板 API | `POST Gitea /repos/.../issues`Gitea API |
| 关闭 parent | 更新黑板 status | `PATCH Gitea /repos/.../issues/{N}` |
**prompt 设计**:只给 Issue 编号和 repo,让庞统 spawn 后自己调 Gitea API 读详情。daemon 不做 Gitea API 调用。
```
## 庞统 TC Round Review(第 {round_num} 轮)
### Parent Issue
- Repo: {repo}
- Issue: #{parent_issue_number}
### 本轮 Sub Issue 状态
- 完成: {done}
- 失败: {failed}
- 总计: {total}
### 你必须做什么
1. 读 parent Issue: GET /repos/{repo}/issues/{parent_issue_number}
2. 读所有 sub Issue 的 comments 和 PR
3. 三问评估:
- Goal 还清晰吗?
- 成果物覆盖 goal 了吗?(逐条检查 + docs/design 同步)
- 下一轮需要做什么?
4. 决策:
- GOAL_ACHIEVED → 关闭 parent Issue
- 需要新轮 → 创建新 sub Issues
### Gitea API
- 读 Issue: GET /repos/{repo}/issues/{N}
- 读 comments: GET /repos/{repo}/issues/{N}/comments
- 创建 sub Issue: POST /repos/{repo}/issues
- 关闭 Issue: PATCH /repos/{repo}/issues/{N} (state=closed)
```
### 不影响黑板流程的保证
1. **Handler 分流**`_check_round_complete` 通过 `TaskTypeRegistry.get_by_project(project_id)` 判断。普通项目(无 handler)走 `_build_review_prompt`(黑板版本不变)
2. **数据隔离**TC review 读 `task_state` 表 + Gitea API;黑板 review 读 `tasks` 表 + `outputs` / `comments`
3. **prompt 隔离**TC prompt 只含 Gitea API 指引;黑板 prompt 只含黑板 API 指引
4. **Handler 分流模式已在 `_dispatch_reviews` 中验证可行**L1379 `if handler: return []` 用 Handler 分流跳过 handler 项目的 PR Review 流程,TC Round Review 采用相同模式
### 实现方案
| 文件 | 改动 |
|------|------|
| `task_type_registry.py` | `BaseTaskHandler``build_round_review_prompt` 默认方法(raise NotImplementedError |
| `toolchain_handler.py` | `ToolchainHandler` override `build_round_review_prompt` → 返回 Gitea API 版 prompt;新增 `get_tc_subtask_summary()` 静态方法 |
| `ticker.py` | `_check_round_complete` 全链路分流:数据查询 + prompt 构建 + round_count 递增 |
| `toolchain_routes.py` | `_handle_issues` closed 分支:更新 `task_state.status = 'done'` |
### get_tc_subtask_summary 方法设计
```python
def get_tc_subtask_summary(tc_db: Path, parent_issue: int) -> Optional[dict]:
"""TC 路径:从 task_state 表查子任务状态摘要"""
conn = get_connection(tc_db)
try:
parent_row = conn.execute(
"SELECT round_count, status FROM task_state WHERE issue_number = ?",
(parent_issue,)
).fetchone()
if not parent_row:
return None
rows = conn.execute(
"SELECT status, COUNT(*) as cnt FROM task_state "
"WHERE parent_issue = ? GROUP BY status",
(parent_issue,)
).fetchall()
if not rows:
return None
summary = {
"parent_id": str(parent_issue),
"parent_status": parent_row["status"], # 从 task_state 读取实际状态
"round_count": parent_row["round_count"],
"total": 0, "done": 0, "failed": 0, "cancelled": 0, "other": 0,
}
for row in rows:
summary["total"] += row["cnt"]
if row["status"] in ("done", "failed", "cancelled"):
summary[row["status"]] += row["cnt"]
else:
summary["other"] += row["cnt"]
summary["all_terminal"] = summary["other"] == 0 and summary["total"] > 0
return summary
finally:
conn.close()
```
### TaskState round_count 同步
TC 路径的 `round_count` 存在 `task_state` 表中。`_check_round_complete` 需要在 spawn review 成功后递增:
```python
if handler and handler.virtual_project == "_toolchain":
# TC 路径:递增 task_state.round_count
conn.execute("UPDATE task_state SET round_count = round_count + 1 WHERE issue_number = ?", ...)
else:
# 黑板路径:原有 bb.increment_round_count(parent_id)
```
@@ -0,0 +1,131 @@
---
title: "专题 98 — CloudCLI 外网部署与 VPS 服务暴露规范"
created: 2026-06-25
version: v1.0
status: active
changelog: v1.0 初版
---
# 专题 98 — CloudCLI 外网部署与 VPS 服务暴露规范
## 1. 概述
CloudCLI 通过 FRP 内网穿透 + VPS Caddy 反代,绑定到 `claude.mysanguo.top` 实现外网访问。
整体链路:外网设备 → HTTPS → VPS CaddySSL 自动签发 + 反代)→ FRP 隧道 → Mac Mini CloudCLI。
## 2. 架构图
```
外网设备(浏览器)
↓ HTTPS
首尔 VPS 43.133.235.218
├── Caddy :443SSL 自动签发 + 反代)
│ └── claude.mysanguo.top → 127.0.0.1:13001
└── frps :7000
↓ FRP 隧道
Mac Mini frpc
└── 127.0.0.1:3001 → CloudCLIlaunchd 持久化)
```
## 3. 配置详情
### 3.1 DNS 记录(NameSilo
| 主机记录 | 类型 | 值 | TTL |
|----------|------|-----|-----|
| claude | A | 43.133.235.218 | 3600 |
### 3.2 frpc 配置(Mac Mini
配置文件路径:`~/.local/etc/frpc.toml`
```toml
[[proxies]]
name = "cloudcli"
type = "tcp"
localIP = "127.0.0.1"
localPort = 3001
remotePort = 13001
```
重启方式:
```bash
launchctl unload ~/Library/LaunchAgents/com.sanguo.frpc.plist
launchctl load ~/Library/LaunchAgents/com.sanguo.frpc.plist
```
### 3.3 VPS Caddy 配置
配置文件路径:`/etc/caddy/Caddyfile`
```caddyfile
claude.mysanguo.top {
reverse_proxy 127.0.0.1:13001
}
```
重载:`sudo systemctl reload caddy`
### 3.4 CloudCLI 持久化(Mac Mini
- LaunchAgent`~/Library/LaunchAgents/com.cloudcli.server.plist`
- 命令:`cloudcli start --port 3001`
- 日志:`~/.cloudcli/server.log`
- 绑定地址:`0.0.0.0:3001`
## 4. ⚠️ 关键教训:腾讯云防火墙不需要开后端端口
### 问题
配置完成后,误以为需要在腾讯云轻量服务器防火墙中开放 13001/TCP 端口。
### 根因
混淆了"VPS 对外暴露端口"和"Caddy 内部反代端口"的概念。
### 正确理解
```
外网 → :443Caddy HTTPS)→ 127.0.0.1:13001VPS 内部回环)→ FRP 隧道 → Mac Mini :3001
这段是 VPS 本地回环通信
不经过腾讯云防火墙
```
### 规则
- 腾讯云防火墙只需要开放 **80/443**Caddy 对外 HTTPS 入口)
- Caddy 到后端服务的反代走 `127.0.0.1`(本地回环),**不需要在腾讯云防火墙开放后端端口**
- ufw 同理,但 ufw 默认不拦截 lo(loopback)接口,所以 `ufw allow 13001` 虽然无害但也无必要
- 只有当服务直接对外暴露(不经过 Caddy 反代)时,才需要在防火墙开放对应端口
### 判断方法
如果 Caddy 配置中写的是 `reverse_proxy 127.0.0.1:XXXX`,则 XXXX 端口不需要对外开放。
## 5. 当前外网服务清单
| 子域名 | VPS Caddy 反代 | FRP 隧道远端端口 | 内网目标 | 服务 |
|--------|---------------|-----------------|---------|------|
| oc.mysanguo.top | → 127.0.0.1:18789 | 18789 | 127.0.0.1:18789 | OpenClaw |
| git.mysanguo.top | → 127.0.0.1:13000 | 13000 | 192.168.2.154:3000 | Gitea |
| claude.mysanguo.top | → 127.0.0.1:13001 | 13001 | 127.0.0.1:3001 | CloudCLI |
## 6. 添加新外网服务的标准流程
1. NameSilo 添加 DNS A 记录(仅此步需要手动操作)
2. Mac Mini frpc.toml 新增隧道段
3. VPS Caddyfile 新增 `reverse_proxy`
4. reload caddy + 重启 frpc
5. 验证 `curl -I https://新域名`
!!! warning "注意"
**不需要在腾讯云防火墙或 ufw 开放后端端口**(除非绕过 Caddy 直接暴露)。
## 7. 安全说明
- CloudCLI 自带账号密码认证(session + cookie),作为唯一安全层
- 未配置 Caddy Basic Auth(避免双重密码体验差)
- 如需增强安全,可在 Caddy 层加 Basic Auth 或 IP 白名单
+1
View File
@@ -0,0 +1 @@
# algorithms package
+16
View File
@@ -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]
+18
View File
@@ -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
+41
View File
@@ -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 == hipivot 是当前区间最大值
if store == hi:
return arr[store]
# 否则在 store+1..hi 中继续找(右侧都 >= pivot
return _quickselect_max(arr, store + 1, hi)
+186 -14
View File
@@ -23,7 +23,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple
import httpx import httpx
from fastapi import APIRouter, Header, Request, Response from fastapi import APIRouter, Header, Request, Response
from src.blackboard.db import init_db from src.blackboard.db import init_db, get_connection
from src.blackboard.models import Task from src.blackboard.models import Task
from src.blackboard.operations import Blackboard from src.blackboard.operations import Blackboard
from src.config.agents import AGENT_IDS from src.config.agents import AGENT_IDS
@@ -215,11 +215,56 @@ def _toolchain_db_path() -> Path:
db = root / TOOLCHAIN_PROJECT_ID / "blackboard.db" db = root / TOOLCHAIN_PROJECT_ID / "blackboard.db"
db.parent.mkdir(parents=True, exist_ok=True) db.parent.mkdir(parents=True, exist_ok=True)
init_db(db) init_db(db)
_init_task_state_table(db)
return db return db
def _init_task_state_table(db: Path) -> None:
"""§22.7: 创建 task_state 表(如不存在)"""
conn = get_connection(db)
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS 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"
")"
)
conn.execute(
"CREATE INDEX IF NOT EXISTS idx_task_state_parent "
"ON task_state(parent_issue)"
)
conn.commit()
except Exception:
logger.debug("task_state table init skipped (may already exist)", exc_info=True)
finally:
conn.close()
def _ensure_task_state(db: Path, issue_number: int, repo: str,
parent_issue: int) -> None:
"""§22.7: 记录 sub Issue 的 parent_issue 映射到 task_state 表"""
conn = get_connection(db)
try:
conn.execute(
"INSERT OR IGNORE INTO task_state (issue_number, repo, parent_issue, status, created_at) "
"VALUES (?, ?, ?, 'pending', datetime('now'))",
(issue_number, repo, parent_issue),
)
conn.commit()
finally:
conn.close()
def _send_toolchain_task( def _send_toolchain_task(
to_agent: str, to_agent: str | None,
title: str, title: str,
description: str, description: str,
event_type: str, event_type: str,
@@ -231,7 +276,7 @@ def _send_toolchain_task(
"""创建 Toolchain Task 并写入 _toolchain DB。 """创建 Toolchain Task 并写入 _toolchain DB。
Args: Args:
to_agent: 收件人 Agent ID to_agent: 收件人 Agent ID,None 表示无指派(待路由/delegate)
title: 任务标题 title: 任务标题
description: 任务描述(模板渲染后的事件信息) description: 任务描述(模板渲染后的事件信息)
event_type: 事件类型(review_result / ci_failure / ... event_type: 事件类型(review_result / ci_failure / ...
@@ -243,7 +288,7 @@ def _send_toolchain_task(
Returns: Returns:
创建的 Task ID 创建的 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) logger.warning("Unknown agent: %s, skipping toolchain task", to_agent)
return "" return ""
@@ -650,9 +695,10 @@ async def _handle_pull_request_review(payload: Dict[str, Any]) -> None:
else: # REQUEST_CHANGES else: # REQUEST_CHANGES
tc_steps = [ tc_steps = [
"按审查意见逐条修改代码", "按审查意见逐条修改代码",
"文档同步:如审查涉及设计/接口变更,同步更新 docs/design/ 对应文档",
"push 到原分支 → CI 自动跑", "push 到原分支 → CI 自动跑",
"CI 通过后等重新 Review", "CI 通过后等重新 Review",
"提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report", "提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report— 报告中必须说明文档是否需要更新及处理结果",
] ]
_send_toolchain_task( _send_toolchain_task(
to_agent=pr_author, to_agent=pr_author,
@@ -1024,6 +1070,47 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
}, },
) )
else: 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 模板获取 stepsfallback 到硬编码)
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 通过后创建 PRGitea API: POST /repos/{repo}/pullshead: fix/{issue_number}-{brief}, base: main)— PR body 必须含 Closes #{issue_number}",
"等 Review",
"提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report)— 报告中必须说明文档是否需要更新及处理结果",
]
title = f"Issue 指派: {issue_title} ({repo}#{issue_number})" title = f"Issue 指派: {issue_title} ({repo}#{issue_number})"
_send_toolchain_task( _send_toolchain_task(
to_agent=assignee, to_agent=assignee,
@@ -1031,14 +1118,7 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
description=text, description=text,
event_type="issue_assigned", event_type="issue_assigned",
action_type="issue_assigned", action_type="issue_assigned",
steps=[ 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",
f"git add -A && git commit -m \"[moz] fix: {issue_title[:30]}\" && git push origin fix/{issue_number}-{brief}",
f"CI 通过后创建 PRGitea API: POST /repos/{repo}/pullshead: fix/{issue_number}-{brief}, base: main",
"等 Review",
"提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report",
],
context_data={ context_data={
"issue_number": issue_number, "issue_number": issue_number,
"repo": repo, "repo": repo,
@@ -1046,10 +1126,101 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
"labels": labels, "labels": labels,
"issue_body": issue_body or "(无描述)", "issue_body": issue_body or "(无描述)",
"brief": brief, "brief": brief,
"business_type": business_type,
}, },
) )
# §22.7: 解析 [parent #{N}] 写入 task_statetoolchain DB
parent_match = re.search(r'\[parent\s* #(\d+)\]', issue_title, re.IGNORECASE)
if parent_match:
parent_issue_num = int(parent_match.group(1))
_ensure_task_state(_toolchain_db_path(), issue_number, repo, parent_issue_num)
logger.info("Issue #%s: parent_issue=%d recorded in task_state",
issue_number, parent_issue_num)
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": 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 + 非部署失败 → 检查 flow/* 和 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
)
has_flow_direct = any(
lbl.lower() == "flow/direct" for lbl in labels_list_opened
)
# §22.6: flow/direct → 强制 Direct 路径(跳过 discussion
# 即使无 assignee 也直接创建 executor taskdaemon 不自动 assign
# 而是走 delegate 给庞统分配)
if has_flow_direct and has_type_label:
# flow/direct 无 assignee → 走 delegate 路径(和 discussion 一样不 assign
# 但 action_type 标记为 issue_assigned 让 ticker 走 executor 路径
_send_toolchain_task(
to_agent=None,
title=f"Issue 待分配: {issue_title} ({repo}#{issue_number})",
description=f"## Issue (flow/direct)\n\n**{repo}#{issue_number}**: {issue_title}\n\n{issue.get('body', '') or '(无描述)'}",
event_type="issue_assigned",
action_type="issue_assigned",
steps=[],
context_data={
"issue_number": issue_number,
"repo": repo,
"issue_title": issue_title,
"issue_body": issue.get("body", "") or "",
},
)
logger.info("Issue #%s: flow/direct → executor task (no assignee)", issue_number)
elif 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: if "部署失败" in issue_title:
# 从 Issue body 提取 commit hashGitea deploy workflow 格式) # 从 Issue body 提取 commit hashGitea deploy workflow 格式)
sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", "")) sha_match = re.search(r'[0-9a-f]{40}', issue.get("body", ""))
@@ -1153,7 +1324,8 @@ async def _handle_issue_comment(payload: Dict[str, Any]) -> None:
steps=[ steps=[
"查看完整 CI 日志(PR 页面或 Gitea Actions 页面)", "查看完整 CI 日志(PR 页面或 Gitea Actions 页面)",
"根据 CI 日志判断失败原因类型:\n a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑\n b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见下方「需要创建 Issue 时」),label 必须包含 type/infrastructure", "根据 CI 日志判断失败原因类型:\n a. 代码问题(lint/test 失败)→ 修复失败的测试 → push 到原分支 → CI 自动重跑\n b. 基础设施问题(runner 环境/Python/venv/Gitea/网络故障)→ 在该仓库创建 Issue 指派 jiangwei-infra(见下方「需要创建 Issue 时」),label 必须包含 type/infrastructure",
"提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report)— 报告中说明判断的原因类型和执行的操作", "文档同步:如修复涉及设计/接口变更,同步更新 docs/design/ 对应文档",
"提交 action reportPOST http://localhost:8083/api/projects/_toolchain/tasks/<task_id>/commentscomment_type=action_report)— 报告中说明判断的原因类型和执行的操作,以及文档是否需要更新",
], ],
context_data={ context_data={
"pr_number": issue_number, "pr_number": issue_number,
+1
View File
@@ -152,6 +152,7 @@ class BootstrapBuilder:
constraints.extend([ constraints.extend([
"- 审查结果必须明确 pass/fail", "- 审查结果必须明确 pass/fail",
"- 评审意见须附证据(文件:行号)", "- 评审意见须附证据(文件:行号)",
"- 需求-设计-编码一致性:PR 改动是否和 Issue/设计文档描述一致?如改了实现但 docs/design 未同步更新,在 Review 中指出",
]) ])
elif role == "planner": elif role == "planner":
constraints.extend([ constraints.extend([
+5 -4
View File
@@ -186,10 +186,11 @@ class DeliveryChecklistSection:
priority: int = 55 # CONSTRAINTS(50) 和 EXTENSION(60) 之间 priority: int = 55 # CONSTRAINTS(50) 和 EXTENSION(60) 之间
CHECKLIST_TEXT = ( CHECKLIST_TEXT = (
"## 交付检查\n" "## 交付检查(强制)\n"
"完成代码改动前确认\n" "⚠️ 这是必须执行的步骤,不是提醒。代码改动完成后,以下检查每一项都要有明确结论\n"
"- 改了实现 → docs/design/ 对应设计文档是否需要更新\n" "- 改了实现 → docs/design/ 对应设计文档是否需要更新?需要则在同一 PR 中更新\n"
"- 改了实现 → tests/ 是否有对应测试脚本需要更新\n" "- 改了实现 → tests/ 是否有对应测试脚本需要更新?需要则在同一 PR 中更新\n"
"- action report 中必须逐项说明上述检查结果(如「文档无需更新」「测试已补充」)\n"
"- 所有成果物变更通过 PR 流程:PR review 把关设计合理性,CI 把关代码质量,CD 把关部署正确性\n" "- 所有成果物变更通过 PR 流程:PR review 把关设计合理性,CI 把关代码质量,CD 把关部署正确性\n"
) )
+70 -28
View File
@@ -103,9 +103,9 @@ SPAWN_PROMPT_TEMPLATE = """{identity_section}
""" """
DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一个 v2.9 四相循环的讨论环节 DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与 Gitea Issue 讨论
## 你的任务 ## 讨论主题
{goal_snapshot} {goal_snapshot}
@@ -113,35 +113,62 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与黑板讨论。这是一
{constraints} {constraints}
## 黑板 API ## 你是谁
你可以随时: {agent_identity}
- 读黑板:GET http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}?expand=all( commentsoutputs)
- 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 读完需求后 Gitea Issue comment 回应必须不是可选
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 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. **你是自主的**黑板思考行动,不要等指令 1. **你是自主的** Issue思考行动不要等指令
2. **不重复别人的工作**动手前先读黑板看谁在做什么(Separation) 2. **不重复别人的工作** 动手前先读 Issue comment 看谁在做什么(Separation)
3. **保持方向对齐**你的产出方向和 parent goal 对齐,不确定时 @pangtong-fujunshi(Alignment) 3. **保持方向对齐** 你的产出方向和 parent goal 对齐不确定时 @pangtong-fujunshi(Alignment)
4. **产出可共享**产出写入黑板,让其他人能看到你的成果(Cohesion) 4. **产出可共享** 产出写入 Gitea Issue/PR让其他人能看到你的成果(Cohesion)
5. **不越界**安全红线不要碰,超出能力的 @ 庞统升级(Boundary) 5. **不越界** 安全红线不要碰超出能力的 @ 庞统升级(Boundary)
6. **随时讨论**执行过程中需要协作时 @ 对应 Agent,讨论是灵活的不是固定阶段的 6. **随时讨论** 执行过程中需要协作时 @ 对应 Agent讨论是灵活的不是固定阶段的
## 讨论完成后 ## 讨论完成后
- 如果讨论收敛到可执行的任务,直接创建 sub task - 如果讨论收敛到可执行的任务直接创建 sub Issueassign 自己
- 如果有分歧或不确定,在黑板上写 comment @ 庞统裁决 - 如果有分歧或不确定 Issue comment @pangtong-fujunshi 裁决
- 标记完成: - **降级机制**如果你认为这个任务足够简单只涉及一个角色可以在 comment 中建议
```bash `@pangtong-fujunshi 建议直接指派 @agent-id理由...`
curl -X POST http://{api_host}:{api_port}/api/projects/{project_id}/tasks/{task_id}/status \ 庞统判断后会创建 sub Issue 直接 assign 给该 agent
-H 'Content-Type: application/json' \ - 标记完成 parent Issue comment 写总结
-d '{{"status": "done", "agent": "{agent_id}"}}'
```
""" """
@@ -383,18 +410,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, def _build_discussion_prompt(self, task_id: str, title: str,
description: str, must_haves: str, description: str, must_haves: str,
project_id: str, agent_id: str) -> str: project_id: str, agent_id: str) -> str:
"""构建讨论类 spawn prompt(§3.3 框架 + Boids)""" """构建讨论类 spawn prompt(§22.2 Gitea API 版本)"""
goal_snapshot = description or title 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( return DISCUSSION_PROMPT_TEMPLATE.format(
goal_snapshot=goal_snapshot, goal_snapshot=goal_snapshot,
constraints=constraints, constraints=constraints,
project_id=project_id, project_id=project_id,
task_id=task_id, task_id=task_id,
agent_id=agent_id, agent_id=agent_id,
api_host=self.api_host, agent_identity=agent_identity,
api_port=self.api_port, repo=repo,
issue_number=issue_number,
) )
def _inject_agent_identity(self, agent_id: str) -> str: def _inject_agent_identity(self, agent_id: str) -> str:
+76 -5
View File
@@ -28,6 +28,15 @@ from src.blackboard.queries import Queries
from src.blackboard.registry import ProjectRegistry from src.blackboard.registry import ProjectRegistry
def _toolchain_db_path_safe() -> Optional[Path]:
"""获取 toolchain DB 路径(安全模式,不存在返回 None)"""
try:
from src.api.toolchain_routes import _toolchain_db_path
return _toolchain_db_path()
except Exception:
return None
@dataclass @dataclass
class BroadcastRound: class BroadcastRound:
"""追踪单个任务的广播状态""" """追踪单个任务的广播状态"""
@@ -417,8 +426,29 @@ class Ticker:
finally: finally:
conn.close() conn.close()
for row in parent_rows: # §22.7: 同时扫描 task_state 中的 parent_issueGitea 路径)
parent_id = row["parent_task"] task_state_parents = []
tc_db = _toolchain_db_path_safe()
if tc_db:
tc_conn = get_connection(tc_db)
try:
tc_rows = tc_conn.execute(
"SELECT DISTINCT parent_issue FROM task_state WHERE parent_issue IS NOT NULL"
).fetchall()
task_state_parents = [str(r["parent_issue"]) for r in tc_rows]
except Exception:
logger.debug("task_state scan skipped", exc_info=True)
finally:
tc_conn.close()
# 合并两个来源的 parent IDs
parent_ids = [row["parent_task"] for row in parent_rows]
# task_state 中的 parent_issue 是 Gitea Issue number(整数),转成字符串以统一处理
for ts_parent in task_state_parents:
if ts_parent not in parent_ids:
parent_ids.append(ts_parent)
for parent_id in parent_ids:
try: try:
summary = bb.get_subtasks_summary(parent_id) summary = bb.get_subtasks_summary(parent_id)
if not summary or not summary["all_terminal"]: if not summary or not summary["all_terminal"]:
@@ -513,7 +543,7 @@ class Ticker:
### 三问 ### 三问
1. Goal 还清晰吗是否有 goal drift 1. Goal 还清晰吗是否有 goal drift
2. 成果物覆盖 goal 了吗逐条检查验收标准 2. 成果物覆盖 goal 了吗逐条检查验收标准 + 确认 docs/design 是否同步更新
3. 下一轮需要做什么创建新 sub tasks / 标记完成 / 调整方向 3. 下一轮需要做什么创建新 sub tasks / 标记完成 / 调整方向
### 失败处理 ### 失败处理
@@ -1161,10 +1191,43 @@ Parent Task ID: {parent_task.id}
if t.id not in self._broadcast_tracker: if t.id not in self._broadcast_tracker:
self._broadcast_tracker[t.id] = BroadcastRound(task_id=t.id) 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 = [] spawned = []
for agent_id in idle_agents: for agent_id in idle_agents:
prompt = self._build_claim_prompt( prompts_to_send = []
agent_id, broadcastable, project_id)
# 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: try:
session_id = await self.spawner.spawn_full_agent( session_id = await self.spawner.spawn_full_agent(
agent_id=agent_id, agent_id=agent_id,
@@ -1187,6 +1250,14 @@ Parent Task ID: {parent_task.id}
return spawned 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, def _build_claim_prompt(self, agent_id: str, tasks: list,
project_id: str) -> str: project_id: str) -> str:
"""#03: 广播认领 prompt(身份+专长注入)""" """#03: 广播认领 prompt(身份+专长注入)"""
+20 -1
View File
@@ -31,9 +31,11 @@ _ACTION_HINTS: Dict[str, str] = {
"review_result": "你收到一个 Review 结果通知,这是一个需要你执行动作的事件(不是纯通知)。", "review_result": "你收到一个 Review 结果通知,这是一个需要你执行动作的事件(不是纯通知)。",
"review_request": "你收到一个 Review 请求,这是一个需要你审查并提交 Review 的事件。", "review_request": "你收到一个 Review 请求,这是一个需要你审查并提交 Review 的事件。",
"review_updated": "你收到一个 PR 更新通知,这是一个需要你重新审查修改部分的事件。", "review_updated": "你收到一个 PR 更新通知,这是一个需要你重新审查修改部分的事件。",
"review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。", "review_comment": "你收到一个 Review 评论,这是一个需要你查看并响应的事件。回复时 @评论者。",
"ci_failure": "你收到一个 CI 失败通知,这是一个需要你修复失败测试的事件。", "ci_failure": "你收到一个 CI 失败通知,这是一个需要你修复失败测试的事件。",
"issue_assigned": "你收到一个 Issue 指派,这是一个需要你编码实现的事件。", "issue_assigned": "你收到一个 Issue 指派,这是一个需要你编码实现的事件。",
"issue_closed": "你收到一个 Issue 关闭通知。这是一条纯通知,阅读即可。",
"issue_discussion": "你收到一个需要讨论的 Issue。请阅读 Issue 内容,发起讨论并引导其他 agent 参与。",
"deploy_failure": "你收到一个部署失败通知,这是一个需要你排查并修复的事件。", "deploy_failure": "你收到一个部署失败通知,这是一个需要你排查并修复的事件。",
"mention": "你收到一个 @mention 通知,这是一个需要你按指引响应的事件。", "mention": "你收到一个 @mention 通知,这是一个需要你按指引响应的事件。",
"review_merged": "你收到一个 PR 合并通知。这是一条纯通知,阅读即可。", "review_merged": "你收到一个 PR 合并通知。这是一条纯通知,阅读即可。",
@@ -247,6 +249,12 @@ class ToolchainConstraintsSection:
'- 如果遇到问题需要其他角色支持,在关联的 PR/Issue 上创建 comment @对方', '- 如果遇到问题需要其他角色支持,在关联的 PR/Issue 上创建 comment @对方',
'- 不要使用 Mail API(飞鸽传书)发送消息', '- 不要使用 Mail API(飞鸽传书)发送消息',
'- 你的所有操作都在 toolchain 流程内,通过 Gitea 留痕', '- 你的所有操作都在 toolchain 流程内,通过 Gitea 留痕',
'- ⚠️ 在 Issue/PR 上写 comment 时,如果内容需要 Issue 的 assignee 或创建者知晓,必须在 comment 中 @对方。纯确认性回复(如"收到")不需要 @。',
"",
"### 6. 文档同步(涉及代码改动时)",
'- 改了实现 → 检查 docs/design/ 对应设计文档是否需要更新',
'- 改了实现 → 检查 tests/ 是否有对应测试脚本需要更新',
'- action report 中必须说明文档是否需要更新及处理结果(如「文档无需更新」)',
"", "",
"### Red Flags(如果脑海中出现以下想法,说明你错了)", "### Red Flags(如果脑海中出现以下想法,说明你错了)",
"", "",
@@ -259,6 +267,7 @@ class ToolchainConstraintsSection:
'| “步骤太多了,选几个做就行” | ❌ 错!必须逐条执行,不可跳过 |', '| “步骤太多了,选几个做就行” | ❌ 错!必须逐条执行,不可跳过 |',
'| “这个步骤不适用于当前情况” | ❌ 如果确实不适用,在 action report 中说明原因,但其他步骤必须执行 |', '| “这个步骤不适用于当前情况” | ❌ 如果确实不适用,在 action report 中说明原因,但其他步骤必须执行 |',
'| “CI/部署失败不是我代码的问题,我什么也不用做” | ❌ 错!即使是基础设施问题,你也必须创建 Issue 指派 jiangwei-infrabody 含错误来源链接 + 日志 + 判断依据),并在 action report 中说明。不能只报告“不是我的问题”就完事 |', '| “CI/部署失败不是我代码的问题,我什么也不用做” | ❌ 错!即使是基础设施问题,你也必须创建 Issue 指派 jiangwei-infrabody 含错误来源链接 + 日志 + 判断依据),并在 action report 中说明。不能只报告“不是我的问题”就完事 |',
'| “文档以后再说” | ❌ 错!文档同步和代码改动在同一 PR 中完成,action report 中必须说明文档处理情况 |',
"", "",
] ]
return "\n".join(lines) return "\n".join(lines)
@@ -325,6 +334,16 @@ class ToolchainHandler(BaseTaskHandler):
return VerifyResult(True, "merged_passthrough", return VerifyResult(True, "merged_passthrough",
"review_merged auto-pass") "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 # 1. 优先检查 action_report comment
report_row = conn.execute( report_row = conn.execute(
"SELECT id FROM comments WHERE task_id=? " "SELECT id FROM comments WHERE task_id=? "
+63 -1
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict, List, Optional
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -87,3 +87,65 @@ def render_template(name: str, variables: Dict[str, str]) -> str:
def clear_cache() -> None: def clear_cache() -> None:
"""清空模板缓存(用于测试或热更新)""" """清空模板缓存(用于测试或热更新)"""
_template_cache.clear() _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 ""
+7 -2
View File
@@ -6,8 +6,14 @@
需要 RUN_INTEGRATION=1 + 生产 daemon 运行 需要 RUN_INTEGRATION=1 + 生产 daemon 运行
""" """
import json
import os 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 re
import sqlite3 import sqlite3
import sys import sys
@@ -17,7 +23,6 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
import pytest
import requests as http_requests import requests as http_requests
# 指向部署目录 # 指向部署目录
+7 -2
View File
@@ -4,15 +4,20 @@
需要 RUN_INTEGRATION=1 + 生产 daemon 运行 需要 RUN_INTEGRATION=1 + 生产 daemon 运行
""" """
import json
import os 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 sys
import time import time
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List from typing import Any, Dict, List
import pytest
import requests as http_requests import requests as http_requests
# 指向部署目录 # 指向部署目录
+4 -7
View File
@@ -1,11 +1,10 @@
import pytest import pytest
import os
skip_no_integration = pytest.mark.skipif( if not os.environ.get("RUN_INTEGRATION"):
not __import__("os").environ.get("RUN_INTEGRATION"), pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
reason="Set RUN_INTEGRATION=1 to run E2E tests against real daemon",
)
pytestmark = [pytest.mark.e2e, skip_no_integration] pytestmark = pytest.mark.e2e
"""v2.7 端到端测试 — 全链路真实环境 """v2.7 端到端测试 — 全链路真实环境
@@ -14,7 +13,6 @@ pytestmark = [pytest.mark.e2e, skip_no_integration]
import asyncio import asyncio
import json import json
import os
import sys import sys
import time import time
import uuid import uuid
@@ -22,7 +20,6 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
+4 -7
View File
@@ -1,12 +1,11 @@
import pytest 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 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 端到端测试 — 新增场景覆盖
覆盖 v3.1 新增功能 覆盖 v3.1 新增功能
@@ -22,7 +21,6 @@ skip_no_integration = pytest.mark.skipif(
""" """
import json import json
import os
import sqlite3 import sqlite3
import sys import sys
import time import time
@@ -31,7 +29,6 @@ from datetime import datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import pytest
import requests as http_requests import requests as http_requests
# ── 路径设置 ── # ── 路径设置 ──
+5 -7
View File
@@ -1,10 +1,11 @@
import os import os
import sys
import pytest import pytest
pytestmark = [pytest.mark.e2e, pytest.mark.skipif( if not os.environ.get("RUN_INTEGRATION"):
not os.environ.get("RUN_INTEGRATION"), pytest.skip("E2E tests require RUN_INTEGRATION=1", allow_module_level=True)
reason="Set RUN_INTEGRATION=1 to run E2E tests",
)] pytestmark = [pytest.mark.e2e]
"""#01 四相循环 单元测试 """#01 四相循环 单元测试
@@ -23,13 +24,10 @@ pytestmark = [pytest.mark.e2e, pytest.mark.skipif(
import asyncio import asyncio
import json import json
import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
import pytest
# ── 路径设置 ── # ── 路径设置 ──
DEPLOY_DIR = Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2" DEPLOY_DIR = Path.home() / ".sanguo_projects" / "sanguo_moziplus_v2"
SRC_DIR = DEPLOY_DIR / "src" SRC_DIR = DEPLOY_DIR / "src"
+7 -2
View File
@@ -6,14 +6,19 @@
覆盖项目管理 Task CRUD SubTask Stage 进度 父状态聚合 依赖链 超时 Mail 覆盖项目管理 Task CRUD SubTask Stage 进度 父状态聚合 依赖链 超时 Mail
""" """
import json
import os 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 sys
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import Any, Dict from typing import Any, Dict
import pytest
from unittest.mock import MagicMock from unittest.mock import MagicMock
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
+28
View File
@@ -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
+63
View File
@@ -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"