From 0a09deaceecfafdbaa18cd99b83f90670bba46d4 Mon Sep 17 00:00:00 2001 From: chengma Date: Tue, 14 Jul 2026 12:23:09 +0800 Subject: [PATCH] feat(coordination): add atomic Gitea task workflow (phase 2) --- .gitea/ISSUE_TEMPLATE/task.md | 36 ++++ .gitea/PULL_REQUEST_TEMPLATE.md | 33 ++++ AGENTS.md | 2 + README.md | 4 + docs/00-ai-start-here.md | 13 +- docs/06-tasks.md | 4 +- docs/README.md | 7 +- docs/adoption-checklist.md | 2 + docs/agent-context.json | 1 + docs/clean-state-checklist.md | 2 + docs/current-state.md | 12 +- docs/gitea-collaboration.md | 121 +++++++++++++ docs/gitea-mcp.md | 9 + docs/method-map.md | 3 + docs/tasks/README.md | 27 ++- docs/tasks/_template.md | 11 ++ scripts/setup_gitea_labels.py | 290 ++++++++++++++++++++++++++++++++ tasks.md | 2 +- 18 files changed, 559 insertions(+), 20 deletions(-) create mode 100644 .gitea/ISSUE_TEMPLATE/task.md create mode 100644 .gitea/PULL_REQUEST_TEMPLATE.md create mode 100644 docs/gitea-collaboration.md create mode 100644 scripts/setup_gitea_labels.py diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..b1835f5 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -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`。映射提交完成前不可领取。 diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..fe35df0 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -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`。 diff --git a/AGENTS.md b/AGENTS.md index f490627..fecf4fb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,6 +36,8 @@ harness coding 需要的项目文档模板主要集中在 `docs/` 目录;根 领取任务时记录默认分支头提交为 `context_ref`;同一会话中文件 SHA 未变化时可复用已读内容,ref 变化后重新读取清单和受影响文档。 +模板复制到启用 Gitea 协作的项目后,还应先读 `docs/gitea-collaboration.md`:dispatcher 串行检查写路径和分配任务,每个任务以唯一 claim 分支防重复领取;每个 worker 同时最多一个活跃任务,工作分支 / worktree 独立,活跃任务的 `write_paths` 不得重叠。 + 如果用户要求修改某类模板,优先阅读对应文件,不要只凭文件名猜内容。 ## 工作规则 diff --git a/README.md b/README.md index fb69511..f158ba0 100644 --- a/README.md +++ b/README.md @@ -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 输出质量。 diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 3713dc1..67d647d 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -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。 如果代码实际状态和任务文件冲突,先说明冲突,不要擅自跳步或重排。 diff --git a/docs/06-tasks.md b/docs/06-tasks.md index d44d6fe..887b952 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -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`。 diff --git a/docs/README.md b/docs/README.md index 7e64720..1ec9054 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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` 进入。 diff --git a/docs/adoption-checklist.md b/docs/adoption-checklist.md index 6d0792d..58ad1e2 100644 --- a/docs/adoption-checklist.md +++ b/docs/adoption-checklist.md @@ -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`、运行脚本、测试配置和主要入口文件。 diff --git a/docs/agent-context.json b/docs/agent-context.json index aa7a96e..1f46645 100644 --- a/docs/agent-context.json +++ b/docs/agent-context.json @@ -43,6 +43,7 @@ ], "gitea": [ "docs/gitea-mcp.md", + "docs/gitea-collaboration.md", "docs/tasks/README.md", "docs/clean-state-checklist.md" ] diff --git a/docs/clean-state-checklist.md b/docs/clean-state-checklist.md index ce8b360..4e1129b 100644 --- a/docs/clean-state-checklist.md +++ b/docs/clean-state-checklist.md @@ -12,5 +12,7 @@ - [ ] [`current-state.md`](current-state.md) 的项目级快照与现实一致(启动/验证路径、目录要点、blocker)。 - [ ] 没有半成品改动处于未记录状态;如有,已写明 `BLOCKED` / `PARTIAL` 和原因。 - [ ] 代码处于可安全恢复的状态(必要时已提交,提交信息清晰)。 +- [ ] 本轮实际修改没有超出任务 `write_paths`,也没有与其他活跃任务发生路径重叠。 +- [ ] 启用 Gitea 时,Issue 的唯一 `status/*`、claim / 工作分支、PR 和任务状态彼此一致;未完成任务没有误删 claim。 任意一项不满足,就先补到满足,再结束会话。 diff --git a/docs/current-state.md b/docs/current-state.md index 0275054..8e29579 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -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;每轮执行记录、验证命令、阻塞点和关键决策写进该任务文件的 `## 执行记录`。 -- 本文件只保留当前快照,不保留完整历史。 \ No newline at end of file +- 任务长期状态改在对应任务文件的 frontmatter;启用 Gitea 时实时状态同步到 Issue。每轮执行记录、验证命令、阻塞点和关键决策写进该任务文件的 `## 执行记录`。 +- 本文件只保留当前快照,不保留完整历史。 diff --git a/docs/gitea-collaboration.md b/docs/gitea-collaboration.md new file mode 100644 index 0000000..70e56f7 --- /dev/null +++ b/docs/gitea-collaboration.md @@ -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//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//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 完成;不得在业务任务上试验。 diff --git a/docs/gitea-mcp.md b/docs/gitea-mcp.md index 15c7568..6ab536e 100644 --- a/docs/gitea-mcp.md +++ b/docs/gitea-mcp.md @@ -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//T-<编号>` 和独立 worktree;每任务 claim 只解决同任务重复领取,不单独保证跨任务路径互斥。 +- MCP 可创建 claim / 工作分支,但当前没有创建标签或删除分支工具。标签用 `python scripts/setup_gitea_labels.py --repo 【owner/repo】 --apply` 幂等初始化;过期 claim 由维护者通过 UI 或受控 REST 人工回收。 diff --git a/docs/method-map.md b/docs/method-map.md index 94e7567..1fb0aa2 100644 --- a/docs/method-map.md +++ b/docs/method-map.md @@ -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) | ## 使用原则 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 9bb4078..94c1276 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -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//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 状态协议。 diff --git a/docs/tasks/_template.md b/docs/tasks/_template.md index ed8254d..081a9a4 100644 --- a/docs/tasks/_template.md +++ b/docs/tasks/_template.md @@ -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 / 工作分支;任何新增写路径先检查与其他活跃任务是否重叠。) + ## 执行记录 (做完在此记录:改了哪些文件、跑的验证命令与结果、阻塞、关键决策。 diff --git a/scripts/setup_gitea_labels.py b/scripts/setup_gitea_labels.py new file mode 100644 index 0000000..6c0b9b9 --- /dev/null +++ b/scripts/setup_gitea_labels.py @@ -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()) diff --git a/tasks.md b/tasks.md index 342f3e5..0b98f50 100644 --- a/tasks.md +++ b/tasks.md @@ -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