@ranger1/dx 0.1.33 → 0.1.35
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 +5 -4
- package/@opencode/agents/codex-reviewer.md +4 -4
- package/@opencode/agents/gemini-reviewer.md +4 -4
- package/@opencode/agents/pr-context.md +2 -2
- package/@opencode/agents/pr-fix.md +7 -6
- package/@opencode/agents/pr-precheck.md +1 -2
- package/@opencode/agents/pr-review-aggregate.md +9 -9
- package/@opencode/agents/pr_context.py +35 -3
- package/@opencode/agents/pr_precheck.py +41 -10
- package/@opencode/agents/pr_review_aggregate.py +135 -25
- package/@opencode/commands/pr-review-loop.md +10 -8
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -14,25 +14,26 @@ tools:
|
|
|
14
14
|
|
|
15
15
|
- `PR #<number>`
|
|
16
16
|
- `round: <number>`
|
|
17
|
+
- `runId: <string>`(必须透传,禁止自行生成)
|
|
17
18
|
- `contextFile: <filename>`
|
|
18
19
|
|
|
19
20
|
## 输出(强制)
|
|
20
21
|
|
|
21
22
|
只输出一行:
|
|
22
23
|
|
|
23
|
-
`reviewFile:
|
|
24
|
+
`reviewFile: ./.cache/<file>.md`
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
## 规则
|
|
27
28
|
|
|
28
29
|
- 默认已在 PR head 分支(可直接读工作区代码)
|
|
29
30
|
- 可用 `git`/`gh` 只读命令获取 diff/上下文
|
|
30
|
-
- 写入 reviewFile
|
|
31
|
+
- 写入 reviewFile:`./.cache/review-CLD-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
|
|
31
32
|
- findings id 必须以 `CLD-` 开头
|
|
32
33
|
|
|
33
34
|
## Cache 约定(强制)
|
|
34
|
-
|
|
35
|
-
-
|
|
35
|
+
|
|
36
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
36
37
|
|
|
37
38
|
## reviewFile 格式(强制)
|
|
38
39
|
|
|
@@ -15,26 +15,26 @@ tools:
|
|
|
15
15
|
|
|
16
16
|
- `PR #<number>`
|
|
17
17
|
- `round: <number>`
|
|
18
|
+
- `runId: <string>`(必须透传,禁止自行生成)
|
|
18
19
|
- `contextFile: <filename>`
|
|
19
20
|
|
|
20
21
|
## 输出(强制)
|
|
21
22
|
|
|
22
23
|
只输出一行:
|
|
23
24
|
|
|
24
|
-
`reviewFile:
|
|
25
|
+
`reviewFile: ./.cache/<file>.md`
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
## 规则
|
|
28
29
|
|
|
29
30
|
- 默认已在 PR head 分支(可直接读工作区代码)
|
|
30
31
|
- 可用 `git`/`gh` 只读命令获取 diff/上下文
|
|
31
|
-
- 写入 reviewFile
|
|
32
|
+
- 写入 reviewFile:`./.cache/review-CDX-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
|
|
32
33
|
- findings id 必须以 `CDX-` 开头
|
|
33
34
|
|
|
34
35
|
## Cache 约定(强制)
|
|
35
|
-
- 本流程所有中间文件都存放在 `~/.opencode/cache/`
|
|
36
|
-
- agent/命令之间仅传递文件名(basename),不传目录
|
|
37
36
|
|
|
37
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
38
38
|
|
|
39
39
|
## reviewFile 格式(强制)
|
|
40
40
|
|
|
@@ -14,26 +14,26 @@ tools:
|
|
|
14
14
|
|
|
15
15
|
- `PR #<number>`
|
|
16
16
|
- `round: <number>`
|
|
17
|
+
- `runId: <string>`(必须透传,禁止自行生成)
|
|
17
18
|
- `contextFile: <filename>`
|
|
18
19
|
|
|
19
20
|
## 输出(强制)
|
|
20
21
|
|
|
21
22
|
只输出一行:
|
|
22
23
|
|
|
23
|
-
`reviewFile:
|
|
24
|
+
`reviewFile: ./.cache/<file>.md`
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
## 规则
|
|
27
28
|
|
|
28
29
|
- 默认已在 PR head 分支(可直接读工作区代码)
|
|
29
30
|
- 可用 `git`/`gh` 只读命令获取 diff/上下文
|
|
30
|
-
- 写入 reviewFile
|
|
31
|
+
- 写入 reviewFile:`./.cache/review-GMN-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
|
|
31
32
|
- findings id 必须以 `GMN-` 开头
|
|
32
33
|
|
|
33
34
|
## Cache 约定(强制)
|
|
34
|
-
- 本流程所有中间文件都存放在 `~/.opencode/cache/`
|
|
35
|
-
- agent/命令之间仅传递文件名(basename),不传目录
|
|
36
35
|
|
|
36
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
37
37
|
|
|
38
38
|
## reviewFile 格式(强制)
|
|
39
39
|
|
|
@@ -20,11 +20,11 @@ tools:
|
|
|
20
20
|
|
|
21
21
|
## 输出(强制)
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
脚本会写入项目内 `./.cache/`,stdout 只输出单一 JSON(可 `JSON.parse()`)。
|
|
24
24
|
|
|
25
25
|
## Cache 约定(强制)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
28
28
|
|
|
29
29
|
## 调用脚本(强制)
|
|
30
30
|
|
|
@@ -26,13 +26,14 @@ tools:
|
|
|
26
26
|
## 前置条件
|
|
27
27
|
|
|
28
28
|
### Cache 约定(强制)
|
|
29
|
-
|
|
30
|
-
-
|
|
29
|
+
|
|
30
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
31
31
|
|
|
32
32
|
### 必需输入
|
|
33
33
|
|
|
34
34
|
- **PR 编号**:调用者必须在 prompt 中明确提供(如:`请修复 PR #123`)
|
|
35
|
-
- **
|
|
35
|
+
- **runId**:调用者必须在 prompt 中提供(必须透传,禁止自行生成)
|
|
36
|
+
- **fixFile**:调用者必须在 prompt 中提供问题清单文件路径(repo 相对路径,例:`./.cache/fix-...md`)(Structured Handoff)
|
|
36
37
|
|
|
37
38
|
### 失败快速退出
|
|
38
39
|
|
|
@@ -126,11 +127,11 @@ Round: 2
|
|
|
126
127
|
|
|
127
128
|
## 输出(强制)
|
|
128
129
|
|
|
129
|
-
|
|
130
|
+
写入:`./.cache/fix-report-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
|
|
130
131
|
|
|
131
132
|
最终只输出一行:
|
|
132
133
|
|
|
133
|
-
`fixReportFile:
|
|
134
|
+
`fixReportFile: ./.cache/<file>.md`
|
|
134
135
|
|
|
135
136
|
|
|
136
137
|
## fixReportFile 内容格式(强制)
|
|
@@ -172,4 +173,4 @@ Rejected: <n>
|
|
|
172
173
|
## 输出有效性保证
|
|
173
174
|
|
|
174
175
|
- fixReportFile 必须成功写入
|
|
175
|
-
- stdout 只能输出一行 `fixReportFile:
|
|
176
|
+
- stdout 只能输出一行 `fixReportFile: ./.cache/<file>.md`
|
|
@@ -10,8 +10,8 @@ tools:
|
|
|
10
10
|
# PR Review Aggregator
|
|
11
11
|
|
|
12
12
|
## Cache 约定(强制)
|
|
13
|
-
|
|
14
|
-
-
|
|
13
|
+
|
|
14
|
+
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
15
15
|
|
|
16
16
|
## 输入(两种模式)
|
|
17
17
|
|
|
@@ -20,15 +20,15 @@ tools:
|
|
|
20
20
|
- `PR #<number>`
|
|
21
21
|
- `round: <number>`
|
|
22
22
|
- `runId: <string>`
|
|
23
|
-
- `contextFile: <
|
|
24
|
-
- `reviewFile: <
|
|
23
|
+
- `contextFile: <path>`(例如:`./.cache/pr-context-...md`)
|
|
24
|
+
- `reviewFile: <path>`(三行,分别对应 CDX/CLD/GMN,例如:`./.cache/review-...md`)
|
|
25
25
|
|
|
26
26
|
### 模式 B:发布修复评论(基于 fixReportFile)
|
|
27
27
|
|
|
28
28
|
- `PR #<number>`
|
|
29
29
|
- `round: <number>`
|
|
30
30
|
- `runId: <string>`
|
|
31
|
-
- `fixReportFile: <
|
|
31
|
+
- `fixReportFile: <path>`(例如:`./.cache/fix-report-...md`)
|
|
32
32
|
|
|
33
33
|
示例:
|
|
34
34
|
|
|
@@ -36,10 +36,10 @@ tools:
|
|
|
36
36
|
PR #123
|
|
37
37
|
round: 1
|
|
38
38
|
runId: abcdef123456
|
|
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
|
|
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
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
## 执行方式(强制)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# PR context builder (deterministic).
|
|
3
3
|
# - Reads PR metadata + recent comments via gh
|
|
4
4
|
# - Reads changed files via git diff (no patch)
|
|
5
|
-
# - Writes Markdown context file to
|
|
5
|
+
# - Writes Markdown context file to project cache: ./.cache/
|
|
6
6
|
# - Prints exactly one JSON object to stdout
|
|
7
7
|
|
|
8
8
|
import argparse
|
|
@@ -16,7 +16,38 @@ from urllib.parse import urlparse
|
|
|
16
16
|
from pathlib import Path
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
def _repo_root():
|
|
20
|
+
# Prefer git top-level so the cache follows the current repo.
|
|
21
|
+
try:
|
|
22
|
+
p = subprocess.run(
|
|
23
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
24
|
+
stdout=subprocess.PIPE,
|
|
25
|
+
stderr=subprocess.DEVNULL,
|
|
26
|
+
text=True,
|
|
27
|
+
)
|
|
28
|
+
out = (p.stdout or "").strip()
|
|
29
|
+
if p.returncode == 0 and out:
|
|
30
|
+
return Path(out)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
return Path.cwd()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cache_dir(repo_root):
|
|
37
|
+
return (repo_root / ".cache").resolve()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _repo_relpath(repo_root, p):
|
|
41
|
+
try:
|
|
42
|
+
rel = p.resolve().relative_to(repo_root.resolve())
|
|
43
|
+
return "./" + rel.as_posix()
|
|
44
|
+
except Exception:
|
|
45
|
+
# Fallback to basename-only.
|
|
46
|
+
return os.path.basename(str(p))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
REPO_ROOT = _repo_root()
|
|
50
|
+
CACHE_DIR = _cache_dir(REPO_ROOT)
|
|
20
51
|
MARKER_SUBSTR = "<!-- pr-review-loop-marker"
|
|
21
52
|
|
|
22
53
|
|
|
@@ -252,7 +283,8 @@ def main(argv):
|
|
|
252
283
|
"repo": {"nameWithOwner": owner_repo},
|
|
253
284
|
"headOid": head_oid,
|
|
254
285
|
"existingMarkerCount": marker_count,
|
|
255
|
-
|
|
286
|
+
# Handoff should be repo-relative path so downstream agents can read it directly.
|
|
287
|
+
"contextFile": _repo_relpath(REPO_ROOT, context_path),
|
|
256
288
|
}
|
|
257
289
|
)
|
|
258
290
|
return 0
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
# - If mergeable == CONFLICTING: return {"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}
|
|
9
9
|
# - Run dx cache clear
|
|
10
10
|
# - Run dx lint and dx build all concurrently
|
|
11
|
-
# - On failure, write fixFile to
|
|
11
|
+
# - On failure, write fixFile to project cache: ./.cache/
|
|
12
|
+
# and return {"ok":false,"fixFile":"./.cache/..."}
|
|
12
13
|
# - On success, return {"ok":true}
|
|
13
14
|
#
|
|
14
15
|
# Stdout contract: print exactly one JSON object and nothing else.
|
|
15
16
|
|
|
16
17
|
import json
|
|
18
|
+
import os
|
|
17
19
|
import re
|
|
18
20
|
import secrets
|
|
19
21
|
import subprocess
|
|
@@ -92,6 +94,34 @@ def _detect_git_remote_host():
|
|
|
92
94
|
return None
|
|
93
95
|
|
|
94
96
|
|
|
97
|
+
def repo_root():
|
|
98
|
+
try:
|
|
99
|
+
p = subprocess.run(
|
|
100
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
101
|
+
stdout=subprocess.PIPE,
|
|
102
|
+
stderr=subprocess.DEVNULL,
|
|
103
|
+
text=True,
|
|
104
|
+
)
|
|
105
|
+
out = (p.stdout or "").strip()
|
|
106
|
+
if p.returncode == 0 and out:
|
|
107
|
+
return Path(out)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return Path.cwd()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cache_dir(repo_root_path):
|
|
114
|
+
return (repo_root_path / ".cache").resolve()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def repo_relpath(repo_root_path, p):
|
|
118
|
+
try:
|
|
119
|
+
rel = p.resolve().relative_to(repo_root_path.resolve())
|
|
120
|
+
return "./" + rel.as_posix()
|
|
121
|
+
except Exception:
|
|
122
|
+
return str(p)
|
|
123
|
+
|
|
124
|
+
|
|
95
125
|
def tail_text(path, max_lines=200, max_chars=12000):
|
|
96
126
|
try:
|
|
97
127
|
data = Path(path).read_text(errors="replace")
|
|
@@ -214,7 +244,8 @@ def main():
|
|
|
214
244
|
return 1
|
|
215
245
|
|
|
216
246
|
run_id = secrets.token_hex(4)
|
|
217
|
-
|
|
247
|
+
root = repo_root()
|
|
248
|
+
cache = cache_dir(root)
|
|
218
249
|
cache.mkdir(parents=True, exist_ok=True)
|
|
219
250
|
|
|
220
251
|
cache_clear_log = cache / f"precheck-pr{pr}-{run_id}-cache-clear.log"
|
|
@@ -227,9 +258,9 @@ def main():
|
|
|
227
258
|
"headRefName": head,
|
|
228
259
|
"baseRefName": base,
|
|
229
260
|
"mergeable": mergeable,
|
|
230
|
-
"cacheClearLog":
|
|
231
|
-
"lintLog":
|
|
232
|
-
"buildLog":
|
|
261
|
+
"cacheClearLog": repo_relpath(root, cache_clear_log),
|
|
262
|
+
"lintLog": repo_relpath(root, lint_log),
|
|
263
|
+
"buildLog": repo_relpath(root, build_log),
|
|
233
264
|
}, indent=2) + "\n")
|
|
234
265
|
|
|
235
266
|
cache_rc = run(["dx", "cache", "clear"], stdout_path=str(cache_clear_log), stderr_path=str(cache_clear_log))
|
|
@@ -245,10 +276,10 @@ def main():
|
|
|
245
276
|
"line": None,
|
|
246
277
|
"title": "dx cache clear failed",
|
|
247
278
|
"description": log_tail,
|
|
248
|
-
"suggestion": f"Open log: {cache_clear_log}",
|
|
279
|
+
"suggestion": f"Open log: {repo_relpath(root, cache_clear_log)}",
|
|
249
280
|
}]
|
|
250
281
|
write_fixfile(str(fix_path), issues)
|
|
251
|
-
print(json.dumps({"ok": False, "fixFile":
|
|
282
|
+
print(json.dumps({"ok": False, "fixFile": repo_relpath(root, fix_path)}))
|
|
252
283
|
return 1
|
|
253
284
|
|
|
254
285
|
import threading
|
|
@@ -285,7 +316,7 @@ def main():
|
|
|
285
316
|
"line": line,
|
|
286
317
|
"title": "dx lint failed",
|
|
287
318
|
"description": log_tail,
|
|
288
|
-
"suggestion": f"Open log: {lint_log}",
|
|
319
|
+
"suggestion": f"Open log: {repo_relpath(root, lint_log)}",
|
|
289
320
|
})
|
|
290
321
|
i += 1
|
|
291
322
|
if results.get("build", 1) != 0:
|
|
@@ -299,11 +330,11 @@ def main():
|
|
|
299
330
|
"line": line,
|
|
300
331
|
"title": "dx build all failed",
|
|
301
332
|
"description": log_tail,
|
|
302
|
-
"suggestion": f"Open log: {build_log}",
|
|
333
|
+
"suggestion": f"Open log: {repo_relpath(root, build_log)}",
|
|
303
334
|
})
|
|
304
335
|
|
|
305
336
|
write_fixfile(str(fix_path), issues)
|
|
306
|
-
print(json.dumps({"ok": False, "fixFile":
|
|
337
|
+
print(json.dumps({"ok": False, "fixFile": repo_relpath(root, fix_path)}))
|
|
307
338
|
return 1
|
|
308
339
|
|
|
309
340
|
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
# Deterministic PR review aggregation (script owns all rules).
|
|
3
3
|
#
|
|
4
4
|
# Workflow:
|
|
5
|
-
# - Mode A: read contextFile + reviewFile(s) from
|
|
5
|
+
# - Mode A: read contextFile + reviewFile(s) from project cache: ./.cache/, parse findings, merge duplicates,
|
|
6
6
|
# post a single PR comment, and optionally generate a fixFile for pr-fix.
|
|
7
7
|
# - Mode B: read fixReportFile from cache and post it as a PR comment.
|
|
8
8
|
#
|
|
9
9
|
# Input rules:
|
|
10
|
-
# - Callers pass
|
|
10
|
+
# - Callers should pass repo-relative paths (e.g. ./.cache/foo.md). For backward-compat, basenames are also accepted.
|
|
11
11
|
# - Duplicate groups come from LLM but are passed as an argument (NOT written to disk).
|
|
12
12
|
# - Prefer: --duplicate-groups-b64 <base64(json)>
|
|
13
13
|
# - Also supported: --duplicate-groups-json '<json>'
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
#
|
|
21
21
|
# PR comment rules:
|
|
22
22
|
# - Every comment must include marker: <!-- pr-review-loop-marker -->
|
|
23
|
-
# - Comment body must NOT contain local filesystem paths (this script scrubs
|
|
23
|
+
# - Comment body must NOT contain local filesystem paths (this script scrubs cache paths, $HOME, and repo absolute paths).
|
|
24
24
|
#
|
|
25
25
|
# fixFile rules:
|
|
26
26
|
# - fixFile includes ONLY P0/P1/P2 findings.
|
|
@@ -38,7 +38,76 @@ from pathlib import Path
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
MARKER = "<!-- pr-review-loop-marker -->"
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _repo_root():
|
|
44
|
+
try:
|
|
45
|
+
p = subprocess.run(
|
|
46
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
47
|
+
stdout=subprocess.PIPE,
|
|
48
|
+
stderr=subprocess.DEVNULL,
|
|
49
|
+
text=True,
|
|
50
|
+
)
|
|
51
|
+
out = (p.stdout or "").strip()
|
|
52
|
+
if p.returncode == 0 and out:
|
|
53
|
+
return Path(out)
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
return Path.cwd()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _cache_dir(repo_root):
|
|
60
|
+
return (repo_root / ".cache").resolve()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_safe_relpath(p):
|
|
64
|
+
if p.is_absolute():
|
|
65
|
+
return False
|
|
66
|
+
if any(part in ("..",) for part in p.parts):
|
|
67
|
+
return False
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_ref(repo_root, cache_dir, ref):
|
|
72
|
+
if not ref:
|
|
73
|
+
return None
|
|
74
|
+
s = str(ref).strip()
|
|
75
|
+
if not s:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# If caller already passes a repo-relative path like ./.cache/foo.md
|
|
79
|
+
looks_like_path = ("/" in s) or ("\\" in s) or s.startswith(".")
|
|
80
|
+
if looks_like_path:
|
|
81
|
+
p = Path(s)
|
|
82
|
+
if p.is_absolute():
|
|
83
|
+
# Only allow absolute paths under cache_dir.
|
|
84
|
+
try:
|
|
85
|
+
p2 = p.resolve()
|
|
86
|
+
p2.relative_to(cache_dir.resolve())
|
|
87
|
+
return p2
|
|
88
|
+
except Exception:
|
|
89
|
+
return None
|
|
90
|
+
if not _is_safe_relpath(p):
|
|
91
|
+
return None
|
|
92
|
+
return (repo_root / p).resolve()
|
|
93
|
+
|
|
94
|
+
# Backward-compat: accept basename-only.
|
|
95
|
+
b = _safe_basename(s)
|
|
96
|
+
if not b:
|
|
97
|
+
return None
|
|
98
|
+
return (cache_dir / b).resolve()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _repo_relpath(repo_root, p):
|
|
102
|
+
try:
|
|
103
|
+
rel = p.resolve().relative_to(repo_root.resolve())
|
|
104
|
+
return "./" + rel.as_posix()
|
|
105
|
+
except Exception:
|
|
106
|
+
return os.path.basename(str(p))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
REPO_ROOT = _repo_root()
|
|
110
|
+
CACHE_DIR = _cache_dir(REPO_ROOT)
|
|
42
111
|
|
|
43
112
|
|
|
44
113
|
def _json_out(obj):
|
|
@@ -57,14 +126,19 @@ def _safe_basename(name):
|
|
|
57
126
|
return base
|
|
58
127
|
|
|
59
128
|
|
|
60
|
-
def _read_cache_text(
|
|
61
|
-
p = CACHE_DIR
|
|
129
|
+
def _read_cache_text(ref):
|
|
130
|
+
p = _resolve_ref(REPO_ROOT, CACHE_DIR, ref)
|
|
131
|
+
if not p:
|
|
132
|
+
raise FileNotFoundError("INVALID_CACHE_REF")
|
|
62
133
|
return p.read_text(encoding="utf-8", errors="replace")
|
|
63
134
|
|
|
64
135
|
|
|
65
|
-
def _write_cache_text(
|
|
136
|
+
def _write_cache_text(ref, content):
|
|
137
|
+
p = _resolve_ref(REPO_ROOT, CACHE_DIR, ref)
|
|
138
|
+
if not p:
|
|
139
|
+
raise ValueError("INVALID_CACHE_REF")
|
|
66
140
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
67
|
-
p =
|
|
141
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
68
142
|
p.write_text(content, encoding="utf-8", newline="\n")
|
|
69
143
|
|
|
70
144
|
|
|
@@ -88,13 +162,21 @@ def _sanitize_for_comment(text):
|
|
|
88
162
|
text = str(text)
|
|
89
163
|
|
|
90
164
|
home = str(Path.home())
|
|
91
|
-
cache_abs = str(CACHE_DIR)
|
|
165
|
+
cache_abs = str(CACHE_DIR.resolve())
|
|
166
|
+
repo_abs = str(REPO_ROOT.resolve())
|
|
92
167
|
|
|
168
|
+
# Backward-compat scrub.
|
|
93
169
|
text = text.replace("~/.opencode/cache/", "[cache]/")
|
|
94
|
-
text = text.replace(cache_abs + "/", "[cache]/")
|
|
95
170
|
if home:
|
|
96
171
|
text = text.replace(home + "/.opencode/cache/", "[cache]/")
|
|
97
172
|
|
|
173
|
+
# New cache scrub.
|
|
174
|
+
text = text.replace(cache_abs + "/", "[cache]/")
|
|
175
|
+
|
|
176
|
+
# Avoid leaking absolute local repo paths.
|
|
177
|
+
if repo_abs:
|
|
178
|
+
text = text.replace(repo_abs + "/", "")
|
|
179
|
+
|
|
98
180
|
return text
|
|
99
181
|
|
|
100
182
|
|
|
@@ -236,8 +318,14 @@ def _counts(findings):
|
|
|
236
318
|
return c
|
|
237
319
|
|
|
238
320
|
|
|
239
|
-
def _post_pr_comment(pr_number,
|
|
240
|
-
|
|
321
|
+
def _post_pr_comment(pr_number, body_ref):
|
|
322
|
+
if isinstance(body_ref, Path):
|
|
323
|
+
p = body_ref
|
|
324
|
+
else:
|
|
325
|
+
p = _resolve_ref(REPO_ROOT, CACHE_DIR, body_ref)
|
|
326
|
+
if not p:
|
|
327
|
+
return False
|
|
328
|
+
body_path = str(p)
|
|
241
329
|
rc = subprocess.run(
|
|
242
330
|
["gh", "pr", "comment", str(pr_number), "--body-file", body_path],
|
|
243
331
|
stdout=subprocess.DEVNULL,
|
|
@@ -334,20 +422,25 @@ def main(argv):
|
|
|
334
422
|
round_num = args.round
|
|
335
423
|
run_id = str(args.run_id)
|
|
336
424
|
|
|
337
|
-
fix_report_file =
|
|
338
|
-
context_file =
|
|
425
|
+
fix_report_file = (args.fix_report_file or "").strip() or None
|
|
426
|
+
context_file = (args.context_file or "").strip() or None
|
|
339
427
|
review_files = []
|
|
340
428
|
for rf in args.review_file or []:
|
|
341
|
-
|
|
342
|
-
if
|
|
343
|
-
review_files.append(
|
|
429
|
+
s = (rf or "").strip()
|
|
430
|
+
if s:
|
|
431
|
+
review_files.append(s)
|
|
344
432
|
|
|
345
433
|
if fix_report_file:
|
|
434
|
+
fix_p = _resolve_ref(REPO_ROOT, CACHE_DIR, fix_report_file)
|
|
435
|
+
if not fix_p or not fix_p.exists():
|
|
436
|
+
_json_out({"error": "FIX_REPORT_FILE_NOT_FOUND"})
|
|
437
|
+
return 1
|
|
346
438
|
fix_md = _read_cache_text(fix_report_file)
|
|
347
439
|
body = _render_mode_b_comment(pr_number, round_num, run_id, fix_md)
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
440
|
+
body_basename = f"review-aggregate-fix-comment-pr{pr_number}-r{round_num}-{run_id}.md"
|
|
441
|
+
body_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / body_basename)
|
|
442
|
+
_write_cache_text(body_ref, body)
|
|
443
|
+
if not _post_pr_comment(pr_number, body_ref):
|
|
351
444
|
_json_out({"error": "GH_PR_COMMENT_FAILED"})
|
|
352
445
|
return 1
|
|
353
446
|
_json_out({"ok": True})
|
|
@@ -360,6 +453,21 @@ def main(argv):
|
|
|
360
453
|
_json_out({"error": "MISSING_REVIEW_FILES"})
|
|
361
454
|
return 1
|
|
362
455
|
|
|
456
|
+
ctx_p = _resolve_ref(REPO_ROOT, CACHE_DIR, context_file)
|
|
457
|
+
if not ctx_p or not ctx_p.exists():
|
|
458
|
+
_json_out({"error": "CONTEXT_FILE_NOT_FOUND"})
|
|
459
|
+
return 1
|
|
460
|
+
|
|
461
|
+
valid_review_files = []
|
|
462
|
+
for rf in review_files:
|
|
463
|
+
p = _resolve_ref(REPO_ROOT, CACHE_DIR, rf)
|
|
464
|
+
if p and p.exists():
|
|
465
|
+
valid_review_files.append(rf)
|
|
466
|
+
review_files = valid_review_files
|
|
467
|
+
if not review_files:
|
|
468
|
+
_json_out({"error": "REVIEW_FILES_NOT_FOUND"})
|
|
469
|
+
return 1
|
|
470
|
+
|
|
363
471
|
raw_reviews = []
|
|
364
472
|
all_findings = []
|
|
365
473
|
for rf in review_files:
|
|
@@ -377,9 +485,10 @@ def main(argv):
|
|
|
377
485
|
stop = len(must_fix) == 0
|
|
378
486
|
|
|
379
487
|
body = _render_mode_a_comment(pr_number, round_num, run_id, counts, must_fix, merged_map, raw_reviews)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
488
|
+
body_basename = f"review-aggregate-comment-pr{pr_number}-r{round_num}-{run_id}.md"
|
|
489
|
+
body_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / body_basename)
|
|
490
|
+
_write_cache_text(body_ref, body)
|
|
491
|
+
if not _post_pr_comment(pr_number, body_ref):
|
|
383
492
|
_json_out({"error": "GH_PR_COMMENT_FAILED"})
|
|
384
493
|
return 1
|
|
385
494
|
|
|
@@ -415,8 +524,9 @@ def main(argv):
|
|
|
415
524
|
lines.append(f" description: {desc}")
|
|
416
525
|
lines.append(f" suggestion: {sugg}")
|
|
417
526
|
|
|
418
|
-
|
|
419
|
-
|
|
527
|
+
fix_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / fix_file)
|
|
528
|
+
_write_cache_text(fix_ref, "\n".join(lines) + "\n")
|
|
529
|
+
_json_out({"stop": False, "fixFile": fix_ref})
|
|
420
530
|
return 0
|
|
421
531
|
|
|
422
532
|
|
|
@@ -12,8 +12,8 @@ agent: sisyphus
|
|
|
12
12
|
|
|
13
13
|
## Cache 约定(强制)
|
|
14
14
|
|
|
15
|
-
-
|
|
16
|
-
- agent
|
|
15
|
+
- 本流程所有中间文件都存放在项目内:`./.cache/`
|
|
16
|
+
- agent/命令之间传递**repo 相对路径**(例如:`./.cache/pr-context-...md`),不要只传 basename
|
|
17
17
|
|
|
18
18
|
## 固定 subagent_type(直接用 Task 调用,不要反复确认)
|
|
19
19
|
|
|
@@ -56,17 +56,18 @@ agent: sisyphus
|
|
|
56
56
|
- 每个 reviewer prompt 必须包含:
|
|
57
57
|
- `PR #{{PR_NUMBER}}`
|
|
58
58
|
- `round: <ROUND>`
|
|
59
|
-
- `
|
|
59
|
+
- `runId: <RUN_ID>`(来自 Step 1 的输出,必须透传,禁止自行生成)
|
|
60
|
+
- `contextFile: ./.cache/<file>.md`(来自 Step 1 的输出)
|
|
60
61
|
- reviewer 默认读 `contextFile`;必要时允许用 `git/gh` 只读命令拿 diff
|
|
61
62
|
- 忽略问题:1.格式化代码引起的噪音 2.已经lint检查以外的格式问题
|
|
62
63
|
- 特别关注: 逻辑、安全、性能、可维护性
|
|
63
64
|
- 同时要注意 pr 前面轮次的 修复和讨论,对于已经拒绝、已修复的问题不要反复的提出
|
|
64
65
|
- 同时也要注意fix的过程中有没有引入新的问题。
|
|
65
|
-
- 每个 reviewer 输出:`reviewFile:
|
|
66
|
+
- 每个 reviewer 输出:`reviewFile: ./.cache/<file>.md`(Markdown)
|
|
66
67
|
|
|
67
68
|
3. Task: `pr-review-aggregate`
|
|
68
69
|
|
|
69
|
-
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`contextFile:
|
|
70
|
+
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`contextFile: ./.cache/<file>.md`、三条 `reviewFile: ./.cache/<file>.md`
|
|
70
71
|
- 输出:`{"stop":true}` 或 `{"stop":false,"fixFile":"..."}`
|
|
71
72
|
- 若 `stop=true`:本轮结束并退出循环
|
|
72
73
|
|
|
@@ -75,14 +76,15 @@ agent: sisyphus
|
|
|
75
76
|
- prompt 必须包含:
|
|
76
77
|
- `PR #{{PR_NUMBER}}`
|
|
77
78
|
- `round: <ROUND>`
|
|
78
|
-
- `
|
|
79
|
+
- `runId: <RUN_ID>`(来自 Step 1 的输出,必须透传,禁止自行生成)
|
|
80
|
+
- `fixFile: ./.cache/<file>.md`
|
|
79
81
|
- 约定:`pr-fix` 对每个 findingId 单独 commit + push(一个 findingId 一个 commit),结束后再 `git push` 兜底
|
|
80
82
|
|
|
81
|
-
- pr-fix 输出:`fixReportFile:
|
|
83
|
+
- pr-fix 输出:`fixReportFile: ./.cache/<file>.md`(Markdown)
|
|
82
84
|
|
|
83
85
|
5. Task: `pr-review-aggregate`(发布修复评论)
|
|
84
86
|
|
|
85
|
-
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`fixReportFile:
|
|
87
|
+
- prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`fixReportFile: ./.cache/<file>.md`
|
|
86
88
|
- 输出:`{"ok":true}`
|
|
87
89
|
|
|
88
90
|
6. 下一轮
|