@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.
- package/@opencode/agents/__pycache__/pr_context.cpython-314.pyc +0 -0
- package/@opencode/agents/__pycache__/pr_precheck.cpython-314.pyc +0 -0
- package/@opencode/agents/__pycache__/pr_review_aggregate.cpython-314.pyc +0 -0
- package/@opencode/agents/claude-reviewer.md +8 -3
- package/@opencode/agents/codex-reviewer.md +9 -3
- package/@opencode/agents/gemini-reviewer.md +9 -3
- package/@opencode/agents/pr-context.md +6 -166
- package/@opencode/agents/pr-fix.md +11 -6
- package/@opencode/agents/pr-precheck.md +25 -341
- package/@opencode/agents/pr-review-aggregate.md +36 -72
- package/@opencode/agents/pr_context.py +211 -0
- package/@opencode/agents/pr_precheck.py +256 -0
- package/@opencode/agents/pr_review_aggregate.py +428 -0
- package/@opencode/commands/doctor.md +0 -1
- package/@opencode/commands/pr-review-loop.md +11 -6
- package/lib/opencode-initial.js +32 -9
- package/package.json +6 -4
|
@@ -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
|
-
##
|
|
21
|
+
## 一键脚本
|
|
19
22
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
注意:脚本会把所有命令输出写入 `~/.opencode/cache/`,stdout 只打印最终单一 JSON。
|
|
23
|
+
脚本位置:`~/.opencode/agents/pr_precheck.py`
|
|
23
24
|
|
|
24
25
|
```bash
|
|
25
|
-
|
|
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
|
-
|
|
31
|
+
当脚本输出 `{"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}` 时:
|
|
279
32
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
37
|
+
# 2) 拉取 base 并合并到当前 PR 分支(不 rebase、不 force push)
|
|
38
|
+
git fetch origin <baseRefName>
|
|
39
|
+
git merge --no-ff --no-commit origin/<baseRefName>
|
|
334
40
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
-
|
|
338
|
-
- fixFile 格式(Markdown,最小字段集,供 `pr-fix` 解析):
|
|
41
|
+
# 3) 解决冲突后确认无未解决文件
|
|
42
|
+
git diff --name-only --diff-filter=U
|
|
43
|
+
git grep -n '<<<<<<< ' -- .
|
|
339
44
|
|
|
340
|
-
|
|
341
|
-
|
|
45
|
+
# 4) 提交并推送
|
|
46
|
+
git add -A
|
|
47
|
+
git commit -m "chore(pr #<PR_NUMBER>): resolve merge conflicts"
|
|
48
|
+
git push
|
|
342
49
|
|
|
343
|
-
|
|
344
|
-
|
|
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: <
|
|
22
|
-
- `reviewFile: <
|
|
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: <
|
|
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:
|
|
38
|
-
reviewFile:
|
|
39
|
-
reviewFile:
|
|
40
|
-
reviewFile:
|
|
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
|
-
|
|
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
|
-
|
|
49
|
+
你只做一件事:在模式 A 里用大模型判断哪些 finding 是重复的,并把重复分组作为参数传给脚本(不落盘)。
|
|
57
50
|
|
|
58
|
-
|
|
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
|
-
模式
|
|
61
|
+
模式 A(带 reviewFile + 重复分组):
|
|
79
62
|
|
|
80
|
-
```
|
|
81
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
```
|