Files
sanguo_moziplus_v2/docs/design/20-issue-centric-orchestration.md
T
cfdaily 3a11327113
CI / lint (pull_request) Failing after 50s
CI / test (pull_request) Has been skipped
CI / frontend (pull_request) Has been skipped
CI / notify-on-failure (pull_request) Successful in 2s
[moz] docs(§20): v2.2 纳入姜维 v2.0 Review S3(must_haves 改名影响面)
2026-06-20 00:45:31 +08:00

19 KiB
Raw Blame History


title: "Issue-Centric Orchestration — Gitea Issue 替代黑板 DB 协作面" created: 2026-06-19 version: v2.1 draft status: draft changelog: v2.1 修正 M1dispatcher 直接 SQL 声明)+ M2Phase 格式)+ S1/S2TaskAdapter 残留清理) 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 模式换底

设计原则:改造现有 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(执行状态)

上层代码基本不用改。但有一个前置条件:dispatcher.py 中存在约 20 处直接操作 tasks 表的 SQL(绕过 Queries 类,如 SELECT assignee FROM tasksUPDATE tasks SET status=?)。这些直接 SQL 需要先迁移到 Queries 方法调用,才能实现 Repository 换底。此项作为 Phase 1 的前置工作。

Queries 内部决定数据从哪来:

数据类型 来源 方式
title / description / assignee / labels Gitea Issue API 读取(webhook 触发时缓存)
comment / 讨论 Gitea Issue comment API 读取
status / retry_count / dispatch_count 本地 SQLite 原有逻辑不变
daemon_meta(原 must_haves 本地 SQLite 原有逻辑不变

本地 SQLite 表(执行状态,Gitea 没有的)

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)
);

为什么不用 TaskAdapterRepository 内部已经把 Gitea 数据 + 本地执行状态合并成 Task 对象返回。上层调用 queries.get_task(issue_number) 得到的 Task 对象和现在一模一样——有 title、有 description、有 status。不需要额外 adapter 层。


§4. 流程设计

4.1 创建 Task

① 庞统/主公在 Gitea 创建 Issue + 指派 agent
② Gitea webhook: issues/assigned
③ daemon toolchain handler 收到 webhook
   → 在 task_state 插入一行(issue_number, repo, assignee, status=pending
④ ticker 扫 task_state 发现 pending → dispatch → spawn agent

和现在的区别: 当前是庞统在黑板 API 创建 task。改造后是庞统在 Gitea 创建 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 #NFixes #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: Repository 内部数据合并

Queries 类内部改造——get_task() 方法从 Gitea API(或缓存)读 title/body,从 task_state 表读 status/retry_count,合并成 Task 对象返回。上层(dispatcher/spawner)无感知。

这是标准的 Repository 模式——接口不变(get_task(id) 返回 Task 对象),实现换底(从 SQLite 单源改为 Gitea + SQLite 双源)。不新增 TaskAdapter 类——Queries 类本身就是 Repository,内部合并是职责内的事。

P3: agent 的 prompt 中引用黑板 API 的地方需要改

PromptSection 中有多处 POST localhost:8083/api/projects/.../tasks/.../comments(黑板 API)。这些要改为 Gitea API

  • task_handler.py TaskApiSection: POST .../statusPOST .../outputs → 不需要(daemon 通过 webhook 自动感知状态)
  • toolchain_handler.py ToolchainApiSection: POST .../commentsaction 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 : NIssue 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 权限配置(主公手动配)+ CI status webhook 验证(确认 Gitea CI status 变化触发 webhook 设计 Review 通过
Phase 1 dispatcher.py 直接 SQL(约 20 处)迁移到 Queries 方法调用 + task_state 表创建 + Queries/Blackboard 内部改造(Gitea + SQLite 双源)+ must_haves→daemon_meta 重命名(17 文件 80 处,机械替换,需 CI 覆盖) Phase 0
Phase 2 dispatcher/ticker 数据源从 tasks 表切换到 task_state Phase 1
Phase 3 spawner 读 Issue body 构建 prompt(替代读黑板 description+ issue_updated_at 缓存失效机制 Phase 2
Phase 4 prompt 改造(黑板 API → Gitea API+ mention_queue 适配(action_report body 标记约定) Phase 3
Phase 5 验证 + 清理废弃的黑板协作面表 Phase 4

每个 Phase 独立可验证,出问题可以回退。


§10. 风险评估

风险 等级 缓解
Gitea API 不可用时 daemon 完全瘫痪 webhook 触发时缓存 Issue body + issue_updated_at 校验(P1 方案 B),减少运行时 Gitea API 依赖
Gitea webhook 丢失 ticker 兜底扫描
task_state 和 Gitea Issue 状态不一致 ticker 定期校验(发现 Issue closed 但 index 未更新则修复)
spawner 性能下降(Gitea API 调用) 方案 B 缓存 Issue bodyspawn 时不调 Gitea API
原 task 流程和新 Issue 流程共存期混乱 可以限定只在特定项目中启用 Issue 模式,逐步切换