@ranger1/dx 0.1.48 → 0.1.49

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.
@@ -14,7 +14,7 @@ tools:
14
14
 
15
15
  - `PR #<number>`
16
16
  - `round: <number>`
17
- - `runId: <string>`(必须透传,禁止自行生成)
17
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
18
18
  - `contextFile: <filename>`
19
19
 
20
20
  ## 输出(强制)
@@ -40,7 +40,11 @@ tools:
40
40
  - 若你的发现 priority 比原问题高 ≥2 级(如 P3→P1, P2→P0),可以升级质疑
41
41
  - 否则不再提出
42
42
 
43
- 判断"问题本质相同"时,比对 decision-log 中的 `essence` 字段与你发现的问题描述。
43
+ 4. **文件一致性**:
44
+ - 匹配 Decision Log 时,**必须检查 `file` 字段是否与当前 finding 的文件一致**。
45
+ - 若 decision-log 中的 `file` 与当前文件不一致(包括重命名、移动、删除),则**视为不同问题**,不进行 essence 匹配(即作为新问题处理)。
46
+ - 若 decision-log 条目缺少 `file` 字段,也视为不匹配。
47
+
44
48
 
45
49
  ### 禁止事项
46
50
  - ⛔ 不质疑已修复问题的实现方式(除非发现修复引入了新 bug)
@@ -15,7 +15,7 @@ tools:
15
15
 
16
16
  - `PR #<number>`
17
17
  - `round: <number>`
18
- - `runId: <string>`(必须透传,禁止自行生成)
18
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
19
19
  - `contextFile: <filename>`
20
20
 
21
21
  ## 输出(强制)
@@ -41,7 +41,11 @@ tools:
41
41
  - 若你的发现 priority 比原问题高 ≥2 级(如 P3→P1, P2→P0),可以升级质疑
42
42
  - 否则不再提出
43
43
 
44
- 判断"问题本质相同"时,比对 decision-log 中的 `essence` 字段与你发现的问题描述。
44
+ 4. **文件一致性**:
45
+ - 匹配 Decision Log 时,**必须检查 `file` 字段是否与当前 finding 的文件一致**。
46
+ - 若 decision-log 中的 `file` 与当前文件不一致(包括重命名、移动、删除),则**视为不同问题**,不进行 essence 匹配(即作为新问题处理)。
47
+ - 若 decision-log 条目缺少 `file` 字段,也视为不匹配。
48
+
45
49
 
46
50
  ### 禁止事项
47
51
  - ⛔ 不质疑已修复问题的实现方式(除非发现修复引入了新 bug)
@@ -14,7 +14,7 @@ tools:
14
14
 
15
15
  - `PR #<number>`
16
16
  - `round: <number>`
17
- - `runId: <string>`(必须透传,禁止自行生成)
17
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
18
18
  - `contextFile: <filename>`
19
19
 
20
20
  ## 输出(强制)
@@ -40,7 +40,11 @@ tools:
40
40
  - 若你的发现 priority 比原问题高 ≥2 级(如 P3→P1, P2→P0),可以升级质疑
41
41
  - 否则不再提出
42
42
 
43
- 判断"问题本质相同"时,比对 decision-log 中的 `essence` 字段与你发现的问题描述。
43
+ 4. **文件一致性**:
44
+ - 匹配 Decision Log 时,**必须检查 `file` 字段是否与当前 finding 的文件一致**。
45
+ - 若 decision-log 中的 `file` 与当前文件不一致(包括重命名、移动、删除),则**视为不同问题**,不进行 essence 匹配(即作为新问题处理)。
46
+ - 若 decision-log 条目缺少 `file` 字段,也视为不匹配。
47
+
44
48
 
45
49
  ### 禁止事项
46
50
  - ⛔ 不质疑已修复问题的实现方式(除非发现修复引入了新 bug)
@@ -17,7 +17,7 @@ Harvest all GitHub PR review feedback (humans + bots, including Copilot) and nor
17
17
 
18
18
  - `PR #<number>`
19
19
  - `round: <number>`
20
- - `runId: <string>`(必须透传,禁止自行生成)
20
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
21
21
  - `contextFile: <filename>`
22
22
 
23
23
  ## Cache 约定(强制)
@@ -110,6 +110,11 @@ python3 ~/.opencode/agents/gh_review_harvest.py \
110
110
  - 若你的发现 priority 比原问题高 ≥2 级(如 P3→P1, P2→P0),可以升级质疑
111
111
  - 否则不再提出
112
112
 
113
+ 3. **文件一致性**:
114
+ - 匹配 Decision Log 时,**必须检查 `file` 字段是否与当前 finding 的文件一致**。
115
+ - 若 decision-log 中的 `file` 与当前文件不一致(包括重命名、移动、删除),则**视为不同问题**,不进行 essence 匹配(即作为新问题处理)。
116
+ - 若 decision-log 条目缺少 `file` 字段,也视为不匹配。
117
+
113
118
  判断"问题本质相同"时,比对 decision-log 中的 `essence` 字段与你发现的问题描述。
114
119
 
115
120
  ### 禁止事项
@@ -18,6 +18,16 @@ tools:
18
18
  - PR 编号(如:`PR #123` 或 `prNumber: 123`)
19
19
  - round(如:`round: 1`;无则默认 1)
20
20
 
21
+ ## 唯一标识 runId(强制)
22
+
23
+ - 脚本必须生成全局唯一标识 `runId`:`<PR>-<ROUND>-<HEAD_SHORT>`
24
+ - 其中:
25
+ - `<PR>`:PR 编号
26
+ - `<ROUND>`:当前轮次
27
+ - `<HEAD_SHORT>`:`headOid` 的前 7 位(git rev-parse --short HEAD)
28
+ - `runId` 必须包含在返回的 JSON 中,供后续步骤使用。
29
+
30
+
21
31
  ## 输出(强制)
22
32
 
23
33
  脚本会写入项目内 `./.cache/`,stdout 只输出单一 JSON(可 `JSON.parse()`)。
@@ -25,6 +35,8 @@ tools:
25
35
  ## Cache 约定(强制)
26
36
 
27
37
  - 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
38
+ - 文件命名:`./.cache/pr-context-pr<PR>-r<ROUND>-<RUN_ID>.md`
39
+ - `RUN_ID` 格式必须为 `<PR>-<ROUND>-<HEAD_SHORT>`
28
40
 
29
41
  ## 调用脚本(强制)
30
42
 
@@ -32,7 +32,7 @@ tools:
32
32
  ### 必需输入
33
33
 
34
34
  - **PR 编号**:调用者必须在 prompt 中明确提供(如:`请修复 PR #123`)
35
- - **runId**:调用者必须在 prompt 中提供(必须透传,禁止自行生成)
35
+ - **runId**:调用者必须在 prompt 中提供(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
36
36
  - **fixFile**:调用者必须在 prompt 中提供问题清单文件路径(repo 相对路径,例:`./.cache/fix-...md`)(Structured Handoff)
37
37
 
38
38
  ### 失败快速退出
@@ -154,12 +154,14 @@ PR: <PR_NUMBER>
154
154
  ### Fixed
155
155
 
156
156
  - id: <FINDING_ID>
157
+ file: <FILE_PATH>
157
158
  commit: <SHA>
158
159
  essence: <问题本质的一句话描述>
159
160
 
160
161
  ### Rejected
161
162
 
162
163
  - id: <FINDING_ID>
164
+ file: <FILE_PATH>
163
165
  priority: <P0|P1|P2|P3>
164
166
  reason: <拒绝原因>
165
167
  essence: <问题本质的一句话描述>
@@ -170,6 +172,8 @@ PR: <PR_NUMBER>
170
172
  - 如果文件不存在:创建新文件,包含 `# Decision Log` 头、`PR: <PR_NUMBER>` 字段,以及第一个 `## Round <ROUND>` 段落
171
173
  - 如果文件存在:追加新的 `## Round <ROUND>` 段落到文件末尾
172
174
  - **禁止删除或覆盖历史轮次的记录**
175
+ - **file 字段**:必须记录问题所在的文件路径(repo 相对路径)。
176
+ - 对于 `pr-precheck` 产生的修复,`file` 字段可填 `__precheck__`。
173
177
 
174
178
  ### essence 字段要求
175
179
 
@@ -178,6 +182,7 @@ essence 是问题本质的一句话描述,用于后续轮次的智能匹配和
178
182
  - 简洁性:≤ 50 字
179
183
  - 问题导向:描述问题核心(而非具体代码位置、文件行号)
180
184
  - 可匹配性:后续轮次的 reviewer 能通过关键词匹配识别该问题
185
+ - **文件强绑定**:必须假设问题与当前文件强绑定(若文件重命名,视为不同问题)
181
186
 
182
187
  **示例对比:**
183
188
 
@@ -16,13 +16,14 @@ tools:
16
16
  ## 输入(prompt 必须包含)
17
17
 
18
18
  - `PR #<number>`
19
+ - `round: <number>`(默认 1)
19
20
 
20
21
  ## 一键脚本
21
22
 
22
23
  脚本位置:`~/.opencode/agents/pr_precheck.py`
23
24
 
24
25
  ```bash
25
- python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
26
+ python3 ~/.opencode/agents/pr_precheck.py --pr <PR_NUMBER> --round <ROUND>
26
27
  ```
27
28
 
28
29
  ## 脚本输出处理(强制)
@@ -30,6 +31,7 @@ python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
30
31
  - 脚本 stdout 只会输出**单一一行 JSON**(可 `JSON.parse()`)。
31
32
  - **成功时**:你的最终输出必须是**脚本 stdout 的那一行 JSON 原样内容**。
32
33
  - 典型返回:`{"ok":true}` 或 `{"ok":false,"fixFile":"..."}`
34
+ - **重要**:如果返回 `fixFile`,请使用基于 `headOid` 的标准 runId(`<PR>-<ROUND>-<HEAD_SHORT>`)来命名文件。
33
35
  - 禁止:解释/分析/补充文字
34
36
  - 禁止:代码块(```)
35
37
  - 禁止:前后空行
@@ -83,5 +85,5 @@ git commit -m "chore(pr #<PR_NUMBER>): resolve merge conflicts"
83
85
  git push
84
86
 
85
87
  # 5) 重新运行预检脚本
86
- python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
88
+ python3 ~/.opencode/agents/pr_precheck.py --pr <PR_NUMBER> --round <ROUND>
87
89
  ```
@@ -19,7 +19,7 @@ tools:
19
19
 
20
20
  - `PR #<number>`
21
21
  - `round: <number>`
22
- - `runId: <string>`
22
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
23
23
  - `contextFile: <path>`(例如:`./.cache/pr-context-...md`)
24
24
  - `reviewFile: <path>`(多行,1+ 条;例如:`./.cache/review-...md`)
25
25
 
@@ -27,7 +27,7 @@ tools:
27
27
 
28
28
  - `PR #<number>`
29
29
  - `round: <number>`
30
- - `runId: <string>`
30
+ - `runId: <string>`(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
31
31
  - `fixReportFile: <path>`(例如:`./.cache/fix-report-...md`)
32
32
 
33
33
  示例:
@@ -35,11 +35,11 @@ tools:
35
35
  ```text
36
36
  PR #123
37
37
  round: 1
38
- runId: abcdef123456
39
- contextFile: ./.cache/pr-context-pr123-r1-abcdef123456.md
40
- reviewFile: ./.cache/review-CDX-pr123-r1-abcdef123456.md
41
- reviewFile: ./.cache/review-CLD-pr123-r1-abcdef123456.md
42
- reviewFile: ./.cache/review-GMN-pr123-r1-abcdef123456.md
38
+ runId: 123-1-a1b2c3d
39
+ contextFile: ./.cache/pr-context-pr123-r1-123-1-a1b2c3d.md
40
+ reviewFile: ./.cache/review-CDX-pr123-r1-123-1-a1b2c3d.md
41
+ reviewFile: ./.cache/review-CLD-pr123-r1-123-1-a1b2c3d.md
42
+ reviewFile: ./.cache/review-GMN-pr123-r1-123-1-a1b2c3d.md
43
43
  ```
44
44
 
45
45
  ## 执行方式(强制)
@@ -65,11 +65,19 @@ runId: abcdef123456
65
65
 
66
66
  如果 decision-log(`./.cache/decision-log-pr<PR_NUMBER>.md`)存在,你需要基于 LLM 判断每个新 finding 与已决策问题的本质是否相同,从而生成 **escalation_groups** 参数。
67
67
 
68
+ **匹配原则**:
69
+ - **Essence 匹配**:对比 `essence` 字段与新 finding 的问题本质。
70
+ - **文件强绑定**:仅当 decision-log 条目的 `file` 与新 finding 的 `file` **完全一致**时才进行匹配。
71
+ - 若文件被重命名/删除/拆分,视为不同问题(为了稳定性,不处理复杂的 rename 映射)。
72
+ - 若 decision-log 条目缺少 `file` 字段(旧数据),则跳过匹配(视为不相关)。
73
+
68
74
  **流程**:
69
75
 
70
- 1. 读取 decision-log,提取已 rejected 问题的 `essence` 字段
71
- 2. 逐个新 finding,与所有已 rejected 问题的 essence 做语义比对(使用 LLM)
72
- 3. 判断是否"问题本质相同"(即便表述不同)
76
+ 1. 读取 decision-log,提取已 rejected 问题的 `essence` 和 `file` 字段
77
+ 2. 逐个新 finding,**先检查 file 是否匹配**
78
+ - 若 file 不匹配 → 视为 New Issue
79
+ - 若 file 匹配 → 继续对比 essence
80
+ 3. 若 essence 也匹配("问题本质相同"):
73
81
  4. 收集可升级的问题(重新质疑阈值):
74
82
  - **升级阈值**:优先级差距 ≥ 2 级
75
83
  - 例如:已 rejected P3 but finding 为 P1 → 可升级质疑
@@ -6,7 +6,6 @@
6
6
  # - Prints exactly one JSON object to stdout
7
7
 
8
8
  import argparse
9
- import hashlib
10
9
  import json
11
10
  import os
12
11
  import re
@@ -183,20 +182,28 @@ def main(argv):
183
182
  pr_number = int(args.pr)
184
183
  round_num = int(args.round)
185
184
 
185
+ def _json_err(error_code, extra=None):
186
+ obj = {"error": error_code, "prNumber": pr_number, "round": round_num}
187
+ if isinstance(extra, dict) and extra:
188
+ obj.update(extra)
189
+ _json_out(obj)
190
+
186
191
  # Preconditions: be in a git repo and gh is authenticated.
187
192
  rc, out, _ = _run_capture(["git", "rev-parse", "--is-inside-work-tree"])
188
193
  if rc != 0 or out.strip() != "true":
189
- _json_out({"error": "NOT_A_GIT_REPO"})
194
+ _json_err("NOT_A_GIT_REPO")
190
195
  return 1
191
196
 
192
197
  host = _detect_git_remote_host() or "github.com"
193
198
  rc, gh_out, gh_err = _run_capture(["gh", "auth", "status", "--hostname", host])
194
199
  if rc == 127:
195
- _json_out({
196
- "error": "GH_CLI_NOT_FOUND",
197
- "detail": "gh not found in PATH",
198
- "suggestion": "Install GitHub CLI: https://cli.github.com/",
199
- })
200
+ _json_err(
201
+ "GH_CLI_NOT_FOUND",
202
+ {
203
+ "detail": "gh not found in PATH",
204
+ "suggestion": "Install GitHub CLI: https://cli.github.com/",
205
+ },
206
+ )
200
207
  return 1
201
208
  if rc != 0:
202
209
  # If host detection is wrong, a global check might still succeed.
@@ -207,44 +214,51 @@ def main(argv):
207
214
  detail = (gh_err or gh_out or "").strip()
208
215
  if len(detail) > 4000:
209
216
  detail = detail[-4000:]
210
- _json_out({
211
- "error": "GH_NOT_AUTHENTICATED",
212
- "host": host,
213
- "detail": detail,
214
- "suggestion": f"Run: gh auth login --hostname {host}",
215
- })
217
+ _json_err(
218
+ "GH_NOT_AUTHENTICATED",
219
+ {
220
+ "host": host,
221
+ "detail": detail,
222
+ "suggestion": f"Run: gh auth login --hostname {host}",
223
+ },
224
+ )
216
225
  return 1
217
226
 
218
227
  rc, owner_repo, _ = _run_capture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
219
228
  owner_repo = owner_repo.strip() if rc == 0 else ""
220
229
  if not owner_repo:
221
- _json_out({"error": "REPO_NOT_FOUND"})
230
+ _json_err("REPO_NOT_FOUND")
222
231
  return 1
223
232
 
224
233
  fields = "number,url,title,body,isDraft,labels,baseRefName,headRefName,baseRefOid,headRefOid,comments"
225
234
  rc, pr_json, _ = _run_capture(["gh", "pr", "view", str(pr_number), "--repo", owner_repo, "--json", fields])
226
235
  if rc != 0:
227
- _json_out({"error": "PR_NOT_FOUND_OR_NO_ACCESS"})
236
+ _json_err("PR_NOT_FOUND_OR_NO_ACCESS")
228
237
  return 1
229
238
  try:
230
239
  pr = json.loads(pr_json)
231
240
  except Exception:
232
- _json_out({"error": "PR_NOT_FOUND_OR_NO_ACCESS"})
241
+ _json_err("PR_NOT_FOUND_OR_NO_ACCESS")
233
242
  return 1
234
243
 
235
244
  head_oid = (pr.get("headRefOid") or "").strip()
236
245
  base_oid = (pr.get("baseRefOid") or "").strip()
237
246
  base_ref = (pr.get("baseRefName") or "").strip()
247
+
248
+ if not head_oid:
249
+ _json_err("PR_HEAD_OID_NOT_FOUND")
250
+ return 1
251
+
252
+ head_short = head_oid[:7]
238
253
  if not base_ref:
239
254
  base_ref = _gh_default_branch(owner_repo)
240
255
  if not base_ref and not base_oid:
241
- _json_out({"error": "PR_BASE_REF_NOT_FOUND"})
256
+ _json_err("PR_BASE_REF_NOT_FOUND")
242
257
  return 1
243
258
  head_ref = (pr.get("headRefName") or "").strip()
244
259
  url = (pr.get("url") or "").strip()
245
260
 
246
- seed = f"{pr_number}:{round_num}:{head_oid}".encode("utf-8")
247
- run_id = hashlib.sha1(seed).hexdigest()[:12]
261
+ run_id = f"{pr_number}-{round_num}-{head_short}"
248
262
 
249
263
  if base_ref:
250
264
  _git_fetch_origin(base_ref)
@@ -277,6 +291,7 @@ def main(argv):
277
291
  fp.write(f"- RunId: {run_id}\n")
278
292
  fp.write(f"- Base: {base_ref}\n")
279
293
  fp.write(f"- Head: {head_ref}\n")
294
+ fp.write(f"- HeadShort: {head_short}\n")
280
295
  fp.write(f"- HeadOid: {head_oid}\n")
281
296
  fp.write(f"- Draft: {pr.get('isDraft')}\n")
282
297
  fp.write(f"- Labels: {', '.join(labels) if labels else '(none)'}\n")
@@ -317,6 +332,7 @@ def main(argv):
317
332
  "runId": run_id,
318
333
  "repo": {"nameWithOwner": owner_repo},
319
334
  "headOid": head_oid,
335
+ "headShort": head_short,
320
336
  "existingMarkerCount": marker_count,
321
337
  # Handoff should be repo-relative path so downstream agents can read it directly.
322
338
  "contextFile": _repo_relpath(REPO_ROOT, context_path),