11 KiB
Gitea 多 Agent 协作协议
本协议是可选增强。启用 Gitea 协作时,任务规格留在 Git,实时协调放在 Issue / PR;未启用时继续使用
tasks/README.md的本地流程。
工件与权威来源
| 工件 | 保存什么 | 不保存什么 |
|---|---|---|
docs/tasks/T-<编号>.md |
任务规格、依赖、允许写路径、验收标准、可审计执行证据 | Token、实例地址、临时聊天 |
| Gitea Issue | 实时状态、领取者、阻塞、结构化 claim / 续租记录 | 需求正文的唯一副本 |
claims/T-<编号> 分支 |
防御性领取标记;分支存在表示 dispatcher 已分配任务 | 工作提交、跨 dispatcher 的线性化锁 |
agent/<agent-id>/T-<编号> 分支 |
单个 agent 的任务提交 | 其他任务的顺手修改 |
| Pull Request | 评审、验证证据、合并决策 | 未进入 Git 的隐含上下文 |
同一个任务只能映射一个任务文件和一个主 Issue。Issue 标题、工作分支和 PR 标题都以 [T-<编号>] 开头;Issue 正文保存任务文件路径,PR 同时链接任务文件和 Issue。
状态与标签
推荐标签:
- 类型:
kind/task标识可执行任务,并从type/docs、type/code中选择一个主要变更类型。 - 状态:
status/todo、status/doing、status/blocked、status/review、status/done。 - 优先级:
priority/p0、priority/p1、priority/p2。
status/*、type/*、priority/* 分别使用 Gitea exclusive scoped labels,同一分组任一时刻最多一个;kind/task 为普通标签。
状态映射:
| 阶段 | 任务文件 | Issue | 分支 / PR |
|---|---|---|---|
| 待领取 | TODO |
open + status/todo |
无 claim 分支 |
| 开发中 | 工作分支上 DOING |
open + status/doing |
claim 与工作分支存在 |
| 阻塞 | BLOCKED |
open + status/blocked |
默认保留 claim,避免误领 |
| 评审中 | 已写完整证据 | open + status/review |
PR open |
| 已完成 | 合入默认分支的 DONE |
closed + status/done |
PR merged;claim 可清理 |
Issue 是实时状态权威;默认分支尚未合入工作提交时,其任务文件仍可能显示 TODO,这不是冲突。合并后,任务文件成为长期审计事实。
任务进入可领取队列
- 先创建任务文件,写清规格、依赖和初始
write_paths,合入默认分支;此时issue、context_ref、claim / 工作分支均为null。 - 用 Issue 模板创建唯一主 Issue。新 Issue 只有
kind/task,尚未带status/todo。 - 把 Issue 编号回填任务文件,并让 Issue 链接该文件;映射提交合入默认分支后,再选择
type/*、priority/*和status/todo。
因此 dispatcher 能从默认分支可靠定位任务 ↔ Issue;没有双向映射或没有 status/todo 的任务都不可领取。
串行分配与防重复领取
仅修改 assignee / status/doing 再读回不是原子操作:两个 agent 可能先后覆盖并各自读到成功。MVP 的互斥保证来自单一 dispatcher(主 agent 或维护者)串行执行分配;worker 不并发自选任务。claim 分支用于识别已分配任务并拦截顺序重试 / 常见旁路,不把 Gitea 的普通 create-branch API 当作线性化锁:
- 读取默认分支任务文件和对应 Issue,确认双向映射、依赖均为
DONE、Issue 为status/todo,且目标 worker 没有其他活跃任务。 - 读取默认分支头提交 SHA,记为
context_ref。串行检查所有活跃预留的write_paths,不得与本任务重叠。 - 从精确的
context_ref创建claims/T-<编号>防御性标记。若已存在、返回非成功或状态不确定就停止并人工核查;并发冲突在不同版本中可能表现为409或5xx,不得自动无限重试,也不得仅因分支 SHA 相同就判定本次分配成功。 - 创建
agent/<agent-id>/T-<编号>工作分支,更新 Issue 为status/doing,按项目规则设置 assignee,并追加结构化 claim 评论。 - dispatcher 读回 Issue 和两个分支;不一致时先修复协调状态,不把任务交给 worker。
- worker 在独立 worktree 读回分配结果,再把工作分支任务文件更新为
DOING,写入context_ref、claim / 工作分支和已接受的write_paths;提交只触碰允许路径。
不同任务的“扫描路径后分别创建各自 claim”本身不具备原子性,因此不得让多个 worker 并发执行步骤 1~5。若团队不使用单一 dispatcher,路径检查只能视为乐观预检,任务必须事先由维护者分配互不重叠的范围,不能宣称有强互斥。
结构化 claim 评论至少包含:
CLAIM
task: T-123
claimed_by: 【agent-id】
allocated_by: 【dispatcher 的 Gitea 登录名】
context_ref: 【40 位提交 SHA】
claim_branch: claims/T-123
work_branch: agent/【agent-id】/T-123
write_paths:
- docs/tasks/T-123.md
- 【其他仓库相对路径】
claimed_at: 【RFC 3339 时间】
lease_until: 【RFC 3339 时间】
断线重连时,只有 Issue 最新有效 claim 的 claimed_by、工作分支和当前 agent 全部一致,才可把已有 claim 当作自己的恢复现场;仅比较 SHA 不足以证明所有权。
写路径防撞
- 默认分支任务文件定义初始
write_paths;领取后,由配置的 dispatcher Gitea 身份发布、且allocated_by与评论作者一致的最新完整 CLAIM / CLAIM RENEWAL,与工作分支任务文件共同定义活跃预留。二者不一致时暂停工作。 write_paths必须列出任务文件本身及预期修改的文件或目录;共享配置、锁文件、导航文件也要列入。- 两条路径相同,或一条是另一条的目录前缀,视为重叠;活跃任务不得存在重叠路径。
- 发现必须修改范围外文件时,先停止并在 Issue 提议扩展范围;dispatcher 串行复查其他活跃预留,接受后追加包含全部字段和新路径的
CLAIM RENEWAL,worker 同步更新工作分支任务文件,二者都完成后才能继续。审计始终以最后一个完整 CLAIM 块为准。 - 活跃任务由 Issue 的
status/doing、status/blocked、status/review判定。每个 agent 同时最多一个活跃任务;项目可以并行多个写路径互不重叠的任务。
建议 worktree 命令:
git fetch origin
git worktree add ../【项目】-T-123 -b agent/【agent-id】/T-123 origin/agent/【agent-id】/T-123
不要让多个 agent 共用同一 worktree,也不要在 claim 分支提交工作代码。
PR 与完成
- 在任务文件
## 执行记录写入实际验证命令和结果,完成标准满足后更新状态。 - PR 使用
.gitea/PULL_REQUEST_TEMPLATE.md,链接Closes #【Issue 编号】、任务文件、context_ref、写路径和验证证据。 - 创建 PR 后把 Issue 切到唯一
status/review;评审失败则把 Issue 和工作分支任务状态一起回到 doing / blocked。 - PR 合并、默认分支任务文件为
DONE后,Issue 才切到status/done并关闭。 - MCP 当前没有删除分支工具。清理 claim 前先确认 PR 已合并、Issue 已完成且无恢复需要,再由维护者通过 Gitea UI 或受控 REST 操作删除。
过期 claim 与断连
- worker 应在
lease_until前请求续租;dispatcher 串行复查后,由自己的 Gitea 身份发布包含全部字段的CLAIM RENEWAL。单次租期最长 24 小时,续租不得更换task、claimed_by、allocated_by、context_ref、claim / 工作分支;审计以最后一个由配置 dispatcher 发布的有效块为准。 - claim 过期不等于可以自动抢占。维护者先检查 Issue 最后活动、工作分支新提交和 PR,再评论回收原因并人工删除 claim 分支。
- Gitea / MCP 断连时,只能继续已经确认归属自己的任务;不能领取新任务、释放锁或猜测远端状态。
只读检测命令:
python scripts/audit_gitea_coordination.py --repo 【owner/repo】 --dispatcher 【Gitea登录名】
从默认分支的 clean checkout 运行审计。--dispatcher(或非敏感环境变量 GITEA_DISPATCHER_LOGIN)指定唯一可信的 dispatcher Gitea 登录名;审计只接受该账号发布且 allocated_by 一致的 CLAIM。它会核对标签、任务依赖、任务 ↔ Issue、claim / 工作分支、PR、活跃写路径和 lease_until,不会写远端。发现过期 claim 后不自动删除:维护者先查 Issue 最后活动、分支新提交和 PR,再评论回收原因,确认无人继续工作后才通过 UI 或受控 REST 删除。
初始化标签
先预览,再显式写入:
python scripts/setup_gitea_labels.py --repo 【owner/repo】
python scripts/setup_gitea_labels.py --repo 【owner/repo】 --apply
脚本从环境变量读取 GITEA_URL、GITEA_TOKEN;HTTP 仍要求 GITEA_ALLOW_INSECURE_HTTP=1。默认命令会连接目标仓库做只读比较,显示 create / update / unchanged;--apply 会把同名标签的颜色、描述和 exclusive 属性校正为本模板值。MCP 没有创建标签工具,因此标签初始化使用 Gitea REST API 或由维护者在 UI 中完成。
并发验收
可用 python scripts/test_gitea_claim_race.py --repo 【owner/repo】 --apply 在唯一 claims/__probe__/race-* 临时分支做兼容性 smoke,期望恰好一个 201、一个 409。脚本只在名称前缀和 SHA 都符合预期时清理并复查 404。一次 smoke 结果不能证明 create-branch 线性化;无论结果如何,MVP 仍依赖 dispatcher 串行分配。不得在业务任务分支上试验。
自动化与升级阈值
python scripts/validate_harness_governance.py完全离线检查上下文清单、导航、本地链接、任务 frontmatter / 依赖 / 写路径、Gitea 模板和已跟踪文本中的敏感值。.gitea/workflows/harness-governance.yml在 push / PR 运行标准库测试和离线检查,不注入本机长期 PAT,也不运行远端审计。平台仍会提供 job token,工作流用permissions: read-all和persist-credentials: false收窄权限与留存。- Actions 模板只有合入默认分支、仓库启用 Actions 且带 Python 3.10+ 的
ubuntu-latestrunner 可用时才会真正执行;内网 runner 还要能取得actions/checkout@v4。没有 runner 时,以相同本地命令作为验收证据,不宣称 CI 已跑绿。 - 退出码统一:
0通过,1发现一致性问题,2配置、网络或运行前提缺失。敏感信息检查只输出规则、文件和行号,不回显命中正文。
MVP 不实现 webhook、协调服务或独立 dashboard。只有出现以下任一信号才重新评估:单项目约 20 个以上并发任务、dispatcher 成为持续瓶颈、跨仓库聚合成为刚需、重复出现路径分配竞态,或审计 / 合规要求集中查询。届时优先增加原子 allocation 服务和 webhook 索引,再评估只读 dashboard;不把前端看板当作并发控制器。