From e9e6dede7a891908571ff53dd05be14759f46641 Mon Sep 17 00:00:00 2001 From: chengma Date: Thu, 16 Jul 2026 19:04:02 +0800 Subject: [PATCH] docs(review): add risk-based read-only agent gates --- .gitea/ISSUE_TEMPLATE/task.md | 8 ++++ .gitea/PULL_REQUEST_TEMPLATE.md | 14 +++++++ AGENTS.md | 8 ++++ README.md | 2 +- docs/00-ai-start-here.md | 4 +- docs/05-coding-rules.md | 3 +- docs/README.md | 2 +- docs/agent-context.json | 12 ++++++ docs/agent-context.md | 2 +- docs/clean-state-checklist.md | 1 + docs/evaluator-rubric.md | 13 +++++++ docs/gitea-collaboration.md | 25 +++++++++--- docs/method-map.md | 1 + docs/tasks/README.md | 54 ++++++++++++++++++++++++++ docs/tasks/_template.md | 7 +++- scripts/validate_harness_governance.py | 26 ++++++++++++- tasks.md | 1 + tests/test_governance.py | 30 ++++++++++++++ 18 files changed, 200 insertions(+), 13 deletions(-) diff --git a/.gitea/ISSUE_TEMPLATE/task.md b/.gitea/ISSUE_TEMPLATE/task.md index 500a78a..c94c90e 100644 --- a/.gitea/ISSUE_TEMPLATE/task.md +++ b/.gitea/ISSUE_TEMPLATE/task.md @@ -25,6 +25,13 @@ labels: 【真实验证命令和可观察结果;长期证据回填到任务文件。】 +## 评审计划 + +- review_mode: `【none | test | dual】` +- writer: `【领取后填写;必须与 claimed_by 一致】` +- test_reviewer: `【只读 agent-id;不适用时为 null】` +- risk_reviewer: `【只读安全/架构 agent-id;不适用时为 null】` + ## 协作状态 - expected_claim_branch: `claims/T-XXX` @@ -33,4 +40,5 @@ labels: - lease_until: `【领取后填写 RFC 3339 时间】` 领取必须遵循 `docs/gitea-collaboration.md` 的 dispatcher 串行分配与 claim 标记流程。不要在本 Issue 粘贴 Token、Authorization header 或私有配置。 +只有 writer 可以领取任务、修改仓库和提交;评审角色不创建 claim、工作分支或提交,也不直接更新本 Issue。 创建后先把 Issue 编号回填任务文件并合入默认分支,再按主要变更选择唯一 `type/docs` 或 `type/code`、一个 `priority/*` 和 `status/todo`。映射提交完成前不可领取。 diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md index fe35df0..e3b39fd 100644 --- a/.gitea/PULL_REQUEST_TEMPLATE.md +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -3,6 +3,7 @@ - Closes #【Issue 编号】 - task_file: `docs/tasks/T-XXX.md` - context_ref: `【领取任务时的提交 SHA】` +- candidate_sha: `【本轮独立评审针对的 PR head SHA】` - claim_branch: `claims/T-XXX` - work_branch: `agent/【agent-id】/T-XXX` - write_paths: @@ -19,6 +20,17 @@ | --- | --- | | `【真实命令】` | 【通过 / 失败摘要】 | +## 独立评审 + +- review_mode: `【none | test | dual】` + +| 角色 | reviewed_sha | verdict | 关键结论 | +| --- | --- | --- | --- | +| 测试评审 | `【SHA / 不适用】` | 【ACCEPT / REVISE / BLOCK / 不适用】 | 【缺失覆盖、边界场景、验证证据】 | +| 安全/架构评审 | `【SHA / 不适用】` | 【ACCEPT / REVISE / BLOCK / 不适用】 | 【权限、安全、模块边界、接口与依赖风险】 | + +评审角色只读,不修改仓库或 Gitea。候选提交变化后必须更新 `candidate_sha`,并重新评审受影响内容。 + ## 风险与回滚 【已知风险、兼容性影响、回滚方法;没有则写“无”。】 @@ -28,6 +40,8 @@ - [ ] 当前 PR 只对应一个任务 / Issue。 - [ ] 变更未超出 `write_paths`,没有夹带无关修改。 - [ ] 任务文件执行记录包含相同的验证证据。 +- [ ] `review_mode` 与任务风险一致;必需评审的 `reviewed_sha` 等于当前 PR head。 +- [ ] 没有未解决的 `BLOCK` 或高风险发现;修复后的新 SHA 已完成必要复审。 - [ ] 合并前任务文件 frontmatter 已为 `DONE`;`Closes` 自动关闭 Issue 不会制造假完成。 - [ ] 未提交 Token、Authorization header、私有配置或实例地址。 - [ ] Issue 已切换到唯一 `status/review`;合并后才标记 `status/done`。 diff --git a/AGENTS.md b/AGENTS.md index dbab8b2..c3206c9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,14 @@ harness coding 需要的项目文档模板主要集中在 `docs/` 目录;根 模板复制到启用 Gitea 协作的项目后,还应先读 `docs/gitea-collaboration.md`:dispatcher 串行检查写路径和分配任务,每个任务以唯一 claim 分支防重复领取;每个 worker 同时最多一个活跃任务,工作分支 / worktree 独立,活跃任务的 `write_paths` 不得重叠。 +任务内评审采用风险分级的单写者模型: + +- `review_mode: none`:低风险文档、链接或非敏感简单配置,只要求唯一写入 Agent 和自动验证。 +- `review_mode: test`:普通行为变更,增加一个只读测试评审 Agent。 +- `review_mode: dual`:权限、Token、隐私、数据迁移、公共 API / Schema、核心架构、部署或外部平台集成,增加只读测试评审 Agent 和只读安全/架构评审 Agent。 +- 只有写入 Agent 持有 `claimed_by`、工作分支和仓库写权限;评审 Agent 不修改文件、不提交 Git、不更新 Gitea,也不在共享工作区执行有副作用的命令。 +- 评审必须针对固定候选提交 SHA;修复产生新 SHA 后重新评审。dispatcher 汇总 `ACCEPT` / `REVISE` / `BLOCK`,存在未解决的阻断或高风险问题时不得合并。 + 如果用户要求修改某类模板,优先阅读对应文件,不要只凭文件名猜内容。 ## 工作规则 diff --git a/README.md b/README.md index 9f6a126..b434225 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ | [`docs/04-architecture.md`](docs/04-architecture.md) | 系统结构、职责边界、数据模型、开发顺序 | | [`docs/05-coding-rules.md`](docs/05-coding-rules.md) | AI 写代码前必须遵守的硬规则 | | [`docs/06-tasks.md`](docs/06-tasks.md) | 任务路线图:阶段划分、里程碑、待办池(只读,不跟踪单任务状态) | -| [`docs/tasks/README.md`](docs/tasks/README.md) | 默认任务管理:一任务一文件 `docs/tasks/T-<编号>.md`,单/多 agent 通用 | +| [`docs/tasks/README.md`](docs/tasks/README.md) | 默认任务管理:一任务一文件、唯一写入者和风险分级只读评审 | | [`docs/adoption-checklist.md`](docs/adoption-checklist.md) | 已有项目接入 harness 文档的迁移清单 | | [`docs/api.md`](docs/api.md) | API 合约模板 | | [`docs/routes.md`](docs/routes.md) | 页面路由、组件归属、导航规则 | diff --git a/docs/00-ai-start-here.md b/docs/00-ai-start-here.md index 9d63257..7531c06 100644 --- a/docs/00-ai-start-here.md +++ b/docs/00-ai-start-here.md @@ -67,13 +67,15 @@ - 若 `docs/tasks/` 暂无可领任务,先按 [`06-tasks.md`](06-tasks.md) 路线图把下一个建议任务落成任务文件,再领取。 - 未启用 Gitea 时,开始前在独立分支 / worktree 把该文件 frontmatter 的 `status` 改为 `DOING`。 - 启用 Gitea 时,由 dispatcher 按 [`gitea-collaboration.md`](gitea-collaboration.md) 串行检查写路径并创建 `claims/T-<编号>` 防御性标记;worker 只接受已读回确认的分配。不要把标签、assignee、读回或普通 create-branch API 单独当作并发锁。 +- 在任务 `## 协作约束` 中按风险选择 `review_mode: none | test | dual`。只有写入 Agent 领取任务和修改仓库;测试、安全/架构评审角色只读,不创建 claim、分支或提交。 +- 需要独立评审时,写入 Agent 完成自测后先产生候选提交;评审角色必须读取该固定 SHA、任务规格和验证证据。修复产生新 SHA 后,原评审结论失效并重新评审。 - 本轮只完成这一个任务;验收通过后改为 `DONE`。 - **执行记录写进该任务文件的 `## 执行记录`**(改了什么、跑了什么验证、结果、决策)。 - 若项目现实发生变化(启动/验证路径、目录结构、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 使用独立工作分支与 worktree。 +> 本约定单 agent 与多 agent 并发通用;并发时遵守 [`tasks/README.md`](tasks/README.md) 的编号、单写者、风险分级评审和写路径防撞规则。任务内只读评审可以并行,但不代表可以并行领取更多任务。 如果代码实际状态和任务文件冲突,先说明冲突,不要擅自跳步或重排。 diff --git a/docs/05-coding-rules.md b/docs/05-coding-rules.md index 31b46dd..909e02a 100644 --- a/docs/05-coding-rules.md +++ b/docs/05-coding-rules.md @@ -57,6 +57,7 @@ - [ ] 没有夹带无关改动。 - [ ] 涉及文档事实变化时,文档已同步。 - [ ] 已在当前任务文件(`docs/tasks/T-<编号>.md`)的 `## 执行记录` 记录跑过的命令和结果作为证据(约定见 [`tasks/README.md`](tasks/README.md)),不靠"代码已写"判定完成。 +- [ ] 任务声明 `review_mode: test` / `dual` 时,必需只读评审已覆盖当前 PR head;未启用 PR 时至少覆盖最后一个实现变更提交,后续仅允许当前任务文件的评审证据提交。没有未解决的 `BLOCK`、critical 或 high 发现。 - [ ] 回复里如实说明跑了什么命令、结果如何。 把真实命令填在这里: @@ -85,4 +86,4 @@ go test ./... ## 9. 拿不准就问 -问题要具体,说明你卡在哪里、有哪些选项、倾向哪个选项以及原因。 \ No newline at end of file +问题要具体,说明你卡在哪里、有哪些选项、倾向哪个选项以及原因。 diff --git a/docs/README.md b/docs/README.md index e53d45f..b10ab94 100644 --- a/docs/README.md +++ b/docs/README.md @@ -23,7 +23,7 @@ - [架构设计](04-architecture.md):系统结构、模块职责、数据模型、关键风险和开发顺序。 - [编码规则](05-coding-rules.md):AI 写代码前必须遵守的硬约束。 - [任务路线图](06-tasks.md):阶段划分、里程碑和待办池;只读,不跟踪单任务状态。 -- [任务文件(默认)](tasks/README.md):一任务一文件 `docs/tasks/T-<编号>.md`,单/多 agent 通用,每个 agent 同时只做一个;启用 Gitea 后由 Issue 承担实时状态。 +- [任务文件(默认)](tasks/README.md):一任务一文件、唯一写入者和风险分级只读评审;每个 agent 同时只做一个任务,启用 Gitea 后由 Issue 承担实时状态。 - [已有项目接入清单](adoption-checklist.md):把本模板补进已有代码库时的迁移步骤和第一轮任务建议。 - [API 合约](api.md):前后端接口形状、错误格式、鉴权约定。 - [路由与页面结构](routes.md):页面路由、页面职责、组件归属。 diff --git a/docs/agent-context.json b/docs/agent-context.json index 1f46645..3c24d4b 100644 --- a/docs/agent-context.json +++ b/docs/agent-context.json @@ -41,6 +41,18 @@ "docs/03-tech-stack.md", "docs/current-state.md" ], + "test_review": [ + "docs/02-requirements.md", + "docs/05-coding-rules.md", + "docs/tasks/README.md", + "docs/evaluator-rubric.md" + ], + "risk_review": [ + "docs/04-architecture.md", + "docs/api.md", + "docs/05-coding-rules.md", + "docs/tasks/README.md" + ], "gitea": [ "docs/gitea-mcp.md", "docs/gitea-collaboration.md", diff --git a/docs/agent-context.md b/docs/agent-context.md index 45c9429..51295c0 100644 --- a/docs/agent-context.md +++ b/docs/agent-context.md @@ -28,7 +28,7 @@ -> 修改与验证 ``` -一个任务可以命中多个路由。例如修改带 API 的页面时,同时读取 `ui` 和 `api`,重复路径只加载一次。 +一个任务可以命中多个路由。例如修改带 API 的页面时,同时读取 `ui` 和 `api`,重复路径只加载一次。只读测试评审在原任务路由上增加 `test_review`;安全/架构评审增加 `risk_review`,仍需读取本轮任务文件和精确候选提交,而不是只读 writer 的总结。 ## 提交 SHA 与缓存 diff --git a/docs/clean-state-checklist.md b/docs/clean-state-checklist.md index 3ff29f0..4411894 100644 --- a/docs/clean-state-checklist.md +++ b/docs/clean-state-checklist.md @@ -13,6 +13,7 @@ - [ ] 没有半成品改动处于未记录状态;如有,已写明 `BLOCKED` / `PARTIAL` 和原因。 - [ ] 代码处于可安全恢复的状态(必要时已提交,提交信息清晰)。 - [ ] 本轮实际修改没有超出任务 `write_paths`,也没有与其他活跃任务发生路径重叠。 +- [ ] `review_mode: test` / `dual` 的必需评审已覆盖当前 PR head;未启用 PR 时至少覆盖最后一个实现变更提交,且之后只有当前任务文件的评审证据提交。没有沿用旧 SHA 结论或遗留阻断 / 高风险发现。 - [ ] 启用 Gitea 时,Issue 的唯一 `status/*`、claim / 工作分支、PR 和任务状态彼此一致;未完成任务没有误删 claim。 - [ ] 已运行 `python scripts/validate_harness_governance.py`;启用且可连接 Gitea 时,还运行了只读 `python scripts/audit_gitea_coordination.py --repo 【owner/repo】 --dispatcher 【Gitea登录名】`。 diff --git a/docs/evaluator-rubric.md b/docs/evaluator-rubric.md index ea8fb4e..4e86011 100644 --- a/docs/evaluator-rubric.md +++ b/docs/evaluator-rubric.md @@ -24,6 +24,19 @@ - **Revise** — 需要修补才能接受(列出必须补的修复)。 - **Block** — 有根本性问题,需要先解决(列出阻塞项)。 +## 独立评审门禁 + +如果任务声明了 `review_mode: test` 或 `dual`,评分前还要核对: + +- 评审角色没有修改仓库、提交 Git 或直接更新 Gitea。 +- 每个必需评审都记录了 `reviewed_sha` 和 `ACCEPT` / `REVISE` / `BLOCK`。 +- 启用 PR 时,最终 `reviewed_sha` 等于待验收的 PR head;未启用 PR 时,它覆盖最后一个实现变更提交,之后只有当前任务文件的评审证据提交。修复后不能继续沿用旧 SHA 的结论。 +- 测试评审检查了验收标准、边界 / 失败路径、回归范围和实际命令证据。 +- `dual` 模式的安全/架构评审检查了权限、秘密、数据风险、模块边界、公共接口、依赖和回滚。 +- 未解决的 `BLOCK`、critical 或 high 发现存在时,结论不得为 **Accept**。 + +`review_mode: none` 不要求为了形式启动评审 Agent,但仍必须满足自动验证和本表六个评分维度。 + ## 后续动作 - 缺失的证据: diff --git a/docs/gitea-collaboration.md b/docs/gitea-collaboration.md index 2e8f441..4bfa9a4 100644 --- a/docs/gitea-collaboration.md +++ b/docs/gitea-collaboration.md @@ -93,13 +93,28 @@ git worktree add ../【项目】-T-123 -b agent/【agent-id】/T-123 origin/agen 不要让多个 agent 共用同一 worktree,也不要在 claim 分支提交工作代码。 +## 任务内只读评审 + +任务可以按 [`tasks/README.md`](tasks/README.md) 选择 `review_mode: none | test | dual`。这属于同一任务内部的质量门禁,不创建额外任务 claim,也不改变单 dispatcher 串行分配规则: + +- writer 是唯一 `claimed_by`,也是唯一可以修改仓库、提交和推送工作分支的角色。 +- 测试评审与安全/架构评审只读本地任务、候选提交、PR diff 和验证证据;不创建 claim / 工作分支,不修改文件,不提交 Git,不直接更新 Issue、标签或 PR。 +- 评审应针对精确的候选提交 SHA。writer 修复并推送新提交后,旧 SHA 上的评审不能作为最终合并证据。 +- 测试评审重点检查验收标准、边界场景、失败路径、回归范围和命令证据。 +- 安全/架构评审重点检查权限、Token / 隐私、不可逆操作、数据迁移、模块边界、公共 API / Schema、依赖和回滚。 +- 需要执行有副作用的测试时,由 writer 运行,或在一次性 worktree / 容器 / 临时环境运行;不得让只读评审污染共享 worktree 或外部系统。 +- dispatcher 汇总 `ACCEPT` / `REVISE` / `BLOCK`。未解决的 `BLOCK`、critical 或 high 发现禁止合并;连续两轮仍为 `BLOCK` 时将任务置为 blocked 并请求人工裁决。 + +评审角色可以并行读取同一个固定 SHA,但项目仍只有一个活跃任务所有者。评审身份和 `review_mode` 写入任务文件与 Issue;精确的 `candidate_sha`、最终 `reviewed_sha` 和 verdict 写入 PR / Issue,任务执行记录保存自测、修复动作和评审链接。不要把当前提交 SHA 写回同一个提交中的任务文件。 + ## 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 操作删除。 +1. writer 运行任务要求的验证,在任务文件 `## 执行记录` 写入实际命令和结果,然后产生候选提交;dispatcher / PR 记录其 `candidate_sha`。 +2. `test` / `dual` 模式下,必需评审角色针对候选 SHA 输出结构化结论;writer 修复后产生新 SHA 并触发必要复审。 +3. PR 使用 `.gitea/PULL_REQUEST_TEMPLATE.md`,链接 `Closes #【Issue 编号】`、任务文件、`context_ref`、`candidate_sha`、写路径、验证证据和独立评审结论。 +4. 必需评审没有未解决的 `BLOCK` / high 风险、且最终 `reviewed_sha` 等于 PR head 后,才把 Issue 切到唯一 `status/review`。评审失败则把 Issue 和工作分支任务状态一起回到 doing / blocked。 +5. PR 合并、默认分支任务文件为 `DONE` 后,Issue 才切到 `status/done` 并关闭。 +6. MCP 当前没有删除分支工具。清理 claim 前先确认 PR 已合并、Issue 已完成且无恢复需要,再由维护者通过 Gitea UI 或受控 REST 操作删除。 ## 过期 claim 与断连 diff --git a/docs/method-map.md b/docs/method-map.md index 14a7616..e70a94b 100644 --- a/docs/method-map.md +++ b/docs/method-map.md @@ -11,6 +11,7 @@ | 启动脆弱 | 每轮会话都要重新学怎么启动、装依赖、跑测试 | 统一启动与验证路径 | [`../init.sh`](../init.sh) / [`../init.ps1`](../init.ps1) | | 范围蔓延 | 一次启动多个任务,最后没有一个完整收尾 | 限制当前活跃范围,一轮只做一个任务 | [`tasks/README.md`](tasks/README.md) | | 提前宣布完成 | 代码改了就说"完成了",但没有可运行证据 | 把完成绑定到验证证据 | [`tasks/README.md`](tasks/README.md)(passing 需证据)+ [`clean-state-checklist.md`](clean-state-checklist.md) | +| 自写自审偏差 | 写入 Agent 只复述自己的实现,自测缺口和安全 / 架构风险被合理化 | 保留唯一写入者,按风险增加只读测试与安全 / 架构评审,并针对固定 SHA 复审 | [`tasks/README.md`](tasks/README.md) + [`gitea-collaboration.md`](gitea-collaboration.md) | | 交接薄弱 | 下一轮看不出哪里可用、哪里坏了、接下来做什么 | 每轮留下明确的当前快照和下一步 | [`current-state.md`](current-state.md) | | 评审主观 | 质量判断靠个人记忆和感觉,agent 容易自我说服通过 | 用固定维度做评分 | [`evaluator-rubric.md`](evaluator-rubric.md) | | 代码库悄悄退化 | 速度上去了,但几轮会话后代码越来越难审、边界越来越糊 | 定期给代码库健康度打分 | [`quality-document.md`](quality-document.md) | diff --git a/docs/tasks/README.md b/docs/tasks/README.md index 309fde3..ce3a3fe 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -65,6 +65,60 @@ write_paths: # 允许修改的仓库相对路径 - 进入评审后 Issue 使用唯一 `status/review`;PR 合并且默认分支任务文件为 `DONE` 后,Issue 才能关闭并标记 `status/done`。 - 领取、结构化 claim 评论、过期锁回收和分支清理的完整规则见 [`../gitea-collaboration.md`](../gitea-collaboration.md)。 +## 单任务多角色评审(风险分级) + +多角色评审仍然只处理一个任务,不增加第二个任务所有者。dispatcher / 主 Agent 负责选择评审级别和裁决结果;只有一个 writer 持有 `claimed_by`、工作分支和仓库写权限。 + +### 评审级别 + +| `review_mode` | 适用任务 | 必需角色 | +| --- | --- | --- | +| `none` | 文案、链接、非敏感简单配置、无行为变化且自动校验充分 | writer | +| `test` | 普通功能、缺陷修复、行为或测试变化 | writer + 只读测试评审 | +| `dual` | 登录、权限、Token、隐私、资金、删除 / 迁移、公共 API / Schema、核心架构、部署、外部平台集成 | writer + 只读测试评审 + 只读安全/架构评审 | + +项目可以提高某类任务的默认级别,但不得把高风险任务降为 `none`。评审安排写在任务文件 `## 协作约束`;启用 Gitea 时同时写入 Issue。 + +### 角色权限 + +| 角色 | 可以做 | 不可以做 | +| --- | --- | --- | +| writer | 修改允许路径、写测试、运行验证、提交和推送工作分支、回填执行记录 | 忽略必需评审、把自己的自测冒充独立评审 | +| 测试评审 | 读取任务、代码、候选 diff 和验证证据;提出测试矩阵、边界场景和缺失覆盖 | 修改文件、提交 Git、更新 Gitea、在共享工作区执行会产生文件或外部状态的命令 | +| 安全/架构评审 | 检查权限、秘密、数据风险、模块边界、公共接口、依赖和回滚 | 修改文件、提交 Git、更新 Gitea、替 writer 顺手修复 | +| dispatcher / 主 Agent | 分配角色、汇总结论、决定 `ACCEPT` / `REVISE` / `BLOCK` | 在仍有未解决阻断或高风险发现时放行 | + +评审角色不是任务领取者:不创建 claim、不创建工作分支、不成为 `claimed_by`。需要实际运行可能写缓存、数据库或外部系统的测试时,由 writer 执行,或使用一次性 worktree、容器 / 临时环境;“只读评审”不能只靠提示词约束。 + +### 执行顺序 + +1. dispatcher 根据任务风险确定 `review_mode`,并把角色写入 `## 协作约束`。 +2. `test` / `dual` 模式下,评审角色可在实现前只读任务规格,分别给出测试重点和风险清单;不得提前修改实现。 +3. writer 完成实现和自测,先把验证证据写入任务文件,再产生候选提交;dispatcher / PR 记录该提交为 `candidate_sha`。不要把当前提交 SHA 写回同一个提交中的任务文件,避免自引用。 +4. 必需评审角色独立读取任务规格、`context_ref`、候选提交 SHA / diff 和验证证据;不能只读 writer 的总结。 +5. 每个评审输出结构化结论。dispatcher 汇总后决定: + - `ACCEPT`:该角色未发现阻断项; + - `REVISE`:存在必须由 writer 修复的问题; + - `BLOCK`:需求、架构、安全前提或验证环境存在根本阻塞。 +6. writer 修复后产生新 SHA;此前针对旧 SHA 的结论不能直接复用,至少重新评审受影响内容。 +7. 启用 PR 时,最终必需评审的 `reviewed_sha` 必须等于待合并 PR head。未启用 PR 时,`reviewed_sha` 至少覆盖最后一个代码 / 配置变更提交;其后的提交只能更新当前任务文件中的评审证据,dispatcher 必须用 `git diff ..HEAD` 确认没有实现变化。 +8. 连续两轮仍为 `BLOCK`,或平台无法提供任务要求的独立评审角色时,把任务标为 `BLOCKED` 并请求维护者 / 用户裁决;不得由 writer 冒充评审或静默降低 `review_mode`。 + +评审结果使用稳定格式: + +```text +REVIEW +role: test | risk +reviewed_sha: 【40 位提交 SHA】 +verdict: ACCEPT | REVISE | BLOCK +findings: + - severity: critical | high | medium | low + evidence: 【文件、测试或行为证据】 + required_action: 【必须修复或说明的动作】 +``` + +评审 Agent 只返回报告;writer 在 `## 执行记录` 写自测、修复动作和评审链接,dispatcher 在 PR / Issue 记录精确的 `reviewed_sha` 与 verdict。若要把评审摘要复制进任务文件,必须先提交该变更,再让必需评审检查新的 PR head;评审 Agent 本身不直接写远端状态。 + ## 用户指令暗语(可选约定) > 用户的工作流通常固定为:提 bug/需求 → 讨论定案 → 落成任务文件 → 提交 → 实现 → 提交。 diff --git a/docs/tasks/_template.md b/docs/tasks/_template.md index 081a9a4..cdd0e60 100644 --- a/docs/tasks/_template.md +++ b/docs/tasks/_template.md @@ -32,7 +32,12 @@ write_paths: ## 协作约束 -(启用 Gitea 时填写对应 Issue、领取时的 `context_ref`、claim / 工作分支;任何新增写路径先检查与其他活跃任务是否重叠。) +- review_mode: 【none | test | dual】 +- writer: 【唯一写入 agent-id;未领取时为 null】 +- test_reviewer: 【只读测试评审 agent-id;不适用时为 null】 +- risk_reviewer: 【只读安全/架构评审 agent-id;不适用时为 null】 + +(启用 Gitea 时填写对应 Issue、领取时的 `context_ref`、claim / 工作分支。只有 writer 可以修改和提交仓库;评审角色只读。任何新增写路径先检查与其他活跃任务是否重叠。候选 SHA 由 PR / 评审报告记录,避免任务文件自引用;分支产生任何新提交后重新评审。) ## 执行记录 diff --git a/scripts/validate_harness_governance.py b/scripts/validate_harness_governance.py index 2ae6d96..4b59543 100644 --- a/scripts/validate_harness_governance.py +++ b/scripts/validate_harness_governance.py @@ -611,14 +611,36 @@ def validate_gitea_artifacts(root: Path) -> list[Finding]: require_markers( root, ".gitea/ISSUE_TEMPLATE/task.md", - ("task_id:", "task_file:", "context_ref:", "write_paths:", "lease_until:"), + ( + "task_id:", + "task_file:", + "context_ref:", + "write_paths:", + "lease_until:", + "review_mode:", + "writer:", + "test_reviewer:", + "risk_reviewer:", + ), ) ) findings.extend( require_markers( root, ".gitea/PULL_REQUEST_TEMPLATE.md", - ("Closes #", "task_file:", "context_ref:", "write_paths:", "验证证据"), + ( + "Closes #", + "task_file:", + "context_ref:", + "candidate_sha:", + "write_paths:", + "验证证据", + "review_mode:", + "测试评审", + "安全/架构评审", + "reviewed_sha", + "verdict", + ), ) ) workflow = ".gitea/workflows/harness-governance.yml" diff --git a/tasks.md b/tasks.md index becb2bb..06cb651 100644 --- a/tasks.md +++ b/tasks.md @@ -98,6 +98,7 @@ Get-ChildItem -Recurse -File | H-602 | Phase 1:增加上下文清单与按需读取流程 | H-601 | 有机器可读清单和无第三方依赖验证;agent 按任务类型读取;同一 SHA 不重复加载 | DONE | | H-603 | Phase 2:建立 Issue / 任务文件 / PR 多 Agent 协调协议 | H-602 | 任务映射、领取读回校验、分支 / worktree 和写路径防撞规则完整 | DONE | | H-604 | Phase 3:增加自动化治理与一致性检查 | H-603 | 离线导航、清单、任务元数据和敏感信息检查可运行;Actions 模板就绪,实际运行以 runner 启用为前提 | DONE | +| H-605 | 增加风险分级的单任务多角色评审门禁 | H-604 | 唯一 writer;`none/test/dual` 分级;只读测试与安全/架构评审针对固定 SHA;PR 模板和治理测试防止协议漂移 | DONE | ## Backlog diff --git a/tests/test_governance.py b/tests/test_governance.py index f41ec73..99cc32b 100644 --- a/tests/test_governance.py +++ b/tests/test_governance.py @@ -25,6 +25,7 @@ from setup_gitea_labels import LABELS, NoRedirect, build_plan from test_gitea_claim_race import PROBE_PREFIX, new_probe_branch from validate_agent_context import validate_manifest from validate_harness_governance import ( + validate_gitea_artifacts, validate_markdown_links, validate_navigation, validate_repository, @@ -63,6 +64,35 @@ class OfflineRuleTests(unittest.TestCase): self.assertTrue(any(item.path == "README.md" for item in findings)) self.assertTrue(any(item.path == "docs/README.md" for item in findings)) + def test_gitea_review_markers_are_required(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + artifacts = ( + ".gitea/ISSUE_TEMPLATE/task.md", + ".gitea/PULL_REQUEST_TEMPLATE.md", + ".gitea/workflows/harness-governance.yml", + ) + for relative in artifacts: + source = ROOT / relative + target = root / relative + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(source.read_text(encoding="utf-8"), encoding="utf-8") + pr_template = root / ".gitea" / "PULL_REQUEST_TEMPLATE.md" + pr_template.write_text( + pr_template.read_text(encoding="utf-8").replace( + "candidate_sha:", "candidate-sha:", 1 + ), + encoding="utf-8", + ) + findings = validate_gitea_artifacts(root) + self.assertTrue( + any( + item.path == ".gitea/PULL_REQUEST_TEMPLATE.md" + and "candidate_sha:" in item.message + for item in findings + ) + ) + def test_task_status_and_scope_errors_are_reported(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory)