@routerhub/agent-rules 1.5.152 → 1.5.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.base.md CHANGED
@@ -188,6 +188,52 @@
188
188
  - ⚠️ 提交前按「最坏情况」自检:边界值、空值、异常路径、并发/重复触发、重试、依赖不可用(Redis/DB/网络)时,行为是否仍正确、会不会出错或重复执行。
189
189
  - ⚠️ 沉淀反模式:每次 review 暴露的设计问题(锁粒度、职责划分、重复实现等),提炼成通用的「反模式」记录,下次写同类代码时自发对照,避免重犯。
190
190
 
191
+ ## ⚠️ 反模式清单(写代码前对照,防止「换件衣服又踩一遍」)
192
+
193
+ 「代码质量 → 沉淀反模式」给出机制,本清单是已沉淀的可迁移反模式。写代码前对照一遍,命中即自查。每条都是「同一类坑换框架/换字段名再现」的通用模式,不局限于本例。发现新的坑,当场按此格式回填。
194
+
195
+ ### ① 一次性资源消费陷阱:读了就不能再读
196
+
197
+ - ⚠️ 对「同一份输入」做两次读取(读取 + 再读取 / 读取 + 绑定 / 读取 + 解析),必须确认第一次读取不会把源头「消耗掉」。
198
+ - **类比:一瓶汽水只能喝一次。喝完后想再倒一杯,瓶子里已经空了。**
199
+ - 典型反例:Gin `ShouldBindJSON` 会消费 request body,之后再 `GetRawData()` 读到的是空——必须先 `GetRawData()` 把原样存下、还原 body 再 bind。
200
+ - 自查:凡对同一数据做了「先 X 再 Y 的两次读取」,先问 X 是否消费了源头;拿不准就把原样先存一份。
201
+
202
+ ### ② 守卫旁路:校验只守了一个入口
203
+
204
+ - ⚠️ 状态变更/停用/删除等「有前置条件的写操作」,必须确保**所有能到达该状态变更的入口**都经过同一套守卫,不能只守 UI/常规路径。
205
+ - **类比:小区只有正门有保安,侧门没锁——坏人从侧门就进去了。**
206
+ - 典型反例:停用守卫只放在 `PATCH .../status`,但 `PUT /vendors/:id` 的请求结构体仍有 `status` 字段,裸 API 传 `status=disabled` 直接绕过守卫。
207
+ - 自查:给某个「状态/权限/开关」加守卫时,先列出**所有能改这个状态的写路径**(PUT/POST/PATCH/脚本/批量),逐个确认都过了守卫。
208
+
209
+ ### ③ 字段语义分离:「不带」≠「传空」
210
+
211
+ - ⚠️ 区分「请求没带这个字段」(保持原值)和「带了字段但值为 null/空」(清空/置空)是两种不同的语义,必须分开处理。
212
+ - **类比:顾客没点饮料(维持现状)≠ 顾客点了「不要饮料」(明确要求不给)。**
213
+ - 典型反例:`vendor_id` 不带应保持原归属,显式传 `null` 才清空;若混为一谈,旧调用方不带字段时归属被静默清空。
214
+ - 自查:字段可选时,用「键是否存在」判断语义,而不是「值是否为 null」;两条路径各测一遍。
215
+
216
+ ### ④ 缓存失效 ≠ 视图状态恢复:清缓存要连带恢复展开/选中态
217
+
218
+ - ⚠️ 清空缓存后,如果界面上仍有「基于旧缓存展开/选中」的视图状态,必须同步重拉或复位,否则出现「展开但空白」的假象。
219
+ - **类比:刷新冰箱时把饮料全部拿出来,但购物单还勾着「已补货」——你盯着空架子以为饮料没了,其实只是没放回去。**
220
+ - 典型反例:`refreshVendorSidebar` 清空 `vendorAccounts` 缓存但 `expandedVendors` 仍为 true,展开的分组不触发重拉,显示空白像「账户全没了」。
221
+ - 自查:任何「清空缓存」操作,顺手列一遍哪些 UI 状态依赖这份缓存,对激活中的(展开/选中/滚动位置)逐个重拉或复位。
222
+
223
+ ### ⑤ 加载状态用独立标记,别拿「数组长度/结果为空」推断
224
+
225
+ - ⚠️ 「是否已加载」要单独用一个布尔标记记录,不要用「数据长度 === 0」推断「还没加载」——真的空数据会被误判成「未加载」,导致重复请求或永不刷新。
226
+ - **类比:柜台空着 ≠ 没营业。营业了但没顾客,和没开门,是两回事,看柜台空判断会搞混。**
227
+ - 典型反例:`if (list.length === 0) fetch()`——真实 0 条数据时每次展开都重复请求;反过来列表非空时切 tab 又永不刷新计数。
228
+ - 自查:用 `loaded` 布尔标记区分「加载过(含空)」与「未加载」;不要用数据本身的有无/长度推断。
229
+
230
+ ### ⑥ 白名单优先于黑名单:状态判断写「只允许 active」而非「排除 disabled」
231
+
232
+ - ⚠️ 判断某状态是否可用时,用白名单(`status === 'active'`)而非黑名单(`status !== 'disabled'`)——状态枚举一扩展,黑名单就漏放新状态。
233
+ - **类比:安检只查「名单上列的违禁品」会漏掉新违禁品;「只放行持有效票的人」才兜得住。**
234
+ - 典型反例:`.filter(v => v.status !== 'disabled')` 在将来新增第三种状态(如 `suspended`)时会被误放行,保存必被后端拒绝。
235
+ - 自查:凡「可选/可用/合法」判定,写成「只保留允许的那些」而不是「排除不允许的那些」。
236
+
191
237
  ## ⚠️ 数据链路改动核对铁律
192
238
 
193
239
  - ⚠️ **给一个数据结构(interface/struct/DTO)新增或修改字段后,必须顺着这份数据流转的每一个转发/序列化点逐一核对,不能只改了数据结构定义或链路两端就算完成**。典型漏改位置是中间层"手写请求体字面量"(如 `JSON.stringify({ a, b })`、手动拼接的 `params`/`payload`),新增字段不会自动带过去,也不会报错,只会表现为"下游一直是空的"这种沉默失败。
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  所有对 @routerhub/agent-rules 的重大更改都会记录在这个文件中。
4
4
 
5
+ ## [1.5.154] - 2026-08-27
6
+
7
+ ### Added
8
+
9
+ - **loop-review 循环收敛状态持久化(跨会话防回跳)**:新增 `skills/loop-review/scripts/save-review-decision.sh`(判断表落盘)+ `query-review-decision.sh`(跨轮去重查询),把每轮「值得修 / Won't fix / 误报」的判断落盘到 `.tmp-loop-review/decisions/pr-<PR>.decisions.json`。会话中断 / 上下文压缩后,下轮用 `query-review-decision.sh` 命中已处理建议直接复用结论,从「靠对话记忆」升级为「靠文件记忆」,根治循环回跳。
10
+ - **外部事实核查命令化**:新增 `scripts/check-facts.sh`,把「审查方法④」从「依赖 reviewer 自觉 dig/curl」变成一条可复跑命令——对 PR 里的域名 / 子域 / 邮箱 / URL / 环境归属批量核查(`dig A` / `dig MX` / `curl -IL`),输出 PASS/FAIL 汇总,FAIL 项按「必须报」处理。
11
+
12
+ ### Changed
13
+
14
+ - **release.sh 规则漂移检查**:发版前若 `AGENTS.base.md` / `AGENTS.private.md` / `skills` / `rules` 有未提交改动,强制先 `node merge.js sync`,并用派生产物指纹(AGENTS.md / CLAUDE.md / .github 指令文件 / .claude/skills 目录)校验确实同步,否则中止发布——防止「规则改了但下游没吃到」的静默漂移。
15
+ - **github-pr-review 嵌入审稿边界**:处理每条评论前先用审稿边界判断(该不该改),而非凭 bot 严重度标签定夺;处理完必须给落点(修复 / Won't fix / 误报)并(loop-review 场景下)落盘 decisions。新增「Review boundary」Rules 章节。
16
+
5
17
  ## [1.5.152] - 2026-08-26
6
18
 
7
19
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routerhub/agent-rules",
3
- "version": "1.5.152",
3
+ "version": "1.5.154",
4
4
  "description": "Shared Copilot agent rules and guidelines for RouterHub projects",
5
5
  "main": "AGENTS.base.md",
6
6
  "bin": {
package/rules/global.md CHANGED
@@ -188,6 +188,52 @@ name: "通用规则"
188
188
  - ⚠️ 提交前按「最坏情况」自检:边界值、空值、异常路径、并发/重复触发、重试、依赖不可用(Redis/DB/网络)时,行为是否仍正确、会不会出错或重复执行。
189
189
  - ⚠️ 沉淀反模式:每次 review 暴露的设计问题(锁粒度、职责划分、重复实现等),提炼成通用的「反模式」记录,下次写同类代码时自发对照,避免重犯。
190
190
 
191
+ ## ⚠️ 反模式清单(写代码前对照,防止「换件衣服又踩一遍」)
192
+
193
+ 「代码质量 → 沉淀反模式」给出机制,本清单是已沉淀的可迁移反模式。写代码前对照一遍,命中即自查。每条都是「同一类坑换框架/换字段名再现」的通用模式,不局限于本例。发现新的坑,当场按此格式回填。
194
+
195
+ ### ① 一次性资源消费陷阱:读了就不能再读
196
+
197
+ - ⚠️ 对「同一份输入」做两次读取(读取 + 再读取 / 读取 + 绑定 / 读取 + 解析),必须确认第一次读取不会把源头「消耗掉」。
198
+ - **类比:一瓶汽水只能喝一次。喝完后想再倒一杯,瓶子里已经空了。**
199
+ - 典型反例:Gin `ShouldBindJSON` 会消费 request body,之后再 `GetRawData()` 读到的是空——必须先 `GetRawData()` 把原样存下、还原 body 再 bind。
200
+ - 自查:凡对同一数据做了「先 X 再 Y 的两次读取」,先问 X 是否消费了源头;拿不准就把原样先存一份。
201
+
202
+ ### ② 守卫旁路:校验只守了一个入口
203
+
204
+ - ⚠️ 状态变更/停用/删除等「有前置条件的写操作」,必须确保**所有能到达该状态变更的入口**都经过同一套守卫,不能只守 UI/常规路径。
205
+ - **类比:小区只有正门有保安,侧门没锁——坏人从侧门就进去了。**
206
+ - 典型反例:停用守卫只放在 `PATCH .../status`,但 `PUT /vendors/:id` 的请求结构体仍有 `status` 字段,裸 API 传 `status=disabled` 直接绕过守卫。
207
+ - 自查:给某个「状态/权限/开关」加守卫时,先列出**所有能改这个状态的写路径**(PUT/POST/PATCH/脚本/批量),逐个确认都过了守卫。
208
+
209
+ ### ③ 字段语义分离:「不带」≠「传空」
210
+
211
+ - ⚠️ 区分「请求没带这个字段」(保持原值)和「带了字段但值为 null/空」(清空/置空)是两种不同的语义,必须分开处理。
212
+ - **类比:顾客没点饮料(维持现状)≠ 顾客点了「不要饮料」(明确要求不给)。**
213
+ - 典型反例:`vendor_id` 不带应保持原归属,显式传 `null` 才清空;若混为一谈,旧调用方不带字段时归属被静默清空。
214
+ - 自查:字段可选时,用「键是否存在」判断语义,而不是「值是否为 null」;两条路径各测一遍。
215
+
216
+ ### ④ 缓存失效 ≠ 视图状态恢复:清缓存要连带恢复展开/选中态
217
+
218
+ - ⚠️ 清空缓存后,如果界面上仍有「基于旧缓存展开/选中」的视图状态,必须同步重拉或复位,否则出现「展开但空白」的假象。
219
+ - **类比:刷新冰箱时把饮料全部拿出来,但购物单还勾着「已补货」——你盯着空架子以为饮料没了,其实只是没放回去。**
220
+ - 典型反例:`refreshVendorSidebar` 清空 `vendorAccounts` 缓存但 `expandedVendors` 仍为 true,展开的分组不触发重拉,显示空白像「账户全没了」。
221
+ - 自查:任何「清空缓存」操作,顺手列一遍哪些 UI 状态依赖这份缓存,对激活中的(展开/选中/滚动位置)逐个重拉或复位。
222
+
223
+ ### ⑤ 加载状态用独立标记,别拿「数组长度/结果为空」推断
224
+
225
+ - ⚠️ 「是否已加载」要单独用一个布尔标记记录,不要用「数据长度 === 0」推断「还没加载」——真的空数据会被误判成「未加载」,导致重复请求或永不刷新。
226
+ - **类比:柜台空着 ≠ 没营业。营业了但没顾客,和没开门,是两回事,看柜台空判断会搞混。**
227
+ - 典型反例:`if (list.length === 0) fetch()`——真实 0 条数据时每次展开都重复请求;反过来列表非空时切 tab 又永不刷新计数。
228
+ - 自查:用 `loaded` 布尔标记区分「加载过(含空)」与「未加载」;不要用数据本身的有无/长度推断。
229
+
230
+ ### ⑥ 白名单优先于黑名单:状态判断写「只允许 active」而非「排除 disabled」
231
+
232
+ - ⚠️ 判断某状态是否可用时,用白名单(`status === 'active'`)而非黑名单(`status !== 'disabled'`)——状态枚举一扩展,黑名单就漏放新状态。
233
+ - **类比:安检只查「名单上列的违禁品」会漏掉新违禁品;「只放行持有效票的人」才兜得住。**
234
+ - 典型反例:`.filter(v => v.status !== 'disabled')` 在将来新增第三种状态(如 `suspended`)时会被误放行,保存必被后端拒绝。
235
+ - 自查:凡「可选/可用/合法」判定,写成「只保留允许的那些」而不是「排除不允许的那些」。
236
+
191
237
  ## ⚠️ 数据链路改动核对铁律
192
238
 
193
239
  - ⚠️ **给一个数据结构(interface/struct/DTO)新增或修改字段后,必须顺着这份数据流转的每一个转发/序列化点逐一核对,不能只改了数据结构定义或链路两端就算完成**。典型漏改位置是中间层"手写请求体字面量"(如 `JSON.stringify({ a, b })`、手动拼接的 `params`/`payload`),新增字段不会自动带过去,也不会报错,只会表现为"下游一直是空的"这种沉默失败。
@@ -123,6 +123,27 @@ For each comment, in severity order:
123
123
  7. **Apply fix** if approved
124
124
  8. **Verify ALL issues** in the comment are addressed (multi-issue comments are common)
125
125
 
126
+ ### 3.1 Apply the review boundary (审稿边界判断)
127
+
128
+ **severity 只是 bot 的标注,不是「该不该改」的依据。** 每一条评论在提案修改之前,先用项目的审稿边界(`AGENTS.base.md`「审稿边界 / Review Boundary」章节,源文件 `rules/review-boundary.md`)过一遍:
129
+
130
+ | 判断 | 处理 |
131
+ |------|------|
132
+ | **会导致实际 bug / 数据丢失 / 数据错误 / 安全漏洞**(有明确触发路径,读 diff 能确认) | ✅ 必须修 |
133
+ | **违反 AGENTS.base.md / AGENTS.private.md 的 ⚠️ 铁律**(DELETE 铁律、部署自包含、数据链路核对、环境配置禁止推断等) | ✅ 必须修 |
134
+ | **口味 / 风格偏好 / 重构建议**(换个命名、建议用某设计模式、提取公共函数) | ⛔ Won't fix |
135
+ | **纯防御性建议**(「如果未来 / 万一 / 边界情况下」开头,当前代码无实际触发路径) | ⛔ Won't fix |
136
+ | **无法确认真假的问题**(读 diff 不能确认会导致 bug) | ⛔ 不报 / Won't fix |
137
+ | **已处理过的问题**(已 Won't fix 或「已修复于 <hash>」,含历史 decisions 命中) | ⛔ 回复「已修复于 <hash>」或复用历史结论 |
138
+ | **重复评论**(同一问题被多视角/多轮重复提出) | 视为更高优先级(验证者重复 = 可能真问题),但仍按边界判定 |
139
+
140
+ - **判定一句话**:「这条不修,功能会不会错 / 数据会不会坏?」会 → 修;不会 → Won't fix。
141
+ - 每条该修的必须**引用具体 diff 行 + 触发路径**做证据,禁止只给结论。
142
+ - 宁可漏报一条真问题,不用十条口味问题去刷屏。
143
+
144
+ **处理完的每条都必须有落点**:修复 commit / Won't fix 回复 / 误报说明。不回复 bot 下轮必重提。
145
+ **同步落盘**:配合 `loop-review` 的 `save-review-decision.sh`,把判断结果写入 `.tmp-loop-review/decisions/`,跨会话去重(与 loop-review 共用同一收敛状态)。
146
+
126
147
  ### 4. Commit changes
127
148
 
128
149
  Use git-commit skill format. Functional fixes get separate commits, cosmetic fixes are batched:
@@ -225,6 +246,15 @@ Bots (Gemini, Codex, etc.) review every push. **Each push should go through revi
225
246
  - **Duplicate comments** -> treat as higher priority than their label (issue was already flagged before)
226
247
  - **Related comments** -> group and fix together when they share root cause or file context
227
248
 
249
+ ## Review boundary (edge, must follow)
250
+
251
+ - **ALWAYS** judge every comment by the project's review boundary (`rules/review-boundary.md` / AGENTS.base.md「审稿边界」章节), NEVER by the bot's severity label alone.
252
+ - **ALWAYS** fix comments that cause actual bug / data loss / security vulnerability with a reproducible trigger path, or that violate AGENTS.base.md / AGENTS.private.md ⚠️ rules.
253
+ - **ALWAYS** Won't fix style/refactor/defensive suggestions with no trigger path — do NOT fix them just because a bot flagged them.
254
+ - **ALWAYS** give every processed comment a resolution: fix commit, Won't fix, or false-alarm reply. A silent skip means the bot re-asks next round.
255
+ - **ALWAYS** record decisions to `.tmp-loop-review/decisions/` via `save-review-decision.sh` when doing loop-review; cross-session dedup depends on it.
256
+ - **NEVER** report an issue you cannot confirm from the diff. One real bug beats ten style suggestions.
257
+
228
258
  ## References
229
259
 
230
260
  - `references/severity_guide.md` - Severity detection patterns (Gemini badges, CodeRabbit emoji, Cursor comments, keyword fallback, related comments heuristics)
@@ -36,6 +36,15 @@ AI review bot 在**每次 push 到 PR 时自动触发新一轮 review**。因此
36
36
  → 若本轮没有任何值得修的新问题 → 结束;否则进入下一轮
37
37
  ```
38
38
 
39
+ ### 双模型交叉验证(第二视角)
40
+
41
+ 循环 review 的「交叉验证」是两个模型互相纠偏——Claude Opus 与 GPT-5.5 由同一个 `github-actions[bot]` 发布,各自独立审查同一份 diff,再用判断表逐条收敛。
42
+
43
+ | 视角 | 谁在审 | 特点 | 捕捉盲区 |
44
+ |---|---|---|---|
45
+ | 第一视角 | 🤖 Claude Opus(`github-actions[bot]` 发布) | 深度语义理解,长链路追踪 | —— |
46
+ | 第二视角 | 🤖 GPT-5.5(同 bot 交叉验证) | 不同训练分布,捕捉 Opus 的误报/漏报 | 单一模型自证式漏报 |
47
+
39
48
  ## 当前 PR
40
49
 
41
50
  !`gh pr view --json number,title,state -q '"PR #\(.number): \(.title) (\(.state))"' 2>/dev/null`
@@ -81,6 +90,37 @@ if [ -n "$LATEST" ] && [ "$LATEST" = "$CUR" ]; then echo "YES"; else echo "NO";
81
90
 
82
91
  ### 2. 汇总建议并逐条判断(核心)
83
92
 
93
+ #### 2.0 先查历史判断,跨轮去重(防止循环回跳的关键)
94
+
95
+ **收敛的记忆在 `decisions/` 目录,不在对话里。** 每次会话中断 / 上下文压缩后,上一轮的「已 Won't fix / 已修复」判断会丢,bot 下轮原样重提,循环就回跳了。因此**每轮开始,先把 bot 的建议与历史决策比对一遍**,命中「已处理过」的锚定到历史记录,不重复判断:
96
+
97
+ ```bash
98
+ # 先建立建议到「去重键」的映射,再逐个查历史
99
+ # 例:某条建议摘要是「并行部署失败被静默忽略」
100
+ bash "$(git rev-parse --show-toplevel)/.claude/skills/loop-review/scripts/query-review-decision.sh" 26 summary "并行部署失败"
101
+ # 命中 → 输出历史决策 JSON(decision=fixed/wontfix/false_alarm),直接复用结论,不再重复判断
102
+ # 未命中(exit 1)→ 这条是新的,进入下方判断框架
103
+ ```
104
+
105
+ **查询策略**:建议有稳定 ID(如 review id + 序号)用 `id` 精确查;没有稳定 ID 就按摘要子串 `summary` 模糊查。**判断历史里已有结论的建议,直接复用「理由 + 落点」**,禁止重新判断一遍(那正是循环回跳的根源)。
106
+
107
+ **每轮结束时,把判断结果落盘**(写入搭配脚本 `save-review-decision.sh`):
108
+
109
+ ```bash
110
+ bash "$(git rev-parse --show-toplevel)/.claude/skills/loop-review/scripts/save-review-decision.sh" '{
111
+ "pr": "26", "id": "opus-3fix", "severity": "warning",
112
+ "summary": "并行部署失败被静默忽略",
113
+ "decision": "fixed", "reason": "后台进程不受 set -e 管控,属真实风险",
114
+ "commit": "3fa21c0"
115
+ }'
116
+ ```
117
+
118
+ 决策 JSON 落盘到 `.tmp-loop-review/decisions/pr-<PR>.decisions.json`,跨会话、跨上下文压缩都能复用。**判断表既要展示给人看(终端表格),也要落盘给机器看(decisions/ JSON)**,两者都做。
119
+
120
+ ⚠️ **落盘的原则**:每条建议都必须有一个 decision(fixed / wontfix / false_alarm / deferred)落盘,没有落盘的建议 = 没有收敛。
121
+
122
+ #### 2.1 逐条判断(核心)
123
+
84
124
  解析两个模型的所有问题,**两模型报同一问题时合并为一条**(不要重复改两遍)。
85
125
 
86
126
  ⚠️ **严重度只是 bot 的标注,不是「该不该修」的依据**。真正决定改不改的是你读完代码后的判断。逐条按下面的决策框架:
@@ -95,10 +135,11 @@ if [ -n "$LATEST" ] && [ "$LATEST" = "$CUR" ]; then echo "YES"; else echo "NO";
95
135
  **判断依据(每条必须读真实代码,禁止凭 review 文字盲改)**:
96
136
 
97
137
  - 这是否是真实 bug / 真实风险?(读代码验证,不轻信 review 描述,也不轻信严重度标签)
98
- - 该建议是否符合本项目规则(AGENTS.base.md / AGENTS.private.md)?
138
+ - 是否违反本项目已固化规则(AGENTS.base.md / AGENTS.private.md 的 ⚠️ 铁律)?
99
139
  - 改动风险多大?会不会超出该建议本身的范围?(为了修一条问题去大改共享逻辑,本身可能引入新风险)
100
140
  - 是"必须修"还是"有则更好"?后者更接近口味,可拒绝。
101
141
  - 两模型是否一致指出?一致 → 优先级更高、基本可信。
142
+ - **涉及部署脚本 / 配置 / 品牌 / 环境归属类改动时,先跑外部事实核查**(「审查方法④」命令化):`bash "$(git rev-parse --show-toplevel)/scripts/check-facts.sh" <核查列表>`,把 PR 里的域名 / 子域 / 邮箱 / URL / 环境归属提取成核查列表跑一遍,FAIL 项按「必须报」处理,WARN 项人工确认。
102
143
 
103
144
  **每轮必须追问一句**:这条问题是不是上一轮已经判断过/处理过的?是 → 直接 Won't fix / 已修复,不重复改。**这一步是循环能收敛的关键。**
104
145
 
@@ -196,6 +237,8 @@ done
196
237
 
197
238
  - ⚠️ **每条建议必须读真实代码再判断,禁止盲从 review 文字、也禁止盲从严重度标签**。review 是 LLM 生成的,可能误报,严重度只是它的标注。
198
239
  - ⚠️ **循环收敛靠「值得修的都修完 + 不值得修的 Won't fix 切断」**,不是靠「严重度清零」。禁止用「无 Critical/Warning」当结束标准。
240
+ - ⚠️ **落点 = 落盘到 `.tmp-loop-review/decisions/`(`save-review-decision.sh`)+ 本轮 PR 回复(`gh pr comment` / inline reply),缺一不可。** 落盘让「会话中断后下轮还能识别已处理」,PR 回复让「bot 不再重提」。缺落盘:pickup 时历史丢失,等价于没收敛;缺回复:bot 下轮原样重提,循环无限。
241
+ - ⚠️ **每轮先查 `query-review-decision.sh`,命中「已处理过」的建议直接复用历史结论**,禁止重新判断一遍——那是循环回跳的根源。
199
242
  - ⚠️ **每条 review 建议必须有落点**(修复 commit / Won't fix / 误报回复),禁止静默跳过——不回复 bot 下轮必重提,循环无限。
200
243
  - ⚠️ **每轮先确认哪些是上一轮已处理过的**,一律不重复改,直接回复「已修复于 <hash>」。
201
244
  - ⚠️ **禁止使用 `[skip ci]` / `[skip review]`**——循环依赖「每次 push 触发 review」,跳过就断了。
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ # query-review-decision.sh —— 查询某条 review 建议是否已处理过(跨轮去重核心)。
3
+ #
4
+ # 用法:
5
+ # bash query-review-decision.sh <PR> <查询字段> [值]
6
+ #
7
+ # 查询字段:
8
+ # id 按建议唯一 ID 精确匹配(最可靠,下游写入时就要带上稳定 id)
9
+ # summary 按建议摘要子串匹配(模糊,用于没有稳定 id 的场景)
10
+ # count 输出该 PR 已记录的决策总数
11
+ #
12
+ # 返回值:
13
+ # 0 = 命中(已处理过),打印匹配的决策 JSON
14
+ # 1 = 未命中(新建议)
15
+ # 2 = 用法错误
16
+ #
17
+ # 配合 save-review-decision.sh 使用:
18
+ # 下轮启动时,对 bot 的每条建议先查一次;命中「已 fixed / wontfix / false_alarm」的
19
+ # 直接跳过,不再重复判断。这是「收敛靠文件记忆」的读取侧。
20
+
21
+ set -euo pipefail
22
+
23
+ DECISIONS_DIR="$(git rev-parse --show-toplevel)/.tmp-loop-review/decisions"
24
+
25
+ PR="${1:-}"
26
+ FIELD="${2:-}"
27
+ VALUE="${3:-}"
28
+
29
+ if [ -z "$PR" ] || [ -z "$FIELD" ]; then
30
+ echo "用法: bash query-review-decision.sh <PR> <id|summary|count> [值]" >&2
31
+ exit 2
32
+ fi
33
+
34
+ DECISION_FILE="$DECISIONS_DIR/pr-$PR.decisions.json"
35
+ if [ ! -f "$DECISION_FILE" ]; then
36
+ exit 1
37
+ fi
38
+
39
+ case "$FIELD" in
40
+ count)
41
+ jq -r '.decisions | length' "$DECISION_FILE"
42
+ exit 0
43
+ ;;
44
+ id)
45
+ HIT=$(jq --arg id "$VALUE" '.decisions[] | select(.id == $id)' "$DECISION_FILE" 2>/dev/null || true)
46
+ ;;
47
+ summary)
48
+ HIT=$(jq --arg s "$VALUE" '.decisions[] | select(.summary | contains($s))' "$DECISION_FILE" 2>/dev/null || true)
49
+ ;;
50
+ *)
51
+ echo "错误: 未知查询字段 $FIELD(支持 id / summary / count)" >&2
52
+ exit 2
53
+ ;;
54
+ esac
55
+
56
+ if [ -n "$HIT" ]; then
57
+ echo "$HIT"
58
+ exit 0
59
+ fi
60
+ exit 1
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env bash
2
+ # save-review-decision.sh —— 把「循环 review 每一轮的判断表」落盘为机器可读状态。
3
+ #
4
+ # 为什么需要它:
5
+ # loop-review 的收敛靠「每条建议都有落点」(修复 commit / Won't fix / 误报)。
6
+ # 但判断表只存在于对话上下文里——会话一中断、上下文一压缩,上一轮的
7
+ # 「这条已 Won't fix / 这条已修复于 <hash>」记忆就丢了。bot 下轮原样重提,
8
+ # 于是又进入「改了又错、错了再改」的循环。
9
+ # 本脚本把判断表落盘到 .tmp-loop-review/decisions/,下次启动先读历史判断
10
+ # 去重,让「收敛」从「靠对话记忆」升级为「靠文件记忆」。
11
+ #
12
+ # 用法:
13
+ # bash save-review-decision.sh <决策JSON> # 写入一条决策
14
+ #
15
+ # 决策 JSON 格式(每条建议一个对象):
16
+ # {
17
+ # "pr": "26", // PR 号
18
+ # "id": "opus-1", // 建议的唯一 ID(用于去重)
19
+ # "severity": "critical", // bot 标注,仅参考
20
+ # "summary": "建议摘要",
21
+ # "decision": "fixed|wontfix|false_alarm|deferred",
22
+ # "reason": "判断理由",
23
+ # "commit": "修复 commit hash(decision=fixed 时)"
24
+ # }
25
+ #
26
+ # 查询(配对脚本 query-review-decision.sh):
27
+ # 下轮启动时用 review_id / summary 查询是否已处理过,命中即跳过。
28
+
29
+ set -euo pipefail
30
+
31
+ DECISIONS_DIR="$(git rev-parse --show-toplevel)/.tmp-loop-review/decisions"
32
+ mkdir -p "$DECISIONS_DIR"
33
+
34
+ INPUT="${1:-}"
35
+ if [ -z "$INPUT" ]; then
36
+ echo "用法: bash save-review-decision.sh '<决策JSON>'" >&2
37
+ exit 1
38
+ fi
39
+
40
+ if ! jq -e . <<<"$INPUT" >/dev/null 2>&1; then
41
+ echo "错误: 输入不是合法 JSON" >&2
42
+ exit 1
43
+ fi
44
+
45
+ PR="$(jq -r '.pr // empty' <<<"$INPUT")"
46
+ if [ -z "$PR" ]; then
47
+ echo "错误: JSON 缺少 pr 字段" >&2
48
+ exit 1
49
+ fi
50
+
51
+ DECISION_FILE="$DECISIONS_DIR/pr-$PR.decisions.json"
52
+
53
+ # 追加/合并:已存在该 id 则覆盖,否则追加
54
+ if [ -f "$DECISION_FILE" ]; then
55
+ MERGED="$(jq --argjson new "$INPUT" '
56
+ .decisions as $old |
57
+ ([$old[] | select(.id != $new.id)] + [$new]) |
58
+ { pr: $new.pr, decisions: . }
59
+ ' "$DECISION_FILE")"
60
+ else
61
+ MERGED="$(jq -n --argjson new "$INPUT" '{ pr: $new.pr, decisions: [$new] }')"
62
+ fi
63
+
64
+ printf '%s\n' "$MERGED" > "$DECISION_FILE"
65
+ echo "✅ 已写入 $DECISION_FILE"