@ranger1/dx 0.1.29 → 0.1.31

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.
@@ -4,365 +4,49 @@ mode: subagent
4
4
  model: openai/gpt-5.2-codex
5
5
  temperature: 0.1
6
6
  tools:
7
- write: true
8
- edit: true
9
7
  bash: true
10
8
  ---
11
9
 
12
10
  # PR Precheck
13
11
 
12
+ ## Cache 约定(强制)
13
+ - 本流程所有中间文件都存放在 `~/.opencode/cache/`
14
+ - agent/命令之间仅传递文件名(basename),不传目录
15
+
16
+
14
17
  ## 输入(prompt 必须包含)
15
18
 
16
19
  - `PR #<number>`
17
20
 
18
- ## 一键脚本(推荐,省 token)
21
+ ## 一键脚本
19
22
 
20
- 把「环境/权限校验、PR 信息读取、checkout、base 分支 fetch、cache clear、lint+build、失败时写 fixFile、最终 JSON 输出」压到一次 `bash` 调用里执行。只有当脚本返回 merge 冲突相关错误时,才进入下面第 3 步做内容级合并。
21
-
22
- 注意:脚本会把所有命令输出写入 `~/.opencode/cache/`,stdout 只打印最终单一 JSON。
23
+ 脚本位置:`~/.opencode/agents/pr_precheck.py`
23
24
 
24
25
  ```bash
25
- # 用法:把 PR 号填到 PR_NUMBER
26
- PR_NUMBER=123 python3 - <<'PY'
27
- import json
28
- import os
29
- import re
30
- import secrets
31
- import subprocess
32
- from pathlib import Path
33
-
34
-
35
- def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
36
- if stdout_path and stderr_path and stdout_path == stderr_path:
37
- f = open(stdout_path, "wb")
38
- try:
39
- p = subprocess.run(cmd, cwd=cwd, stdout=f, stderr=f)
40
- return p.returncode
41
- finally:
42
- f.close()
43
-
44
- stdout_f = open(stdout_path, "wb") if stdout_path else subprocess.DEVNULL
45
- stderr_f = open(stderr_path, "wb") if stderr_path else subprocess.DEVNULL
46
- try:
47
- p = subprocess.run(cmd, cwd=cwd, stdout=stdout_f, stderr=stderr_f)
48
- return p.returncode
49
- finally:
50
- if stdout_path:
51
- stdout_f.close()
52
- if stderr_path:
53
- stderr_f.close()
54
-
55
-
56
- def run_capture(cmd, *, cwd=None):
57
- p = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
58
- return p.returncode, p.stdout, p.stderr
59
-
60
-
61
- def tail_text(path, max_lines=200, max_chars=12000):
62
- try:
63
- data = Path(path).read_text(errors="replace")
64
- except Exception:
65
- return "(failed to read log)"
66
- lines = data.splitlines()
67
- tail = "\n".join(lines[-max_lines:])
68
- if len(tail) > max_chars:
69
- tail = tail[-max_chars:]
70
- return tail
71
-
72
-
73
- def first_file_line(text):
74
- # Best-effort: match "path:line:col" or "path:line".
75
- for m in re.finditer(r"^([^\s:]+\.[a-zA-Z0-9]+):(\d+)(?::(\d+))?\b", text, flags=re.M):
76
- file = m.group(1)
77
- line = int(m.group(2))
78
- return file, line
79
- return None, None
80
-
81
-
82
- def write_fixfile(path, issues):
83
- p = Path(path)
84
- p.parent.mkdir(parents=True, exist_ok=True)
85
- # Minimal schema for pr-fix parser.
86
- out = ["## IssuesToFix", ""]
87
- for it in issues:
88
- out.append(f"- id: {it['id']}")
89
- out.append(f" priority: {it['priority']}")
90
- out.append(f" category: {it['category']}")
91
- out.append(f" file: {it['file']}")
92
- out.append(f" line: {it['line'] if it['line'] is not None else 'null'}")
93
- out.append(f" title: {it['title']}")
94
- # Keep as one line; caller should truncate.
95
- desc = it["description"].replace("\n", "\\n")
96
- sugg = it["suggestion"].replace("\n", "\\n")
97
- out.append(f" description: {desc}")
98
- out.append(f" suggestion: {sugg}")
99
- p.write_text("\n".join(out) + "\n")
100
-
101
-
102
- def main():
103
- pr = os.environ.get("PR_NUMBER", "").strip()
104
- if not pr.isdigit():
105
- print(json.dumps({"error": "PR_NUMBER_NOT_PROVIDED"}))
106
- return
107
-
108
- # Step 1: must be in git repo.
109
- rc, out, _ = run_capture(["git", "rev-parse", "--is-inside-work-tree"])
110
- if rc != 0 or out.strip() != "true":
111
- print(json.dumps({"error": "NOT_A_GIT_REPO"}))
112
- return
113
-
114
- # Step 1: gh auth.
115
- rc = run(["gh", "auth", "status"]) # devnull
116
- if rc != 0:
117
- print(json.dumps({"error": "GH_NOT_AUTHENTICATED"}))
118
- return
119
-
120
- # Read PR info.
121
- rc, pr_json, _ = run_capture(["gh", "pr", "view", pr, "--json", "headRefName,baseRefName,mergeable"])
122
- if rc != 0:
123
- print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
124
- return
125
- try:
126
- pr_info = json.loads(pr_json)
127
- except Exception:
128
- print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
129
- return
130
-
131
- head = (pr_info.get("headRefName") or "").strip()
132
- base = (pr_info.get("baseRefName") or "").strip()
133
- mergeable = (pr_info.get("mergeable") or "").strip()
134
-
135
- # Step 2: checkout PR branch if needed.
136
- rc, cur_branch, _ = run_capture(["git", "rev-parse", "--abbrev-ref", "HEAD"])
137
- if rc != 0:
138
- print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
139
- return
140
- if head and cur_branch.strip() != head:
141
- if run(["gh", "pr", "checkout", pr]) != 0:
142
- print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
143
- return
144
-
145
- # Step 3 pre-req: resolve base ref.
146
- if not base:
147
- rc, out, _ = run_capture(["gh", "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"])
148
- if rc == 0:
149
- base = out.strip()
150
- if not base:
151
- print(json.dumps({"error": "PR_BASE_REF_NOT_FOUND"}))
152
- return
153
-
154
- # Fetch base.
155
- if run(["git", "fetch", "origin", base]) != 0:
156
- ok = False
157
- for fallback in ("main", "master"):
158
- if fallback == base:
159
- continue
160
- if run(["git", "fetch", "origin", fallback]) == 0:
161
- base = fallback
162
- ok = True
163
- break
164
- if not ok:
165
- print(json.dumps({"error": "PR_BASE_REF_FETCH_FAILED"}))
166
- return
167
-
168
- # If mergeable reports conflict, ask agent to go to conflict-resolution step.
169
- if mergeable == "CONFLICTING":
170
- print(json.dumps({"error": "PR_MERGE_CONFLICTS_UNRESOLVED"}))
171
- return
172
-
173
- # Step 4: cache clear then lint + build (in parallel), logs to cache.
174
- run_id = secrets.token_hex(4)
175
- cache = Path.home() / ".opencode" / "cache"
176
- cache.mkdir(parents=True, exist_ok=True)
177
- cache_clear_log = cache / f"precheck-pr{pr}-{run_id}-cache-clear.log"
178
- lint_log = cache / f"precheck-pr{pr}-{run_id}-lint.log"
179
- build_log = cache / f"precheck-pr{pr}-{run_id}-build.log"
180
- meta_log = cache / f"precheck-pr{pr}-{run_id}-meta.json"
181
-
182
- # Keep meta for debugging (not printed to stdout).
183
- meta_log.write_text(json.dumps({
184
- "pr": int(pr),
185
- "headRefName": head,
186
- "baseRefName": base,
187
- "mergeable": mergeable,
188
- "cacheClearLog": str(cache_clear_log),
189
- "lintLog": str(lint_log),
190
- "buildLog": str(build_log),
191
- }, indent=2) + "\n")
192
-
193
- cache_rc = run(["dx", "cache", "clear"], stdout_path=str(cache_clear_log), stderr_path=str(cache_clear_log))
194
- if cache_rc != 0:
195
- fix_file = f"~/.opencode/cache/precheck-fix-pr{pr}-{run_id}.md"
196
- fix_path = str(cache / f"precheck-fix-pr{pr}-{run_id}.md")
197
- log_tail = tail_text(cache_clear_log)
198
- issues = [{
199
- "id": "PRE-001",
200
- "priority": "P1",
201
- "category": "quality",
202
- "file": "<unknown>",
203
- "line": None,
204
- "title": "dx cache clear failed",
205
- "description": log_tail,
206
- "suggestion": f"Open log: {cache_clear_log}",
207
- }]
208
- write_fixfile(fix_path, issues)
209
- print(json.dumps({"ok": False, "fixFile": fix_file}))
210
- return
211
-
212
- import threading
213
-
214
- results = {}
215
-
216
- def worker(name, cmd, log_path):
217
- results[name] = run(cmd, stdout_path=str(log_path), stderr_path=str(log_path))
218
-
219
- t1 = threading.Thread(target=worker, args=("lint", ["dx", "lint"], lint_log))
220
- t2 = threading.Thread(target=worker, args=("build", ["dx", "build", "all"], build_log))
221
- t1.start(); t2.start(); t1.join(); t2.join()
222
-
223
- if results.get("lint", 1) == 0 and results.get("build", 1) == 0:
224
- print(json.dumps({"ok": True}))
225
- return
226
-
227
- fix_file = f"~/.opencode/cache/precheck-fix-pr{pr}-{run_id}.md"
228
- fix_path = str(cache / f"precheck-fix-pr{pr}-{run_id}.md")
229
-
230
- issues = []
231
- i = 1
232
- if results.get("lint", 1) != 0:
233
- log_tail = tail_text(lint_log)
234
- file, line = first_file_line(log_tail)
235
- issues.append({
236
- "id": f"PRE-{i:03d}",
237
- "priority": "P1",
238
- "category": "lint",
239
- "file": file or "<unknown>",
240
- "line": line,
241
- "title": "dx lint failed",
242
- "description": log_tail,
243
- "suggestion": f"Open log: {lint_log}",
244
- })
245
- i += 1
246
- if results.get("build", 1) != 0:
247
- log_tail = tail_text(build_log)
248
- file, line = first_file_line(log_tail)
249
- issues.append({
250
- "id": f"PRE-{i:03d}",
251
- "priority": "P0",
252
- "category": "build",
253
- "file": file or "<unknown>",
254
- "line": line,
255
- "title": "dx build all failed",
256
- "description": log_tail,
257
- "suggestion": f"Open log: {build_log}",
258
- })
259
-
260
- write_fixfile(fix_path, issues)
261
- print(json.dumps({"ok": False, "fixFile": fix_file}))
262
-
263
-
264
- if __name__ == "__main__":
265
- try:
266
- main()
267
- except Exception:
268
- # Keep stdout contract.
269
- print(json.dumps({"error": "PRECHECK_SCRIPT_FAILED"}))
270
-
271
- PY
26
+ python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
272
27
  ```
273
28
 
274
- ## 要做的事(按顺序)
275
-
276
- 优先使用上面的「一键脚本」完成第 1/2/4/5 步;仅当脚本返回 merge 冲突相关错误时,再进入第 3 步进行内容级合并(完成后重跑脚本)。
29
+ ## 仅当出现 merge 冲突时怎么处理
277
30
 
278
- 1. 校验环境/权限
31
+ 当脚本输出 `{"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}` 时:
279
32
 
280
- - 必须在 git 仓库内,否则输出 `{"error":"NOT_A_GIT_REPO"}`
281
- - `gh auth status` 必须通过,否则输出 `{"error":"GH_NOT_AUTHENTICATED"}`
282
- - PR 必须存在且可访问,否则输出 `{"error":"PR_NOT_FOUND_OR_NO_ACCESS"}`
283
-
284
- 2. 切换到 PR 分支
285
-
286
- - 读取 PR 的 `headRefName`
287
- - 如果当前分支不是 headRefName:执行 `gh pr checkout <PR_NUMBER>`
288
- - 切换失败输出 `{"error":"PR_CHECKOUT_FAILED"}`
289
-
290
- 3. 检查 PR 合并冲突(如有则解决 + 提交 + 推送)
291
-
292
- - 读取 PR 的 `baseRefName` 与 `mergeable`
293
- - base 分支名必须兼容 `main`/`master`:
294
- - 优先使用 PR 返回的 `baseRefName`
295
- - 若 `baseRefName` 为空:用 `gh repo view --json defaultBranchRef -q .defaultBranchRef.name` 获取仓库默认分支
296
- - 若仍取不到:输出 `{"error":"PR_BASE_REF_NOT_FOUND"}`
297
- - 拉取 base 分支(后续 merge/affected build 都依赖):
298
- - `git fetch origin <baseRefName>`(若失败则按 `main/master` fallback 重试)
299
- - 仍失败:输出 `{"error":"PR_BASE_REF_FETCH_FAILED"}`
300
- - 若 `mergeable=CONFLICTING`(存在合并冲突):
301
- - 尝试把 base 合入当前 PR 分支(不 rebase、不 force push):
302
- - `git merge --no-ff --no-commit origin/<baseRefName>`
303
- - 若 merge 产生冲突文件(`git diff --name-only --diff-filter=U` 非空):
304
- - 先按文件类型做“低风险确定性策略”,再对剩余文件做“基于内容的智能合并”
305
- - 低风险确定性策略(示例,按仓库实际补充):
306
- - lockfiles(如 `pnpm-lock.yaml`/`package-lock.json`/`yarn.lock`):优先 `--theirs`(以 base 为准,减少依赖漂移)
307
- - 其余生成物/构建产物:能识别则同上(优先 base),识别不了不要瞎选
308
- - 对剩余冲突文件:
309
- - 读取包含冲突标记(`<<<<<<<`/`=======`/`>>>>>>>`)的文件内容
310
- - 基于代码语义进行合并:
311
- - 保证语法正确(JS/TS/JSON/YAML 等)
312
- - 变更尽量小
313
- - 若两边都合理:优先保留 PR 的业务逻辑,同时把 base 的必要改动(接口/字段/类型)合进去
314
- - 写回文件,确保冲突标记完全消除
315
- - 合并完成后必须验证:
316
- - `git diff --name-only --diff-filter=U` 为空
317
- - 不再存在冲突标记(允许用 `git grep -n '<<<<<<< ' -- <files>` 复核)
318
- - 若仍有未解决冲突:
319
- - `git merge --abort`
320
- - 输出 `{"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}`
321
- - 全部解决后:
322
- - `git add -A` 后 `git commit`(建议 message:`chore(pr #<PR_NUMBER>): resolve merge conflicts`)
323
- - `git push`(如无 upstream:`git push -u origin HEAD`)
324
- - 任一步失败则输出 `{"error":"PR_CONFLICT_AUTO_RESOLVE_FAILED"}`
325
- - 推送失败输出 `{"error":"PR_CONFLICT_PUSH_FAILED"}`
326
-
327
- 4. 预检:lint + build
328
-
329
- - 运行 `dx cache clear`
330
- - 运行 `dx lint`
331
- - 运行 `dx build all`
33
+ ```bash
34
+ # 1) 获取 base 分支名
35
+ gh pr view <PR_NUMBER> --json baseRefName --jq .baseRefName
332
36
 
333
- 5. lint/build 失败:生成 fixFile(Markdown)并返回失败
37
+ # 2) 拉取 base 并合并到当前 PR 分支(不 rebase、不 force push)
38
+ git fetch origin <baseRefName>
39
+ git merge --no-ff --no-commit origin/<baseRefName>
334
40
 
335
- - 写入前先 `mkdir -p "$HOME/.opencode/cache"`
336
- - fixFile 路径:`~/.opencode/cache/precheck-fix-pr<PR_NUMBER>-<RUN_ID>.md`
337
- - fixFile 只包含 `## IssuesToFix`
338
- - fixFile 格式(Markdown,最小字段集,供 `pr-fix` 解析):
41
+ # 3) 解决冲突后确认无未解决文件
42
+ git diff --name-only --diff-filter=U
43
+ git grep -n '<<<<<<< ' -- .
339
44
 
340
- ```md
341
- ## IssuesToFix
45
+ # 4) 提交并推送
46
+ git add -A
47
+ git commit -m "chore(pr #<PR_NUMBER>): resolve merge conflicts"
48
+ git push
342
49
 
343
- - id: PRE-001
344
- priority: P0|P1|P2|P3
345
- category: lint|build|quality
346
- file: <path>
347
- line: <number|null>
348
- title: <short>
349
- description: <error message>
350
- suggestion: <how to fix>
50
+ # 5) 重新运行预检脚本
51
+ python3 ~/.opencode/agents/pr_precheck.py <PR_NUMBER>
351
52
  ```
352
- - 每条 issue 的 `id` 必须以 `PRE-` 开头(例如 `PRE-001`)
353
- - 尽量从输出中提取 file/line;取不到则 `line: null`
354
-
355
- ## 输出(强制)
356
-
357
- 只输出一个 JSON 对象:
358
-
359
- - 通过:`{"ok":true}`
360
- - 需要修复:`{"ok":false,"fixFile":"~/.opencode/cache/precheck-fix-pr123-<RUN_ID>.md"}`
361
- - 环境/权限/分支问题:`{"error":"..."}`
362
-
363
- ## 规则
364
-
365
- - 不要输出任何时间字段
366
- - 不要在 stdout 输出 lint/build 的长日志(写入 fixFile 的 description 即可)
367
- - stdout 只能输出最终的单一 JSON 对象(其余命令输出请重定向到文件或丢弃)
368
- - 允许使用 bash 生成 runId(例如 8-12 位随机/sha1 截断均可)
@@ -4,13 +4,15 @@ mode: subagent
4
4
  model: openai/gpt-5.1-codex-mini
5
5
  temperature: 0.1
6
6
  tools:
7
- write: true
8
- edit: false
9
7
  bash: true
10
8
  ---
11
9
 
12
10
  # PR Review Aggregator
13
11
 
12
+ ## Cache 约定(强制)
13
+ - 本流程所有中间文件都存放在 `~/.opencode/cache/`
14
+ - agent/命令之间仅传递文件名(basename),不传目录
15
+
14
16
  ## 输入(两种模式)
15
17
 
16
18
  ### 模式 A:评审聚合 + 生成 fixFile + 发布评审评论
@@ -18,15 +20,15 @@ tools:
18
20
  - `PR #<number>`
19
21
  - `round: <number>`
20
22
  - `runId: <string>`
21
- - `contextFile: <path>`
22
- - `reviewFile: <path>`(三行,分别对应 CDX/CLD/GMN)
23
+ - `contextFile: <filename>`
24
+ - `reviewFile: <filename>`(三行,分别对应 CDX/CLD/GMN)
23
25
 
24
26
  ### 模式 B:发布修复评论(基于 fixReportFile)
25
27
 
26
28
  - `PR #<number>`
27
29
  - `round: <number>`
28
30
  - `runId: <string>`
29
- - `fixReportFile: <path>`
31
+ - `fixReportFile: <filename>`
30
32
 
31
33
  示例:
32
34
 
@@ -34,86 +36,48 @@ tools:
34
36
  PR #123
35
37
  round: 1
36
38
  runId: abcdef123456
37
- contextFile: ~/.opencode/cache/pr-context-pr123-r1-abcdef123456.md
38
- reviewFile: ~/.opencode/cache/review-CDX-pr123-r1-abcdef123456.md
39
- reviewFile: ~/.opencode/cache/review-CLD-pr123-r1-abcdef123456.md
40
- reviewFile: ~/.opencode/cache/review-GMN-pr123-r1-abcdef123456.md
39
+ contextFile: pr-context-pr123-r1-abcdef123456.md
40
+ reviewFile: review-CDX-pr123-r1-abcdef123456.md
41
+ reviewFile: review-CLD-pr123-r1-abcdef123456.md
42
+ reviewFile: review-GMN-pr123-r1-abcdef123456.md
41
43
  ```
42
44
 
43
- ## 你要做的事(按模式执行)
44
-
45
- 模式 A:
45
+ ## 执行方式(强制)
46
46
 
47
- 1. Read `contextFile` 与全部 `reviewFile`
48
- 2. 计算 needsFix(P0/P1/P2 任意 > 0)
49
- 3. 合并重复的问题为一个
50
- 4. 发布评审评论到 GitHub(gh pr comment),必须带 marker,评论正文必须内联包含:
51
- - Summary(P0/P1/P2/P3 统计)
52
- - P0/P1/P2 问题列表(至少 id/title/file:line/suggestion)
53
- - 三个 reviewer 的 reviewFile 原文(建议放到 <details>)
54
- 5. 若 needsFix:生成 `fixFile`(Markdown)并返回;否则发布“完成”评论并返回 stop
47
+ 所有确定性工作(解析/聚合/发评论/生成 fixFile/输出 JSON)都由 `~/.opencode/agents/pr_review_aggregate.py` 完成。
55
48
 
56
- 模式 B:
49
+ 你只做一件事:在模式 A 里用大模型判断哪些 finding 是重复的,并把重复分组作为参数传给脚本(不落盘)。
57
50
 
58
- 1. Read `fixReportFile`
59
- 2. 发布修复评论到 GitHub(gh pr comment),必须带 marker,评论正文必须内联 fixReportFile 内容
60
- 3. 输出 `{"ok":true}`
51
+ ## 重复分组(给大模型输出)
61
52
 
62
- ## 输出(强制)
63
-
64
- 模式 A:只输出一个 JSON 对象(很小):
53
+ 大模型只输出一行 JSON(不要代码块、不要解释文字、不要换行):
65
54
 
66
55
  ```json
67
- {
68
- "stop": false,
69
- "fixFile": "~/.opencode/cache/fix-pr123-r1-abcdef123456.md"
70
- }
56
+ {"duplicateGroups":[["CDX-001","CLD-003"],["GMN-002","CLD-005","CDX-004"]]}
71
57
  ```
72
58
 
73
- 字段:
74
-
75
- - `stop`: boolean
76
- - `fixFile`: string(仅 stop=false 时必须提供)
59
+ ## 调用脚本(强制)
77
60
 
78
- 模式 B:只输出:
61
+ 模式 A(带 reviewFile + 重复分组):
79
62
 
80
- ```json
81
- { "ok": true }
63
+ ```bash
64
+ python3 ~/.opencode/agents/pr_review_aggregate.py \
65
+ --pr <PR_NUMBER> \
66
+ --round <ROUND> \
67
+ --run-id <RUN_ID> \
68
+ --context-file <CONTEXT_FILE> \
69
+ --review-file <REVIEW_FILE_1> \
70
+ --review-file <REVIEW_FILE_2> \
71
+ --review-file <REVIEW_FILE_3> \
72
+ --duplicate-groups-b64 <BASE64_JSON>
82
73
  ```
83
74
 
84
- ## 规则
85
-
86
- - 不要输出 ReviewResult JSON
87
- - 不要校验/要求 reviewer 的 JSON
88
- - 不要生成/输出任何时间字段
89
- - `fixFile` 只包含 P0/P1/P2
90
- - `id` 必须使用 reviewer 给出的 findingId(例如 `CDX-001`),不要再改前缀
91
-
92
- ## 评论要求
93
-
94
- - 每条评论必须包含:`<!-- pr-review-loop-marker -->`
95
- - body 必须是最终字符串(用 `--body-file` 读取文件),不要依赖 heredoc 变量展开
96
- - 禁止在评论里出现本地缓存文件路径(例如 `~/.opencode/cache/...`)
97
-
98
- ## fixFile 输出路径与格式
99
-
100
- - 路径:`~/.opencode/cache/fix-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
101
- - 格式:
102
-
103
- ```md
104
- # Fix File
105
-
106
- PR: <PR_NUMBER>
107
- Round: <ROUND>
108
-
109
- ## IssuesToFix
75
+ 模式 B(带 fixReportFile):
110
76
 
111
- - id: CDX-001
112
- priority: P1
113
- category: quality
114
- file: <path>
115
- line: <number|null>
116
- title: <short>
117
- description: <text>
118
- suggestion: <text>
77
+ ```bash
78
+ python3 ~/.opencode/agents/pr_review_aggregate.py \
79
+ --pr <PR_NUMBER> \
80
+ --round <ROUND> \
81
+ --run-id <RUN_ID> \
82
+ --fix-report-file <FIX_REPORT_FILE>
119
83
  ```