feat(coordination): add atomic Gitea task workflow (phase 2)

This commit is contained in:
chengma
2026-07-14 12:23:09 +08:00
parent 94ff8b7e06
commit 0a09deacee
18 changed files with 559 additions and 20 deletions
+36
View File
@@ -0,0 +1,36 @@
---
name: Agent task
about: 创建与 docs/tasks/T-<编号>.md 一一对应的开发任务
title: "[T-XXX] "
ref: ""
labels:
- kind/task
---
## 任务映射
- task_id: `T-XXX`
- task_file: `docs/tasks/T-XXX.md`
- context_ref: `【领取时的默认分支提交 SHA】`
- deps: `【T-编号列表或无】`
- write_paths:
- `docs/tasks/T-XXX.md`
- `【允许修改的仓库相对路径】`
## 问题与方案
【链接任务文件对应章节;Issue 只写协调所需摘要,不复制整份规格。】
## 验收入口
【真实验证命令和可观察结果;长期证据回填到任务文件。】
## 协作状态
- expected_claim_branch: `claims/T-XXX`
- work_branch: `【领取后填写】`
- claimed_by: `【领取后填写非敏感 agent-id】`
- lease_until: `【领取后填写 RFC 3339 时间】`
领取必须遵循 `docs/gitea-collaboration.md` 的原子 claim 流程。不要在本 Issue 粘贴 Token、Authorization header 或私有配置。
创建后先把 Issue 编号回填任务文件并合入默认分支,再按主要变更选择唯一 `type/docs` 或 `type/code`、一个 `priority/*` 和 `status/todo`。映射提交完成前不可领取。
+33
View File
@@ -0,0 +1,33 @@
## 任务映射
- Closes #【Issue 编号】
- task_file: `docs/tasks/T-XXX.md`
- context_ref: `【领取任务时的提交 SHA】`
- claim_branch: `claims/T-XXX`
- work_branch: `agent/【agent-id】/T-XXX`
- write_paths:
- `docs/tasks/T-XXX.md`
- `【本 PR 允许修改的仓库相对路径】`
## 变更摘要
【改了什么,以及为什么符合任务方案。】
## 验证证据
| 命令 | 结果 |
| --- | --- |
| `【真实命令】` | 【通过 / 失败摘要】 |
## 风险与回滚
【已知风险、兼容性影响、回滚方法;没有则写“无”。】
## 检查清单
- [ ] 当前 PR 只对应一个任务 / Issue。
- [ ] 变更未超出 `write_paths`,没有夹带无关修改。
- [ ] 任务文件执行记录包含相同的验证证据。
- [ ] 合并前任务文件 frontmatter 已为 `DONE`;`Closes` 自动关闭 Issue 不会制造假完成。
- [ ] 未提交 Token、Authorization header、私有配置或实例地址。
- [ ] Issue 已切换到唯一 `status/review`;合并后才标记 `status/done`。
+2
View File
@@ -36,6 +36,8 @@ harness coding 需要的项目文档模板主要集中在 `docs/` 目录;根
领取任务时记录默认分支头提交为 `context_ref`;同一会话中文件 SHA 未变化时可复用已读内容,ref 变化后重新读取清单和受影响文档。
模板复制到启用 Gitea 协作的项目后,还应先读 `docs/gitea-collaboration.md`:dispatcher 串行检查写路径和分配任务,每个任务以唯一 claim 分支防重复领取;每个 worker 同时最多一个活跃任务,工作分支 / worktree 独立,活跃任务的 `write_paths` 不得重叠。
如果用户要求修改某类模板,优先阅读对应文件,不要只凭文件名猜内容。
## 工作规则
+4
View File
@@ -33,7 +33,10 @@
| [`docs/agent-context.schema.json`](docs/agent-context.schema.json) | 上下文清单结构契约 |
| [`docs/agent-context.md`](docs/agent-context.md) | 上下文清单的读取、缓存、权威来源和断连降级说明 |
| [`docs/gitea-mcp.md`](docs/gitea-mcp.md) | 可选:Gitea MCP 共享文档与任务协调接入、安全和降级规则 |
| [`docs/gitea-collaboration.md`](docs/gitea-collaboration.md) | 可选:Issue / 任务文件 / PR 映射、原子领取和写路径防撞协议 |
| [`scripts/validate_agent_context.py`](scripts/validate_agent_context.py) | 零第三方依赖校验上下文清单、Schema 和仓库相对路径 |
| [`scripts/setup_gitea_labels.py`](scripts/setup_gitea_labels.py) | 默认只读预览远端差异、显式 `--apply` 的 Gitea 协作标签初始化脚本 |
| [`.gitea/ISSUE_TEMPLATE/task.md`](.gitea/ISSUE_TEMPLATE/task.md) / [`.gitea/PULL_REQUEST_TEMPLATE.md`](.gitea/PULL_REQUEST_TEMPLATE.md) | Gitea 任务 Issue 与 PR 模板 |
| [`docs/method-map.md`](docs/method-map.md) | 失败模式 → 首要修复 → 工件的诊断 / 导航对照表 |
| [`docs/evaluator-rubric.md`](docs/evaluator-rubric.md) | 单次会话输出的结构化评审评分表 |
| [`docs/quality-document.md`](docs/quality-document.md) | 代码库长期健康度追踪(产品域 × 架构层评级) |
@@ -76,6 +79,7 @@
项目进入多轮长期开发后,再按需启用:
- `docs/gitea-mcp.md`:需要跨 agent 读取 Gitea 文档、Issue 和 PR 时启用。
- `docs/gitea-collaboration.md`:需要多 agent 原子领取、独立分支 / worktree 和写路径防撞时启用。
- `docs/clean-state-checklist.md`:每轮结束前检查仓库是否可恢复。
- `docs/method-map.md`:遇到失败模式时定位该补哪个工件。
- `docs/evaluator-rubric.md`:评审单次 agent 输出质量。
+7 -6
View File
@@ -40,12 +40,12 @@
读完上述文档后,每轮会话按这个机械顺序进入,恢复持久状态再动手:
1. `pwd`:确认在正确的仓库根目录。
2. 读 [`current-state.md`](current-state.md) 和 `docs/tasks/` 中 `status: DOING` 的任务文件:恢复已验证状态、下一步和当前 blocker。
2. 读 [`current-state.md`](current-state.md) 和 `docs/tasks/` 中当前 agent 的活跃任务;启用 Gitea 时同时读对应 Issue,恢复已验证状态、claim、下一步和当前 blocker。
3. `git log --oneline -5`:看清最近发生了什么。
4. 运行 `./init.sh`(Windows 原生 PowerShell 用 `./init.ps1`):统一安装、验证、打印启动命令;如果脚本提示命令未替换,先配置脚本顶部三个命令。
5. 跑一条基础 smoke / 端到端路径,确认基线没坏。
6. **如果基线已坏,先修基线**,不要在坏的起点上叠新功能。
7. 基线绿了,再从 `docs/tasks/` 领取唯一任务(路线图见 [`06-tasks.md`](06-tasks.md))。
7. 基线绿了,再从 `docs/tasks/` 为当前 agent 领取唯一任务(路线图见 [`06-tasks.md`](06-tasks.md));启用 Gitea 时由 dispatcher 串行分配并取得原子 claim。
## 当前阶段
@@ -63,16 +63,17 @@
任务以「一任务一文件」存放在 `docs/tasks/`(约定见 [`tasks/README.md`](tasks/README.md)):
- 只领取一个 frontmatter `status: TODO` 且依赖均 `DONE` 的任务文件,取编号最靠前的。
- 每个 agent 只领取一个 frontmatter `status: TODO` 且依赖均 `DONE` 的任务文件,取编号最靠前的;项目可并行多个 `write_paths` 互不重叠的任务。
- 若 `docs/tasks/` 暂无可领任务,先按 [`06-tasks.md`](06-tasks.md) 路线图把下一个建议任务落成任务文件,再领取。
- 开始前把该文件 frontmatter 的 `status` 改为 `DOING`(同一时间最多 1 个)。
- 未启用 Gitea 时,开始前在独立分支 / worktree 把该文件 frontmatter 的 `status` 改为 `DOING`。
- 启用 Gitea 时,由 dispatcher 按 [`gitea-collaboration.md`](gitea-collaboration.md) 串行检查写路径并原子创建 `claims/T-<编号>`;worker 只接受已读回确认的分配。标签、assignee 和更新后读回不能代替同任务领取锁。
- 本轮只完成这一个任务;验收通过后改为 `DONE`。
- **执行记录写进该任务文件的 `## 执行记录`**(改了什么、跑了什么验证、结果、决策)。
- 若项目现实发生变化(启动/验证路径、目录结构、blocker),覆盖更新 [`current-state.md`](current-state.md);任务状态以任务文件 frontmatter 为准,不逐任务改写快照。
- 若项目现实发生变化(启动/验证路径、目录结构、blocker),覆盖更新 [`current-state.md`](current-state.md);未启用 Gitea 时以任务文件 frontmatter 为状态权威,启用后以 Issue 为实时状态,不逐任务改写快照。
- 结束会话前过一遍 [`clean-state-checklist.md`](clean-state-checklist.md),确保下一轮无需人工修复即可开工。
- 做完即停,汇报验证结果,等待下一步指令。
> 本约定单 agent 与多 agent 并发通用;并发时注意 [`tasks/README.md`](tasks/README.md) 的防撞号规则,只改自己领取的任务文件。
> 本约定单 agent 与多 agent 并发通用;并发时遵守 [`tasks/README.md`](tasks/README.md) 的编号和写路径防撞规则,每个 agent 使用独立工作分支与 worktree。
如果代码实际状态和任务文件冲突,先说明冲突,不要擅自跳步或重排。
+2 -2
View File
@@ -1,12 +1,12 @@
# 任务路线图(Roadmap)
> 本文是**只读路线图**:维护阶段划分、里程碑、待办池和建议拆分清单,把 MVP 拆成小步、可独立交付的任务,让 AI 一步一步开发,避免一次生成整个项目。
> 真实任务以「一任务一文件」存放在 [`tasks/`](tasks/README.md)(`docs/tasks/T-<编号>.md`),任务状态以各任务文件 frontmatter 为准;**本文不跟踪单任务状态**。
> 真实任务以「一任务一文件」存放在 [`tasks/`](tasks/README.md)(`docs/tasks/T-<编号>.md`)。未启用 Gitea 时以任务文件 frontmatter 为状态权威;启用后以 Issue 为实时状态、合并后的任务文件为长期事实。**本文不跟踪单任务状态**。
## 使用规则
1. **开工先落文件**:从下方「建议拆分清单」把下一个任务按 [`tasks/README.md`](tasks/README.md) 落成 `docs/tasks/T-<编号>.md`(沿用建议编号),把验收要点展开成可执行、可观察的步骤,再开始实现。
2. **一次只做一个任务**:领取、状态流转、执行记录、完成定义全部遵循 [`tasks/README.md`](tasks/README.md) 和 [编码规则](05-coding-rules.md);`DONE` 需要可运行证据。
2. **每个 agent 一次只做一个任务**:领取、状态流转、执行记录、完成定义全部遵循 [`tasks/README.md`](tasks/README.md) 和 [编码规则](05-coding-rules.md);`DONE` 需要可运行证据,多 agent 只并行写路径互不重叠的任务。
3. **不跳步**:依赖未完成的任务不能开工。
4. **本文只在规划变化时修改**:调整阶段划分、里程碑、增删建议任务或 Backlog 条目时才动本文;单个任务开工或完成**不**修改本文。
5. **动手前**先读 `00-ai-start-here.md`、`05-coding-rules.md` 和 `current-state.md`。
+5 -2
View File
@@ -23,19 +23,22 @@
- [架构设计](04-architecture.md):系统结构、模块职责、数据模型、关键风险和开发顺序。
- [编码规则](05-coding-rules.md):AI 写代码前必须遵守的硬约束。
- [任务路线图](06-tasks.md):阶段划分、里程碑和待办池;只读,不跟踪单任务状态。
- [任务文件(默认)](tasks/README.md):一任务一文件 `docs/tasks/T-<编号>.md`,任务状态以 frontmatter 为准,单/多 agent 通用,agent 每轮只做一个。
- [任务文件(默认)](tasks/README.md):一任务一文件 `docs/tasks/T-<编号>.md`,单/多 agent 通用,每个 agent 同时只做一个;启用 Gitea 后由 Issue 承担实时状态。
- [已有项目接入清单](adoption-checklist.md):把本模板补进已有代码库时的迁移步骤和第一轮任务建议。
- [API 合约](api.md):前后端接口形状、错误格式、鉴权约定。
- [路由与页面结构](routes.md):页面路由、页面职责、组件归属。
- [当前实现状态](current-state.md):可覆盖的当前快照,记录仓库现实状态、可运行命令和下一步可做任务。
- [Agent 上下文清单](agent-context.md) / [`agent-context.json`](agent-context.json) / [`Schema`](agent-context.schema.json):按任务类型选择文档、用提交 / 文件 SHA 避免重复读取。
- [Gitea MCP 接入](gitea-mcp.md):可选的共享文档、Issue / PR 协调、安全配置和断连降级规则。
- [Gitea 多 Agent 协作](gitea-collaboration.md):可选的任务映射、原子 claim、写路径防撞和 PR 状态协议。
- [收尾检查清单](clean-state-checklist.md):会话结束前逐项检查,保证下一轮无需人工修复即可继续。
- [方法对照表](method-map.md):失败模式 → 首要修复 → 工件;出问题先查这里对症补工件。
- [评审评分表](evaluator-rubric.md):单次会话输出的结构化评审(6 维 0-2 分 + 校准说明)。
- [质量文档](quality-document.md):代码库长期健康度追踪,区别于单次输出评审。
- [`../init.sh`](../init.sh) / [`../init.ps1`](../init.ps1):标准启动与验证入口脚本(根目录),统一安装、验证和启动命令。按操作系统二选一:WSL / Git Bash / macOS / Linux 用 `init.sh`,Windows 原生 PowerShell 用 `init.ps1`;换技术栈只改脚本顶部三个命令变量;未替换前脚本会主动失败,避免把示例命令误当真实项目命令。
- [`../scripts/validate_agent_context.py`](../scripts/validate_agent_context.py):零第三方依赖校验上下文清单、Schema 和仓库相对路径。
- [`../scripts/setup_gitea_labels.py`](../scripts/setup_gitea_labels.py):默认只读预览远端差异、显式写入的 Gitea 协作标签初始化脚本。
- [Gitea Issue 模板](../.gitea/ISSUE_TEMPLATE/task.md) / [PR 模板](../.gitea/PULL_REQUEST_TEMPLATE.md):任务映射、写路径和验证证据字段。
## 任务 / 进度 / 当前状态
@@ -49,6 +52,6 @@
## 维护原则
- 需求变化先改文档,再改代码。
- 代码现实变化后同步 `current-state.md`;任务状态改在对应任务文件 frontmatter,执行过程写进该任务文件的 `## 执行记录`。
- 代码现实变化后同步 `current-state.md`;任务长期状态和执行证据写进对应任务文件,启用 Gitea 时实时状态同步到 Issue。
- API、数据模型、路由、技术栈一旦在文档中定稿,代码不得另起一套。
- agent 开始新任务前,必须从 `00-ai-start-here.md` 进入。
+2
View File
@@ -31,6 +31,8 @@
推荐随后补齐:`docs/01-vision.md`、`docs/02-requirements.md`、`docs/03-tech-stack.md`、`docs/04-architecture.md`、`docs/api.md`、`docs/routes.md`、`docs/clean-state-checklist.md`;`progress.md` 可选(历史归档 / 项目级大事记)。
需要 Gitea 多 Agent 协作时,再复制 `docs/gitea-mcp.md`、`docs/gitea-collaboration.md`、`.gitea/` 模板和 `scripts/setup_gitea_labels.py`;先只读预览远端标签差异,再由维护者显式 `--apply`。
## 接入步骤
1. 确认仓库根目录,读取已有 `README`、运行脚本、测试配置和主要入口文件。
+1
View File
@@ -43,6 +43,7 @@
],
"gitea": [
"docs/gitea-mcp.md",
"docs/gitea-collaboration.md",
"docs/tasks/README.md",
"docs/clean-state-checklist.md"
]
+2
View File
@@ -12,5 +12,7 @@
- [ ] [`current-state.md`](current-state.md) 的项目级快照与现实一致(启动/验证路径、目录要点、blocker)。
- [ ] 没有半成品改动处于未记录状态;如有,已写明 `BLOCKED` / `PARTIAL` 和原因。
- [ ] 代码处于可安全恢复的状态(必要时已提交,提交信息清晰)。
- [ ] 本轮实际修改没有超出任务 `write_paths`,也没有与其他活跃任务发生路径重叠。
- [ ] 启用 Gitea 时,Issue 的唯一 `status/*`、claim / 工作分支、PR 和任务状态彼此一致;未完成任务没有误删 claim。
任意一项不满足,就先补到满足,再结束会话。
+5 -5
View File
@@ -34,7 +34,7 @@
## 任务状态
任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准(可用脚本汇总成只读看板),历史执行记录见各任务文件的 `## 执行记录`。本节只写摘要:
未启用 Gitea 时,任务状态以 `docs/tasks/` 各任务文件 frontmatter 的 `status` 为准;启用 Gitea 时,Issue 是实时状态权威,合并到默认分支的任务文件保存长期状态和执行证据。本节只写项目级摘要:
- 已完成:【列出 DONE 任务】。
- 正在进行:【如有,列出 DOING 任务】。
@@ -60,8 +60,8 @@
1. 读仓库级 agent 规则文件(如有)。
2. 读 `docs/00-ai-start-here.md`。
3. 读 `docs/05-coding-rules.md`。
4. 在 `docs/tasks/` 取第一个 frontmatter `status: TODO` 且依赖均 `DONE` 的任务文件;暂无可领任务时,先按 `docs/06-tasks.md` 路线图落成任务文件。
5. 将该任务文件 frontmatter 的 `status` 改为 `DOING`。
4. 在 `docs/tasks/` 找到 frontmatter `status: TODO` 且依赖均 `DONE` 的任务文件;暂无任务时,先按 `docs/06-tasks.md` 路线图落成任务文件。
5. 未启用 Gitea 时,在独立分支 / worktree 把任务改为 `DOING`;启用 Gitea 时,先按 `gitea-collaboration.md` 由 dispatcher 完成原子 claim 和路径分配,worker 读回成功后再开始。
## 维护规则
@@ -69,11 +69,11 @@
- 新增或移动入口文件。
- 初始化框架或模块。
- 任务从 `TODO` 进入 `DOING` 或 `DONE`。
- 新增可运行命令。
- 发现文档和代码现实不一致。
- 阶段、项目级 blocker 或可领取任务摘要发生需要跨会话保留的变化。
同时注意:
- 任务状态变化改在对应任务文件的 frontmatter;每轮执行记录、验证命令、阻塞点和关键决策写进该任务文件的 `## 执行记录`。
- 任务长期状态改在对应任务文件的 frontmatter;启用 Gitea 时实时状态同步到 Issue。每轮执行记录、验证命令、阻塞点和关键决策写进该任务文件的 `## 执行记录`。
- 本文件只保留当前快照,不保留完整历史。
+121
View File
@@ -0,0 +1,121 @@
# Gitea 多 Agent 协作协议
> 本协议是可选增强。启用 Gitea 协作时,任务规格留在 Git,实时协调放在 Issue / PR;未启用时继续使用 [`tasks/README.md`](tasks/README.md) 的本地流程。
## 工件与权威来源
| 工件 | 保存什么 | 不保存什么 |
| --- | --- | --- |
| `docs/tasks/T-<编号>.md` | 任务规格、依赖、允许写路径、验收标准、可审计执行证据 | Token、实例地址、临时聊天 |
| Gitea Issue | 实时状态、领取者、阻塞、结构化 claim / 续租记录 | 需求正文的唯一副本 |
| `claims/T-<编号>` 分支 | 唯一领取锁;分支存在表示任务已被领取 | 工作提交 |
| `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`,这不是冲突。合并后,任务文件成为长期审计事实。
## 任务进入可领取队列
1. 先创建任务文件,写清规格、依赖和初始 `write_paths`,合入默认分支;此时 `issue`、`context_ref`、claim / 工作分支均为 `null`。
2. 用 Issue 模板创建唯一主 Issue。新 Issue 只有 `kind/task`,尚未带 `status/todo`。
3. 把 Issue 编号回填任务文件,并让 Issue 链接该文件;映射提交合入默认分支后,再选择 `type/*`、`priority/*` 和 `status/todo`。
因此 dispatcher 能从默认分支可靠定位任务 ↔ Issue;没有双向映射或没有 `status/todo` 的任务都不可领取。
## 串行分配与原子领取
仅修改 assignee / `status/doing` 再读回不是原子操作:两个 agent 可能先后覆盖并各自读到成功。MVP 指定一个 dispatcher(主 agent 或维护者)串行执行分配;worker 不并发自选任务。dispatcher 再使用 Gitea “同名分支只能创建一次”的约束,防止同一任务因重试或旁路操作被重复领取:
1. 读取默认分支任务文件和对应 Issue,确认双向映射、依赖均为 `DONE`、Issue 为 `status/todo`,且目标 worker 没有其他活跃任务。
2. 读取默认分支头提交 SHA,记为 `context_ref`。串行检查所有活跃预留的 `write_paths`,不得与本任务重叠。
3. 从精确的 `context_ref` 原子创建 `claims/T-<编号>`。创建成功者获得该任务锁;收到 `409` 或“分支已存在”即停止,不得仅因分支 SHA 相同就判定成功。
4. 创建 `agent/<agent-id>/T-<编号>` 工作分支,更新 Issue 为 `status/doing`,按项目规则设置 assignee,并追加结构化 claim 评论。
5. dispatcher 读回 Issue 和两个分支;不一致时先修复协调状态,不把任务交给 worker。
6. worker 在独立 worktree 读回分配结果,再把工作分支任务文件更新为 `DOING`,写入 `context_ref`、claim / 工作分支和已接受的 `write_paths`;提交只触碰允许路径。
不同任务的“扫描路径后分别创建各自 claim”本身不具备原子性,因此不得让多个 worker 并发执行步骤 1~5。若团队不使用单一 dispatcher,路径检查只能视为乐观预检,任务必须事先由维护者分配互不重叠的范围,不能宣称有强互斥。
结构化 claim 评论至少包含:
```text
CLAIM
task: T-123
claimed_by: 【agent-id】
allocated_by: 【dispatcher-id】
context_ref: 【40 位提交 SHA】
claim_branch: claims/T-123
work_branch: agent/【agent-id】/T-123
write_paths: 【仓库相对路径列表】
claimed_at: 【RFC 3339 时间】
lease_until: 【RFC 3339 时间】
```
断线重连时,只有 Issue 最新有效 claim 的 `claimed_by`、工作分支和当前 agent 全部一致,才可把已有 claim 当作自己的恢复现场;仅比较 SHA 不足以证明所有权。
## 写路径防撞
- 默认分支任务文件定义初始 `write_paths`;领取后,最新被 dispatcher 接受的结构化 claim / scope 评论与工作分支任务文件共同定义活跃预留。二者不一致时暂停工作。
- `write_paths` 必须列出任务文件本身及预期修改的文件或目录;共享配置、锁文件、导航文件也要列入。
- 两条路径相同,或一条是另一条的目录前缀,视为重叠;活跃任务不得存在重叠路径。
- 发现必须修改范围外文件时,先停止并在 Issue 提议扩展范围;dispatcher 串行复查其他活跃预留,接受后追加结构化 scope 评论,worker 同步更新工作分支任务文件,二者都完成后才能继续。
- 活跃任务由 Issue 的 `status/doing`、`status/blocked`、`status/review` 判定。每个 agent 同时最多一个活跃任务;项目可以并行多个写路径互不重叠的任务。
建议 worktree 命令:
```powershell
git fetch origin
git worktree add ../【项目】-T-123 -b agent/【agent-id】/T-123 origin/agent/【agent-id】/T-123
```
不要让多个 agent 共用同一 worktree,也不要在 claim 分支提交工作代码。
## PR 与完成
1. 在任务文件 `## 执行记录` 写入实际验证命令和结果,完成标准满足后更新状态。
2. PR 使用 `.gitea/PULL_REQUEST_TEMPLATE.md`,链接 `Closes #【Issue 编号】`、任务文件、`context_ref`、写路径和验证证据。
3. 创建 PR 后把 Issue 切到唯一 `status/review`;评审失败则把 Issue 和工作分支任务状态一起回到 doing / blocked。
4. PR 合并、默认分支任务文件为 `DONE` 后,Issue 才切到 `status/done` 并关闭。
5. MCP 当前没有删除分支工具。清理 claim 前先确认 PR 已合并、Issue 已完成且无恢复需要,再由维护者通过 Gitea UI 或受控 REST 操作删除。
## 过期 claim 与断连
- agent 应在 `lease_until` 前用新评论续租;续租不更换 claim 分支。
- claim 过期不等于可以自动抢占。维护者先检查 Issue 最后活动、工作分支新提交和 PR,再评论回收原因并人工删除 claim 分支。
- Gitea / MCP 断连时,只能继续已经确认归属自己的任务;不能领取新任务、释放锁或猜测远端状态。
## 初始化标签
先预览,再显式写入:
```powershell
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 中完成。
## 并发验收
在专用测试任务上让两个进程同时创建同一个临时 claim 分支,验收结果必须是恰好一个 `201`、另一个 `409`。测试前后均需人工确认分支名,清理由受控 REST / UI 完成;不得在业务任务上试验。
+9
View File
@@ -88,3 +88,12 @@ tool_timeout_sec = 60
4. 保存 `read_file` 返回的文件 SHA;同一会话内 SHA 未变化时复用内容。
Gitea 中的文件与本地 `docs/` 是同一 Git 工件的远端与 checkout,不要再创建第三份人工同步副本。
## Issue / PR 协调
多 agent 协作时遵循 [`gitea-collaboration.md`](gitea-collaboration.md):
- 任务文件保存规格和长期证据,Issue 保存实时状态,PR 保存评审与合并决策。
- 领取互斥依赖唯一 `claims/T-<编号>` 分支;assignee、`status/doing` 和读回仅作状态确认。
- MVP 由单一 dispatcher 串行分配任务并检查 `write_paths`,每个 worker 使用 `agent/<agent-id>/T-<编号>` 和独立 worktree;每任务 claim 只解决同任务重复领取,不单独保证跨任务路径互斥。
- MCP 可创建 claim / 工作分支,但当前没有创建标签或删除分支工具。标签用 `python scripts/setup_gitea_labels.py --repo 【owner/repo】 --apply` 幂等初始化;过期 claim 由维护者通过 UI 或受控 REST 人工回收。
+3
View File
@@ -15,7 +15,10 @@
| 评审主观 | 质量判断靠个人记忆和感觉,agent 容易自我说服通过 | 用固定维度做评分 | [`evaluator-rubric.md`](evaluator-rubric.md) |
| 代码库悄悄退化 | 速度上去了,但几轮会话后代码越来越难审、边界越来越糊 | 定期给代码库健康度打分 | [`quality-document.md`](quality-document.md) |
| 文档堆叠失控 | 入口文件越来越长,"每次失败加一句" | 渐进披露,入口保持薄 | [`00-ai-start-here.md`](00-ai-start-here.md) + 拆分到具体文档 |
| 每轮全量重读 | 多 agent 反复拉取全部文档,慢且容易混入无关上下文 | 用任务路由和提交 / 文件 SHA 增量读取 | [`agent-context.md`](agent-context.md) + [`agent-context.json`](agent-context.json) |
| 多 agent 抢改任务文件 | 多个 agent 并发时抢改同一个看板/进度文件,出现"读到旧版本"、ID 撞号、合并冲突 | 一任务一文件(默认模式已内建),执行记录进任务文件,不逐任务改共享收尾文件 | [`tasks/README.md`](tasks/README.md) |
| 多 agent 重复领取 | 两个 agent 同时把同一 Issue 改为 doing,读回后都以为成功 | 用唯一 claim 分支做原子锁,标签只展示状态 | [`gitea-collaboration.md`](gitea-collaboration.md) |
| 多 agent 写路径碰撞 | 不同任务同时修改同一目录或共享配置,合并时才发现冲突 | dispatcher 串行声明 / 比较 `write_paths`,worker 使用独立 worktree | [`gitea-collaboration.md`](gitea-collaboration.md) + [`tasks/README.md`](tasks/README.md) |
## 使用原则
+24 -3
View File
@@ -27,6 +27,13 @@ phase: 1 # 所属阶段,沿用路线图的 Phase 编号
deps: [T-100] # 依赖的任务 ID
status: TODO # TODO | DOING | DONE | BLOCKED
created: 【日期】
issue: null # Gitea Issue 编号;未启用 Gitea 时保持 null
context_ref: null # 领取时默认分支提交 SHA
claim_branch: null
work_branch: null
write_paths: # 允许修改的仓库相对路径
- docs/tasks/T-101.md
- 【path/to/module】
---
## 问题 / 背景
@@ -38,12 +45,25 @@ created: 【日期】
## 领取 / 完成流程
- 状态:`TODO` · `DOING`(同一时间最多 1 个)· `DONE` · `BLOCKED`。
- 一次只领一个 `status: TODO` 且依赖全 `DONE` 的任务,取编号最靠前的;若本目录暂无可领任务,先按路线图把下一个建议任务落成任务文件,再领取。
- 状态:`TODO` · `DOING` · `DONE` · `BLOCKED`。每个 agent 同时最多一个活跃任务;项目可以并行多个写路径互不重叠的任务。
- 每个 agent 一次只领一个 `status: TODO` 且依赖全 `DONE` 的任务,取编号最靠前的;若本目录暂无可领任务,先按路线图把下一个建议任务落成任务文件,再领取。
- `write_paths` 必须在动手前写清。两个活跃任务路径相同,或一条是另一条的目录前缀,均视为冲突,不能并行。
- 做完自测、按「passing 需证据」把验证命令与结果写清、改 `status: DONE`。
- **执行记录写进本任务文件的 `## 执行记录` 一节**(改了什么、跑了什么验证、结果、决策)——不逐任务追加共享的 `progress.md`(可选历史归档)、也不逐任务覆盖 `current-state.md`(项目级快照,只在启动/验证路径、目录结构或 blocker 变化时更新)。
- **只改自己那个任务文件**;不要编辑别人正在做的任务文件。
未启用 Gitea 时,在独立分支 / worktree 中把任务文件从 `TODO` 改为 `DOING` 即可。启用 Gitea 时,必须先按 [`../gitea-collaboration.md`](../gitea-collaboration.md) 原子创建唯一 `claims/T-<编号>` 分支;assignee、标签和“更新后读回”都不能代替领取锁。
## 与 Gitea Issue / PR 的映射(可选)
- 一个任务文件对应一个主 Issue;Issue 负责实时领取、阻塞和评审状态,任务文件负责版本化规格和长期证据。
- 先把任务文件合入默认分支,再创建 Issue;随后把 Issue 编号回填任务文件并合入默认分支,最后才添加 `status/todo`。映射未完成的 Issue 不可领取。
- Issue、claim 分支、工作分支和 PR 都携带同一个 `T-<编号>`;不得用一个 PR 顺带完成多个任务。
- 工作分支命名为 `agent/<agent-id>/T-<编号>`,每个 agent 使用独立 worktree。
- MVP 由一个 dispatcher / 主 agent 串行分配任务。唯一 claim 分支保证同一任务不被重复领取,dispatcher 的串行检查保证不同任务的 `write_paths` 不冲突。
- 进入评审后 Issue 使用唯一 `status/review`;PR 合并且默认分支任务文件为 `DONE` 后,Issue 才能关闭并标记 `status/done`。
- 领取、结构化 claim 评论、过期锁回收和分支清理的完整规则见 [`../gitea-collaboration.md`](../gitea-collaboration.md)。
## 用户指令暗语(可选约定)
> 用户的工作流通常固定为:提 bug/需求 → 讨论定案 → 落成任务文件 → 提交 → 实现 → 提交。
@@ -72,6 +92,7 @@ created: 【日期】
## 与路线图和共享文件的关系
- [`../06-tasks.md`](../06-tasks.md):只读路线图(Phase 划分、里程碑、Backlog、建议拆分清单);真实任务状态一律以本目录任务文件的 frontmatter 为准。
- [`../06-tasks.md`](../06-tasks.md):只读路线图(Phase 划分、里程碑、Backlog、建议拆分清单);未启用 Gitea 时以任务文件 frontmatter 为准,启用后以 Issue 为实时状态、合并后的任务文件为长期事实。
- `../../progress.md`:可选工件,用作历史归档或项目级大事记;执行记录写各任务文件,不逐任务追加。
- [`../current-state.md`](../current-state.md):项目级快照(启动/验证路径、目录要点、blocker);任务状态不在此维护,可由脚本汇总 frontmatter 生成。
- [`../gitea-collaboration.md`](../gitea-collaboration.md):启用 Gitea 时的任务映射、原子领取、写路径防撞和 PR 状态协议。
+11
View File
@@ -5,6 +5,13 @@ phase: 1
deps: []
status: TODO
created: 【日期】
issue: null
context_ref: null
claim_branch: null
work_branch: null
write_paths:
- docs/tasks/T-XXX.md
- 【允许修改的仓库相对路径】
---
## 问题 / 背景
@@ -23,6 +30,10 @@ created: 【日期】
(明确不碰的模块/流程)
## 协作约束
(启用 Gitea 时填写对应 Issue、领取时的 `context_ref`、claim / 工作分支;任何新增写路径先检查与其他活跃任务是否重叠。)
## 执行记录
(做完在此记录:改了哪些文件、跑的验证命令与结果、阻塞、关键决策。
+290
View File
@@ -0,0 +1,290 @@
#!/usr/bin/env python3
"""Preview or idempotently apply Harness Coding labels to one Gitea repo."""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
LABELS: tuple[dict[str, Any], ...] = (
{
"name": "kind/task",
"color": "0052CC",
"description": "Harness Coding task",
"exclusive": False,
},
{
"name": "type/docs",
"color": "5319E7",
"description": "Documentation change",
"exclusive": True,
},
{
"name": "type/code",
"color": "1D76DB",
"description": "Code or automation change",
"exclusive": True,
},
{
"name": "status/todo",
"color": "C5DEF5",
"description": "Ready to claim",
"exclusive": True,
},
{
"name": "status/doing",
"color": "FBCA04",
"description": "Claimed and in progress",
"exclusive": True,
},
{
"name": "status/blocked",
"color": "D93F0B",
"description": "Blocked; claim retained",
"exclusive": True,
},
{
"name": "status/review",
"color": "BFD4F2",
"description": "Pull request under review",
"exclusive": True,
},
{
"name": "status/done",
"color": "0E8A16",
"description": "Merged and completed",
"exclusive": True,
},
{
"name": "priority/p0",
"color": "B60205",
"description": "Highest priority",
"exclusive": True,
},
{
"name": "priority/p1",
"color": "D93F0B",
"description": "High priority",
"exclusive": True,
},
{
"name": "priority/p2",
"color": "FBCA04",
"description": "Normal priority",
"exclusive": True,
},
)
class ApiError(RuntimeError):
def __init__(self, status: int, reason: str) -> None:
super().__init__(f"Gitea API 返回 HTTP {status}:{reason}")
self.status = status
class NoRedirect(urllib.request.HTTPRedirectHandler):
"""Never forward the Authorization header to a redirected origin."""
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
return None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="读取远端差异,并可幂等创建或校正 Harness Coding Gitea 标签。"
)
parser.add_argument(
"--repo",
default=os.environ.get("GITEA_REPOSITORY"),
help="目标 owner/repo;也可设置 GITEA_REPOSITORY。",
)
parser.add_argument(
"--apply",
action="store_true",
help="应用预览中的 create/update;省略时只读远端并打印差异。",
)
return parser.parse_args()
def validate_config(url: str, token: str, repo: str) -> tuple[str, str, str]:
parsed = urllib.parse.urlsplit(url.strip())
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError("GITEA_URL 必须是 http(s) 实例根地址。")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("GITEA_URL 不得包含凭据、query 或 fragment。")
path = parsed.path.rstrip("/")
if path.lower().endswith("/api/v1"):
raise ValueError("GITEA_URL 不得包含 /api/v1。")
if parsed.scheme == "http" and os.environ.get("GITEA_ALLOW_INSECURE_HTTP") != "1":
raise ValueError("HTTP 需要显式设置 GITEA_ALLOW_INSECURE_HTTP=1。")
if not token:
raise ValueError("缺少 GITEA_TOKEN。")
parts = repo.split("/")
if len(parts) != 2 or not all(parts):
raise ValueError("--repo 必须使用 owner/repo 格式。")
root = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
return root.rstrip("/"), parts[0], parts[1]
class GiteaClient:
def __init__(self, root: str, owner: str, repo: str, token: str) -> None:
owner_q = urllib.parse.quote(owner, safe="")
repo_q = urllib.parse.quote(repo, safe="")
self.base = f"{root}/api/v1/repos/{owner_q}/{repo_q}"
self.token = token
proxy_handler = (
urllib.request.ProxyHandler({})
if os.environ.get("GITEA_DIRECT") == "1"
else urllib.request.ProxyHandler()
)
self.opener = urllib.request.build_opener(proxy_handler, NoRedirect())
def request(
self, method: str, path: str, payload: dict[str, Any] | None = None
) -> Any:
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
self.base + path,
data=data,
method=method,
headers={
"Accept": "application/json",
"Authorization": f"token {self.token}",
"Content-Type": "application/json",
},
)
try:
with self.opener.open(request, timeout=30) as response:
body = response.read()
return json.loads(body.decode("utf-8")) if body else None
except urllib.error.HTTPError as exc:
raise ApiError(exc.code, exc.reason) from None
except urllib.error.URLError as exc:
raise RuntimeError(f"连接 Gitea 失败:{exc.reason}") from None
def list_labels(self) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
page = 1
while True:
labels = self.request("GET", f"/labels?limit=50&page={page}")
if not isinstance(labels, list):
raise RuntimeError("Gitea labels 响应格式异常。")
for label in labels:
if isinstance(label, dict) and isinstance(label.get("name"), str):
result[label["name"]] = label
if len(labels) < 50:
return result
page += 1
def normalize_color(value: Any) -> str:
return str(value or "").lstrip("#").upper()
def needs_update(current: dict[str, Any], desired: dict[str, Any]) -> bool:
return (
normalize_color(current.get("color")) != desired["color"]
or str(current.get("description") or "") != desired["description"]
or bool(current.get("exclusive")) != desired["exclusive"]
)
def build_plan(
existing: dict[str, dict[str, Any]],
) -> list[tuple[str, dict[str, Any], dict[str, Any] | None]]:
plan = []
for desired in LABELS:
current = existing.get(desired["name"])
if current is None:
action = "create"
elif needs_update(current, desired):
action = "update"
else:
action = "unchanged"
plan.append((action, desired, current))
return plan
def show_plan(repo: str, plan: list[tuple[str, dict[str, Any], Any]]) -> None:
print(f"目标仓库:{repo}")
for action, desired, _ in plan:
scope = "exclusive" if desired["exclusive"] else "normal"
print(f"- {action:9} {desired['name']} #{desired['color']} {scope}")
counts = {name: sum(action == name for action, _, _ in plan) for name in (
"create",
"update",
"unchanged",
)}
print(
"计划汇总:"
f"创建 {counts['create']},更新 {counts['update']},未变化 {counts['unchanged']}。"
)
def apply_plan(
client: GiteaClient,
plan: list[tuple[str, dict[str, Any], dict[str, Any] | None]],
) -> None:
created = updated = unchanged = 0
for action, desired, current in plan:
if action == "unchanged":
unchanged += 1
continue
if action == "update":
label_id = None if current is None else current.get("id")
if not isinstance(label_id, int):
raise RuntimeError(f"标签 {desired['name']} 缺少数字 id。")
client.request("PATCH", f"/labels/{label_id}", dict(desired))
updated += 1
continue
try:
client.request("POST", "/labels", dict(desired))
created += 1
except ApiError as exc:
if exc.status != 422:
raise
latest = client.list_labels().get(desired["name"])
if latest is None:
raise
if needs_update(latest, desired):
label_id = latest.get("id")
if not isinstance(label_id, int):
raise RuntimeError(f"标签 {desired['name']} 缺少数字 id。")
client.request("PATCH", f"/labels/{label_id}", dict(desired))
updated += 1
else:
unchanged += 1
print(f"标签同步完成:创建 {created},更新 {updated},未变化 {unchanged}。")
def main() -> int:
args = parse_args()
if not args.repo:
print("ERROR: 需要 --repo owner/repo 或 GITEA_REPOSITORY。", file=sys.stderr)
return 2
url = os.environ.get("GITEA_URL", "")
token = os.environ.get("GITEA_TOKEN", "")
try:
root, owner, name = validate_config(url, token, args.repo)
client = GiteaClient(root, owner, name, token)
plan = build_plan(client.list_labels())
show_plan(args.repo, plan)
if not args.apply:
print("dry-run:未写入;追加 --apply 才会应用上述 create/update。")
return 0
apply_plan(client, plan)
return 0
except (ValueError, ApiError, RuntimeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+1 -1
View File
@@ -96,7 +96,7 @@ Get-ChildItem -Recurse -File
| --- | --- | --- | --- | --- |
| H-601 | Phase 0:建立 Gitea MCP 安全与连接基线 | H-409 | 固定 MCP 版本;私有配置不入库;HTTP 需显式确认风险;读写审批和断连降级规则清楚 | DONE |
| H-602 | Phase 1:增加上下文清单与按需读取流程 | H-601 | 有机器可读清单和无第三方依赖验证;agent 按任务类型读取;同一 SHA 不重复加载 | DONE |
| H-603 | Phase 2:建立 Issue / 任务文件 / PR 多 Agent 协调协议 | H-602 | 任务映射、领取读回校验、分支 / worktree 和写路径防撞规则完整 | TODO |
| H-603 | Phase 2:建立 Issue / 任务文件 / PR 多 Agent 协调协议 | H-602 | 任务映射、领取读回校验、分支 / worktree 和写路径防撞规则完整 | DONE |
| H-604 | Phase 3:增加自动化治理与一致性检查 | H-603 | 导航、清单、任务元数据、敏感信息和 Gitea Actions 检查可运行 | TODO |
## Backlog