Files
sanguo_moziplus_v2/docs/design/20-issue-centric-orchestration.md
T
cfdaily b0f4572ba6
CI / lint (pull_request) Successful in 30s
CI / test (pull_request) Successful in 8m30s
CI / frontend (pull_request) Successful in 19s
CI / notify-on-failure (pull_request) Successful in 0s
[moz] docs(§20): v2.0 修订——Repository 模式换底 + Review 反馈整合
庞统修正:
- 砍掉 task_index + TaskAdapter,改为 Repository 模式(Queries 类内部换底)
- 协作数据从 Gitea API 读,执行数据从 task_state 表读

司马懿 Review S1-S4 + G2:
- S1: 表数量 12→14 修正
- S2: P4 补充 action_report 识别方案(body 标记约定)
- S3: PR merge 关 Issue 需要 Closes #N commit message 约定
- S4: CI status webhook 验证放到 Phase 0
- G2: Mail 剩余职责明确

姜维 Review:
- task_state 加 issue_updated_at 缓存失效判断
- must_haves 改名 daemon_meta
2026-06-20 00:40:04 +08:00

381 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面"
created: 2026-06-19
version: v2.0 draft
status: draft
changelog: v2.0 纳入姜维+司马懿 Review 反馈 + 庞统 Repository 模式修正
v1.1 纳入姜维 Review 反馈
v1.0 初版
---
# Issue-Centric Orchestration
> **作者**: 庞统(副军师)🐦
> **日期**: 2026-06-19
> **定位**: 将黑板 DB 的协作面迁移到 Gitea Issuedaemon 逻辑保持不变
> **前置文档**: PRD-v3.0(共享意识空间)、§14 TaskTypeRegistry、§17 ToolchainHandler
---
## §1. 设计目标
| # | 目标 | 说明 |
|---|------|------|
| 1 | 黑板 DB 协作面迁移到 Gitea Issue | 需求、讨论、产出从黑板 DB 迁到 Issue |
| 2 | 成果物以 Gitea 为基础存放 | 分支 commit + PR |
| 3 | webhook 部分替代 ticker | 主动触发替代轮询,ticker 保留兜底 |
| 4 | task 状态 + spawner 逻辑不变 | daemon 内部状态机不变 |
| 5 | prompt 改造 | 黑板 API 引用改为 Gitea API,告知 agent 使用 Gitea 协作 |
**核心原则**: 只有数据存储位置变了(黑板 DB → Gitea Issue),daemon 的调度逻辑(dispatcher/ticker/spawner)不变。
---
## §2. 现状分析
### 2.1 黑板 DB 当前承担的角色
根据 PRD-v3.0,黑板是**共享意识空间**——所有 agent 通过它读写状态、感知变化、协调工作。
黑板 DB 包含 14 张表:
| 表 | 用途 | 分类 |
|---|------|------|
| tasks | 任务(标题、描述、状态、指派、retry) | 协作面 + 执行面 |
| comments | 讨论、@mention、action_report | 协作面 |
| outputs | 产出物(文本摘要、文件路径) | 协作面 |
| events | 事件流(SSE 推送) | 协作面 |
| reviews | 审查记录(verdict、round、consensus | 执行面 |
| checkpoints | 阶段审查(approve/reject | 执行面 |
| decisions | 决策记录 | 执行面 |
| observations | 风险观察 | 执行面 |
| experiences | 经验沉淀 | 执行面 |
| routing_decisions | 路由记录 | 执行面 |
| task_attempts | 重试历史 | 执行面 |
| mention_queue | @mention 队列 | 执行面 |
| experience_tags | 经验标签 | 执行面(§19 已标记废弃) |
| agents | Agent 注册信息 | 执行面 |
**协作面**tasks/comments/outputs/events= 迁移到 Gitea Issue
**执行面**reviews/checkpoints/decisions 等)= 保留在 daemon 内部
### 2.2 daemon 数据访问方式
当前 daemon 三个核心模块如何读写黑板 DB:
| 模块 | 读什么 | 怎么读 |
|------|-------|-------|
| ticker | pending task 列表 | `SELECT * FROM tasks WHERE status='pending'` (SQLite) |
| dispatcher | task 详情(title/description/must_haves | `Task.from_row(row)` 从 SQLite 行构建 |
| spawner | task 上下文构建 prompt | 从 task 对象的 title/description/must_haves 字段 |
| 模块 | 写什么 | 怎么写 |
|------|-------|-------|
| dispatcher | task statuspending→claimed→working | `UPDATE tasks SET status=?` |
| spawner | task statusworking→done/failed | `UPDATE tasks SET status=?` |
| handler | comment / output | `INSERT INTO comments/outputs` |
**关键发现**: daemon 大量依赖 `SELECT * FROM tasks WHERE status=?` 这种 SQL 查询来发现和调度 task。如果数据源迁到 Gitea Issue,这些查询的方式会变(从 SQLite 变为 Gitea API 或本地索引),但**查询的语义和返回的数据结构不变**。
---
## §3. 目标架构
### 3.1 分层
```
Gitea(协作介质,替代黑板 DB 协作面)
┌──────────────────────────────────────────┐
│ Issue #42: "实现功能 A" │
│ body: 需求描述 + 验收标准 │
│ assignee: zhangfei-dev │
│ labels: type/feat, priority/P2 │
│ comments: 讨论、@mention、进展汇报 │
│ │
│ 分支: fix/42-feature-a │
│ PR #43: fix/42 → main │
│ CI: lint + test │
│ Review: APPROVE / REQUEST_CHANGES │
│ → merge → Issue auto-close │
└──────────────────────────────────────────┘
↕ webhook(被动) ↕ API(主动)
┌──────────────────────────────────────────┐
│ daemon(执行引擎,内部状态管理不变) │
│ │
│ task_state(轻量索引,替代 tasks 表): │
│ issue_number → status, branch, retry │
│ │
│ 执行面表(不变): │
│ reviews, checkpoints, decisions, │
│ observations, experiences, │
│ routing_decisions, task_attempts │
│ │
│ 调度逻辑(不变): │
│ ticker → 扫 task_state → dispatch │
│ dispatcher → 读 Gitea Issue → spawn │
│ spawner → 读 Gitea Issue → prompt │
└──────────────────────────────────────────┘
```
### 3.2 数据映射
| 黑板 DB | Gitea 对应 | 迁移方式 |
|---------|-----------|---------|
| tasks.title | Issue.title | 直接对应 |
| tasks.description | Issue.body | 直接对应 |
| tasks.assignee | Issue.assignee | 直接对应 |
| tasks.status (pending/working/review/done) | daemon task_state 内部维护 | Issue open/closed 只表示生命周期 |
| tasks.priority | Issue label (priority/P0-P3) | label 模拟 |
| tasks.must_haves (JSON) | daemon task_state 内部存储 | daemon 专用元数据 |
| tasks.depends_on | Issue blocked_by | Gitea 原生 dependency |
| tasks.parent_task | Issue body 引用(如 `Parent: #42` | 约定 |
| tasks.retry_count / dispatch_count | daemon task_state 内部维护 | 执行面数据 |
| comments | Issue comment | 直接对应 |
| outputs | 分支 commit(代码/文档)+ Issue comment(摘要) | 成果物载体迁移 |
| events | webhook | 主动推送替代 SSE |
### 3.3 数据访问层改造:Repository 模式换底
**设计原则**:不新建 task_state + TaskAdapter,而是改造现有 RepositoryQueries/Blackboard 类)的实现。
当前数据访问层已有 Repository 模式的基础——`Blackboard` 类和 `Queries` 类封装了所有数据访问,上层(dispatcher/spawner/ticker)通过方法调用(`queries.pending_dispatchable()``blackboard.get_task()`),不直接写 SQL。
改造做法:**Repository 接口不变,实现从 SQLite-only 改为 Gitea + SQLite。**
```
现在:
dispatcher → Queries(SQLite) → 黑板 DB
改造后:
dispatcher → Queries(Gitea-backed) → Gitea API(协作数据)+ SQLite(执行状态)
```
上层代码完全不用改。Queries 内部决定数据从哪来:
| 数据类型 | 来源 | 方式 |
|---------|------|------|
| title / description / assignee / labels | Gitea Issue | API 读取(webhook 触发时缓存) |
| comment / 讨论 | Gitea Issue comment | API 读取 |
| status / retry_count / dispatch_count | 本地 SQLite | 原有逻辑不变 |
| daemon_meta(原 must_haves | 本地 SQLite | 原有逻辑不变 |
**本地 SQLite 表(执行状态,Gitea 没有的)**
```sql
CREATE TABLE task_state (
issue_number INTEGER, -- Gitea Issue 编号
repo TEXT, -- 仓库名
status TEXT DEFAULT 'pending', -- daemon 内部状态机
retry_count INTEGER DEFAULT 0,
dispatch_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 2,
daemon_meta TEXT, -- JSONevent_type, steps 等 daemon 元数据)
issue_body_cache TEXT, -- 缓存的 Issue body(优化用,可失效重拉)
issue_updated_at TEXT, -- Gitea Issue 的 updated_at(缓存失效判断)
created_at TEXT,
updated_at TEXT,
PRIMARY KEY (issue_number, repo)
);
```
**和 task_state + TaskAdapter 的区别**:不新增表名(task_state 替代 task_state)、不新增 adapter 类。Repository 内部把 Gitea 数据 + 本地执行状态合并成 Task 对象返回。上层调用 `queries.get_task(issue_number)` 得到的 Task 对象和现在一模一样——有 title、有 description、有 status。
---
## §4. 流程设计
### 4.1 创建 Task
```
① 庞统/主公在 Gitea 创建 Issue + 指派 agent
② Gitea webhook: issues/assigned
③ daemon toolchain handler 收到 webhook
→ 在 task_state 插入一行(issue_number, repo, assignee, status=pending
④ ticker 扫 task_state 发现 pending → dispatch → spawn agent
```
**和现在的区别**: 当前是庞统在黑板 API 创建 task。改造后是庞统在 Gitea 创建 Issuewebhook 自动触发 daemon 建索引。
### 4.2 执行 Task
```
① dispatcher 扫 task_state 发现 pending task
② spawner 从 Gitea API 读 Issue body(需求描述)
③ spawner 用 Issue body 构建 prompt(替代从黑板 DB 读 description
→ prompt 结构: Issue body(需求)+ PromptSection 注入(工作流程、约束、API 指引)
④ agent 收到 prompt → 执行
⑤ agent 在 Gitea Issue comment 汇报进展(替代黑板 comment)
⑥ daemon 更新 task_state status=working
```
### 4.3 审查
```
① agent 编码完成 → push 到分支 → 创建 PR
② Gitea webhook: pull_request/opened
→ daemon 更新 task_state status=review
③ Reviewer 在 Gitea 做 PR Review
④ Gitea webhook: pull_request_review
→ daemon 根据 Review 结果更新 task_state
⑤ Review 通过 → PR merge
→ Gitea 自动关闭 Issue
→ Gitea webhook: issues/closed
→ daemon 更新 task_state status=done
```
**commit message 约定**(司马懿 S3):Gitea PR merge 自动关闭 Issue 需要 commit message 包含 `Closes #N``Fixes #N` 关键词。agent 创建 PR 时在描述中加上此约定,确保 merge 后 Issue 自动关闭。
**审查统一走 PR Review**——不区分设计审查和代码审查,所有成果物都在分支上,Reviewer 一次性审。
### 4.4 CI 失败处理
```
① PR 创建 → CI 自动跑
② CI 失败 → Gitea webhook: pull_requestCI status
→ daemon toolchain handler 创建 ci_failure toolchain task
→ 指派给 PR 作者
→ agent 按 ci_failure steps 处理(已有逻辑,不变)
③ agent 修复 → push 到同分支 → PR 自动更新 → CI 重跑
```
### 4.5 ticker 兜底
webhook 可能丢失或延迟。ticker 保留原有逻辑,改为:
-`task_state` 中 status=pending 的记录(替代扫黑板 tasks 表)
-`task_state` 中 status=working 但超时的记录
- 如果发现 Gitea Issue 已 closed 但 task_state 还是 working → 更新为 done
---
## §5. daemon 需要改的地方
**原则: daemon 调度逻辑不变,只改数据访问层。**
### 5.1 数据访问层改造
| 模块 | 现在 | 改造后 | 影响范围 |
|------|------|-------|---------|
| `queries.pending_dispatchable()` | `SELECT * FROM tasks WHERE status='pending'` | `SELECT * FROM task_state WHERE status='pending'` | SQL 改表名 |
| `Task.from_row(row)` 构建 task 对象 | 从 tasks 表行直接取 title/description | 从 task_state 取 issue_number → 调 Gitea API 读 Issue title/body | 需要新增 Gitea API 调用 |
| `UPDATE tasks SET status=?` | 直接更新 SQLite | 更新 task_stateSQLite | SQL 改表名 |
| `INSERT INTO comments` | 写黑板 DB | 改为 Gitea Issue comment API | 需要新增 Gitea API 调用 |
### 5.2 ⚠️ 需要讨论的改造点
以下是因为数据源变了,daemon 实现需要调整的地方:
**P1: spawner 每次 spawn 都要调 Gitea API 读 Issue body**
当前 spawner 从 SQLite 读 task description(微秒级)。改为从 Gitea API 读(毫秒级,HTTP 请求)。
- **方案 A**: 每次 spawn 时实时调 Gitea API。简单但慢
- **方案 B**: webhook 触发时缓存 Issue body 到 `task_state.issue_body_cache` + `issue_updated_at`。spawn 时从缓存读
- **缓存失效**: spawner 构建 prompt 时比对 `issue_updated_at` 和 Gitea API 的 Issue `updated_at`,不匹配才重新拉取(正常情况 Issue body 创建后不改,开销可忽略)
- **推荐**: 方案 B + updated_at 校验
**P2: dispatcher 的 `Task.from_row(row)` 返回的对象缺少字段**
当前 Task 对象有 30+ 个字段(title/description/priority/risk_level 等)。task_state 只存少量字段。dispatcher/dispatch 逻辑如果访问了 Task 对象的 title/description 字段,会取不到值。
- **方案 Alazy load**: Task 对象新增 `issue_data` 字段,首次访问 title/description 时从 Gitea API 或缓存加载
- **方案 BTaskAdapter,推荐 ✅)**: 新增 `TaskAdapter` 类,封装 task_state + Gitea Issue 数据的合并访问。调用方代码不用改,只改 adapter 内部实现。比 lazy load 字段更干净——合并逻辑集中在一处,而不是散落在 Task 对象的多个 property 中
- **影响**: 需要检查 dispatcher 中所有访问 task.title/task.description 的地方
**P3: agent 的 prompt 中引用黑板 API 的地方需要改**
PromptSection 中有多处 `POST localhost:8083/api/projects/.../tasks/.../comments`(黑板 API)。这些要改为 Gitea API
- `task_handler.py` TaskApiSection: `POST .../status``POST .../outputs` → 不需要(daemon 通过 webhook 自动感知状态)
- `toolchain_handler.py` ToolchainApiSection: `POST .../comments`action report)→ 改为 `POST Gitea API .../issues/.../comments`
- `toolchain_handler.py` ToolchainApiSection: `POST .../outputs` → 改为"push 到分支"指引
**P4: comments 表的 @mention 机制**
当前 @mention 通过黑板 `mention_queue` 表排队。改造后 @mention 通过 Gitea Issue/PR commentwebhook 自然触发)。但 mention_queue 的消费逻辑(ticker 扫描 → 通知 → agent 处理)需要适配。
- **方案**: mention_queue 保留,但数据来源从黑板 comment 改为 Gitea webhook payload
- **⚠️ 适配层**: Gitea webhook payload 中的 comment body 是完整 markdown 文本(不像黑板 comment 是结构化 JSON,有 comment_type、author 等字段)。mention_queue 消费侧需要适配:从 webhook payload 的 `comment.body` 中正则提取 @mention,而非 SQL WHERE 精确查询。这层适配在 Phase 4 mention 迁移时细化
- **影响**: ticker 中的 mention 处理逻辑需要适配
- **action_report 识别**(司马懿 S2):黑板 comment 有结构化 `comment_type=action_report` 字段,Gitea comment 只有 markdown body。迁移后用 **body 中的固定标记**识别,如 `<!-- action_report -->` 或约定 body 以 `[Action Report]` 开头。具体格式在 Phase 4 实施时确定。
---
## §6. prompt 改造
### 6.1 受影响的 PromptSection
| Section | 文件 | 当前内容 | 改造后 |
|---------|------|---------|-------|
| TaskApiSection | task_handler.py | 黑板 APIstatus 回写、outputs 提交) | 删除 status 回写(daemon 自动管);outputs 改为 git push |
| ToolchainApiSection | toolchain_handler.py | 黑板 APIaction_report comment、outputs | action_report 改为 Issue commentoutputs 改为 git push |
| TaskConstraintsSection | task_handler.py | "blackboard comment" 引用 | 改为 "Issue/PR comment" |
| ToolchainConstraintsSection | toolchain_handler.py | 已禁止 Mail | 同时告知 agent 使用 Gitea 协作 |
### 6.2 agent prompt 新增指引
agent 需要知道工作方式变了。新增一个通用 section(或加入现有 constraints section):
```
## 协作方式
- 你的任务通过 Gitea Issue 管理
- 需求描述在 Issue body 中
- 进展汇报通过 Issue comment
- 代码产出通过分支 commit + PR
- 审查通过 PR Review
- 不要使用黑板 API,不要使用 Mail API
```
---
## §7. Issue ↔ 分支 ↔ PR 关系
| 场景 | Issue : 分支 : PR |
|------|------------------|
| 简单任务(bugfix、小功能) | 1 : 1 : 1 |
| 复杂任务(多阶段) | 1 : 1 : N(分阶段提交 PR,同一个分支) |
| 极复杂任务(需拆解) | 1 : N : N(Issue body 列出子任务,每个一个分支+PR) |
**默认 1:1:1**。分支命名规范不变:`fix/{issue_number}-{brief}`
---
## §8. 不做的事
| 不做 | 理由 |
|------|------|
| 不做数据迁移 | 主公确认当前无正式使用数据 |
| 不做 Issue 状态 labelstatus/xxx | 中间状态 daemon 内部管,Issue 只有 open/closed |
| 不改 Mail | Mail 职责不变。Issue-centric 模式下 agent 不用 Mail(通过 Issue/PR comment 协作)。Mail 剩余职责:非 Gitea 相关的 agent 间点对点通知(如庞统通知赵云准备数据) |
| 不改前端(本阶段) | 前端改造独立于后端,后续设计 |
| 不做存量 task 退役 | 原 task 流程和 Issue 流程可共存,原 task 自然退役 |
| 不改 experiences/checkpoints/decisions 表 | 执行面表保留在 daemon,不受影响 |
---
## §9. 实施路径
| 阶段 | 内容 | 依赖 |
|------|------|------|
| **Phase 0** | **前置:webhook 权限** — jiangwei-infra 升级为 repo admin,或主公手动配置 webhook | 设计 Review 通过 |
| Phase 0 | webhook 权限配置 + CI status webhook 验证(主公手动配 webhook;验证 Gitea CI status 变化是否触发 webhook | 设计 Review 通过 |
| Phase 1 | task_state 表创建 + Repository 改造(Queries 类内部从 Gitea + SQLite 双源读) | Phase 0 |Issue assigned → 建索引)+ **验证 Gitea CI status webhook 是否触发** | Phase 0 |
| Phase 2 | dispatcher/ticker 数据源从 tasks 表切换到 task_state + TaskAdapter 合并访问层 | Phase 1 |
| Phase 3 | spawner 读 Issue body 构建 prompt(替代读黑板 description+ issue_updated_at 缓存失效机制 | Phase 2 |
| Phase 4 | prompt 改造(黑板 API → Gitea API+ mention_queue 适配层(webhook payload → @mention 提取) | Phase 3 |
| Phase 5 | 验证 + 清理废弃的黑板协作面表 | Phase 4 |
每个 Phase 独立可验证,出问题可以回退。
---
## §10. 风险评估
| 风险 | 等级 | 缓解 |
|------|------|------|
| Gitea API 不可用时 daemon 完全瘫痪 | 中 | webhook 触发时缓存 Issue body + issue_updated_at 校验(P1 方案 B),减少运行时 Gitea API 依赖 |
| Gitea webhook 丢失 | 低 | ticker 兜底扫描 |
| task_state 和 Gitea Issue 状态不一致 | 中 | ticker 定期校验(发现 Issue closed 但 index 未更新则修复) |
| spawner 性能下降(Gitea API 调用) | 低 | 方案 B 缓存 Issue bodyspawn 时不调 Gitea API |
| 原 task 流程和新 Issue 流程共存期混乱 | 中 | 可以限定只在特定项目中启用 Issue 模式,逐步切换 |
| Gitea CI status 变化不触发 webhook | 中 | Phase 1 首先验证;如果不触发,ticker 兜底轮询 commit status API |
| webhook 管理权限不足 | 中 | Phase 0 前置解决:升级 jiangwei-infra 为 repo admin 或主公手动配置 |