@ranger1/dx 0.1.77 → 0.1.78
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/README.md +92 -31
- package/bin/dx.js +3 -3
- package/lib/cli/commands/stack.js +198 -237
- package/lib/cli/commands/start.js +0 -6
- package/lib/cli/dx-cli.js +10 -1
- package/lib/cli/help.js +4 -4
- package/lib/{opencode-initial.js → codex-initial.js} +3 -82
- package/package.json +1 -2
- package/@opencode/agents/__pycache__/gh_review_harvest.cpython-314.pyc +0 -0
- 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/__pycache__/test_pr_review_aggregate.cpython-314-pytest-9.0.2.pyc +0 -0
- package/@opencode/agents/__pycache__/test_pr_review_aggregate.cpython-314.pyc +0 -0
- package/@opencode/agents/claude-reviewer.md +0 -82
- package/@opencode/agents/codex-reviewer.md +0 -83
- package/@opencode/agents/gemini-reviewer.md +0 -82
- package/@opencode/agents/gh-thread-reviewer.md +0 -122
- package/@opencode/agents/gh_review_harvest.py +0 -292
- package/@opencode/agents/pr-context.md +0 -82
- package/@opencode/agents/pr-fix.md +0 -243
- package/@opencode/agents/pr-precheck.md +0 -89
- package/@opencode/agents/pr-review-aggregate.md +0 -151
- package/@opencode/agents/pr_context.py +0 -351
- package/@opencode/agents/pr_precheck.py +0 -505
- package/@opencode/agents/pr_review_aggregate.py +0 -868
- package/@opencode/agents/test_pr_review_aggregate.py +0 -701
- package/@opencode/commands/doctor.md +0 -271
- package/@opencode/commands/git-commit-and-pr.md +0 -282
- package/@opencode/commands/git-release.md +0 -642
- package/@opencode/commands/oh_attach.json +0 -92
- package/@opencode/commands/opencode_attach.json +0 -29
- package/@opencode/commands/opencode_attach.py +0 -142
- package/@opencode/commands/pr-review-loop.md +0 -211
|
@@ -1,292 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
# Deterministic GitHub PR review harvester.
|
|
4
|
-
#
|
|
5
|
-
# - Fetches inline review threads via GraphQL (reviewThreads) with pagination.
|
|
6
|
-
# - Fetches PR reviews and PR issue comments via REST (gh api) with pagination.
|
|
7
|
-
# - Writes a raw JSON file into project cache: ./.cache/
|
|
8
|
-
# - Prints exactly one JSON object to stdout: {"rawFile":"./.cache/...json"}
|
|
9
|
-
|
|
10
|
-
import argparse
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
from datetime import datetime, timezone
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
MARKER_SUBSTR = "<!-- pr-review-loop-marker"
|
|
20
|
-
|
|
21
|
-
def _repo_root():
|
|
22
|
-
try:
|
|
23
|
-
p = subprocess.run(
|
|
24
|
-
["git", "rev-parse", "--show-toplevel"],
|
|
25
|
-
stdout=subprocess.PIPE,
|
|
26
|
-
stderr=subprocess.DEVNULL,
|
|
27
|
-
text=True,
|
|
28
|
-
)
|
|
29
|
-
out = (p.stdout or "").strip()
|
|
30
|
-
if p.returncode == 0 and out:
|
|
31
|
-
return Path(out)
|
|
32
|
-
except Exception:
|
|
33
|
-
pass
|
|
34
|
-
return Path.cwd()
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def _cache_dir(repo_root):
|
|
38
|
-
return (repo_root / ".cache").resolve()
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _repo_relpath(repo_root, p):
|
|
42
|
-
try:
|
|
43
|
-
rel = p.resolve().relative_to(repo_root.resolve())
|
|
44
|
-
return "./" + rel.as_posix()
|
|
45
|
-
except Exception:
|
|
46
|
-
return os.path.basename(str(p))
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
REPO_ROOT = _repo_root()
|
|
50
|
-
CACHE_DIR = _cache_dir(REPO_ROOT)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _json_out(obj):
|
|
54
|
-
sys.stdout.write(json.dumps(obj, ensure_ascii=True))
|
|
55
|
-
sys.stdout.write("\n")
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def _run_capture(cmd):
|
|
59
|
-
try:
|
|
60
|
-
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
61
|
-
return p.returncode, p.stdout, p.stderr
|
|
62
|
-
except FileNotFoundError as e:
|
|
63
|
-
return 127, "", str(e)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def _has_loop_marker(text):
|
|
67
|
-
if not text:
|
|
68
|
-
return False
|
|
69
|
-
try:
|
|
70
|
-
return MARKER_SUBSTR in str(text)
|
|
71
|
-
except Exception:
|
|
72
|
-
return False
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _require_gh_auth():
|
|
76
|
-
rc, out, err = _run_capture(["gh", "auth", "status"])
|
|
77
|
-
if rc == 127:
|
|
78
|
-
return False, "GH_CLI_NOT_FOUND", "gh not found in PATH"
|
|
79
|
-
if rc != 0:
|
|
80
|
-
detail = (err or out or "").strip()
|
|
81
|
-
if len(detail) > 4000:
|
|
82
|
-
detail = detail[-4000:]
|
|
83
|
-
return False, "GH_NOT_AUTHENTICATED", detail
|
|
84
|
-
return True, None, None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _resolve_owner_repo(explicit_repo):
|
|
88
|
-
if explicit_repo:
|
|
89
|
-
s = str(explicit_repo).strip()
|
|
90
|
-
if s and "/" in s:
|
|
91
|
-
return s
|
|
92
|
-
rc, out, _ = _run_capture(["gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"])
|
|
93
|
-
owner_repo = out.strip() if rc == 0 else ""
|
|
94
|
-
return owner_repo or None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _gh_api_json(args):
|
|
98
|
-
rc, out, err = _run_capture(["gh", "api"] + args)
|
|
99
|
-
if rc != 0:
|
|
100
|
-
raise RuntimeError(f"GH_API_FAILED: {(err or out or '').strip()}")
|
|
101
|
-
try:
|
|
102
|
-
return json.loads(out or "null")
|
|
103
|
-
except Exception:
|
|
104
|
-
raise RuntimeError("GH_API_JSON_PARSE_FAILED")
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _gh_api_graphql(query, variables):
|
|
108
|
-
cmd = ["gh", "api", "graphql", "-f", f"query={query}"]
|
|
109
|
-
for k, v in (variables or {}).items():
|
|
110
|
-
if isinstance(v, int):
|
|
111
|
-
cmd.extend(["-F", f"{k}={v}"])
|
|
112
|
-
elif v is None:
|
|
113
|
-
cmd.extend(["-f", f"{k}="])
|
|
114
|
-
else:
|
|
115
|
-
cmd.extend(["-f", f"{k}={v}"])
|
|
116
|
-
|
|
117
|
-
rc, out, err = _run_capture(cmd)
|
|
118
|
-
if rc != 0:
|
|
119
|
-
raise RuntimeError(f"GH_GRAPHQL_FAILED: {(err or out or '').strip()}")
|
|
120
|
-
try:
|
|
121
|
-
return json.loads(out or "null")
|
|
122
|
-
except Exception:
|
|
123
|
-
raise RuntimeError("GH_GRAPHQL_JSON_PARSE_FAILED")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _flatten_threads(gql_data):
|
|
127
|
-
threads = []
|
|
128
|
-
pr = (((gql_data or {}).get("data") or {}).get("repository") or {}).get("pullRequest") or {}
|
|
129
|
-
conn = pr.get("reviewThreads") or {}
|
|
130
|
-
nodes = conn.get("nodes") or []
|
|
131
|
-
for t in nodes:
|
|
132
|
-
is_resolved = bool((t or {}).get("isResolved"))
|
|
133
|
-
is_outdated = bool((t or {}).get("isOutdated"))
|
|
134
|
-
if is_resolved or is_outdated:
|
|
135
|
-
continue
|
|
136
|
-
comments_conn = (t or {}).get("comments") or {}
|
|
137
|
-
comments_nodes = comments_conn.get("nodes") or []
|
|
138
|
-
comments = []
|
|
139
|
-
for c in comments_nodes:
|
|
140
|
-
body = (c or {}).get("body") or ""
|
|
141
|
-
body_text = (c or {}).get("bodyText") or ""
|
|
142
|
-
if _has_loop_marker(body) or _has_loop_marker(body_text):
|
|
143
|
-
continue
|
|
144
|
-
author = (c or {}).get("author") or {}
|
|
145
|
-
comments.append(
|
|
146
|
-
{
|
|
147
|
-
"id": (c or {}).get("id"),
|
|
148
|
-
"databaseId": (c or {}).get("databaseId"),
|
|
149
|
-
"url": (c or {}).get("url"),
|
|
150
|
-
"author": {
|
|
151
|
-
"login": author.get("login"),
|
|
152
|
-
"type": author.get("__typename"),
|
|
153
|
-
},
|
|
154
|
-
"body": body,
|
|
155
|
-
"bodyText": body_text,
|
|
156
|
-
"createdAt": (c or {}).get("createdAt"),
|
|
157
|
-
"updatedAt": (c or {}).get("updatedAt"),
|
|
158
|
-
}
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
if not comments:
|
|
162
|
-
continue
|
|
163
|
-
threads.append(
|
|
164
|
-
{
|
|
165
|
-
"id": (t or {}).get("id"),
|
|
166
|
-
"isResolved": False,
|
|
167
|
-
"isOutdated": False,
|
|
168
|
-
"path": (t or {}).get("path"),
|
|
169
|
-
"line": (t or {}).get("line"),
|
|
170
|
-
"originalLine": (t or {}).get("originalLine"),
|
|
171
|
-
"startLine": (t or {}).get("startLine"),
|
|
172
|
-
"originalStartLine": (t or {}).get("originalStartLine"),
|
|
173
|
-
"comments": comments,
|
|
174
|
-
}
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
page_info = conn.get("pageInfo") or {}
|
|
178
|
-
return threads, {
|
|
179
|
-
"hasNextPage": bool(page_info.get("hasNextPage")),
|
|
180
|
-
"endCursor": page_info.get("endCursor"),
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def _fetch_all_review_threads(owner, repo, pr_number):
|
|
185
|
-
query = (
|
|
186
|
-
"query($owner:String!,$repo:String!,$prNumber:Int!,$after:String){"
|
|
187
|
-
"repository(owner:$owner,name:$repo){"
|
|
188
|
-
"pullRequest(number:$prNumber){"
|
|
189
|
-
"reviewThreads(first:100,after:$after){"
|
|
190
|
-
"pageInfo{hasNextPage endCursor}"
|
|
191
|
-
"nodes{"
|
|
192
|
-
"id isResolved isOutdated path line originalLine startLine originalStartLine "
|
|
193
|
-
"comments(first:100){nodes{"
|
|
194
|
-
"id databaseId url body bodyText createdAt updatedAt author{login __typename}"
|
|
195
|
-
"}}"
|
|
196
|
-
"}"
|
|
197
|
-
"}"
|
|
198
|
-
"}"
|
|
199
|
-
"}"
|
|
200
|
-
"}"
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
after = None
|
|
204
|
-
all_threads = []
|
|
205
|
-
while True:
|
|
206
|
-
data = _gh_api_graphql(query, {"owner": owner, "repo": repo, "prNumber": pr_number, "after": after})
|
|
207
|
-
threads, page = _flatten_threads(data)
|
|
208
|
-
all_threads.extend(threads)
|
|
209
|
-
if not page.get("hasNextPage"):
|
|
210
|
-
break
|
|
211
|
-
after = page.get("endCursor")
|
|
212
|
-
if not after:
|
|
213
|
-
break
|
|
214
|
-
return all_threads
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def main(argv):
|
|
218
|
-
class _ArgParser(argparse.ArgumentParser):
|
|
219
|
-
def error(self, message):
|
|
220
|
-
raise ValueError(message)
|
|
221
|
-
|
|
222
|
-
parser = _ArgParser(add_help=False)
|
|
223
|
-
parser.add_argument("--pr", type=int, required=True)
|
|
224
|
-
parser.add_argument("--round", type=int, default=1)
|
|
225
|
-
parser.add_argument("--run-id", required=True)
|
|
226
|
-
parser.add_argument("--repo")
|
|
227
|
-
|
|
228
|
-
try:
|
|
229
|
-
args = parser.parse_args(argv)
|
|
230
|
-
except ValueError:
|
|
231
|
-
_json_out({"error": "INVALID_ARGS"})
|
|
232
|
-
return 2
|
|
233
|
-
|
|
234
|
-
ok, code, detail = _require_gh_auth()
|
|
235
|
-
if not ok:
|
|
236
|
-
_json_out({"error": code, "detail": detail})
|
|
237
|
-
return 1
|
|
238
|
-
|
|
239
|
-
owner_repo = _resolve_owner_repo(args.repo)
|
|
240
|
-
if not owner_repo:
|
|
241
|
-
_json_out({"error": "REPO_NOT_FOUND"})
|
|
242
|
-
return 1
|
|
243
|
-
if "/" not in owner_repo:
|
|
244
|
-
_json_out({"error": "INVALID_REPO"})
|
|
245
|
-
return 1
|
|
246
|
-
|
|
247
|
-
owner, repo = owner_repo.split("/", 1)
|
|
248
|
-
pr_number = int(args.pr)
|
|
249
|
-
round_num = int(args.round)
|
|
250
|
-
run_id = str(args.run_id).strip()
|
|
251
|
-
if not run_id:
|
|
252
|
-
_json_out({"error": "MISSING_RUN_ID"})
|
|
253
|
-
return 1
|
|
254
|
-
|
|
255
|
-
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
256
|
-
raw_basename = f"gh-review-raw-pr{pr_number}-r{round_num}-{run_id}.json"
|
|
257
|
-
raw_path = CACHE_DIR / raw_basename
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
threads = _fetch_all_review_threads(owner, repo, pr_number)
|
|
261
|
-
|
|
262
|
-
reviews = _gh_api_json([f"repos/{owner_repo}/pulls/{pr_number}/reviews", "--paginate"])
|
|
263
|
-
issue_comments = _gh_api_json([f"repos/{owner_repo}/issues/{pr_number}/comments", "--paginate"])
|
|
264
|
-
|
|
265
|
-
if isinstance(reviews, list):
|
|
266
|
-
reviews = [r for r in reviews if not _has_loop_marker((r or {}).get("body") or "")]
|
|
267
|
-
if isinstance(issue_comments, list):
|
|
268
|
-
issue_comments = [c for c in issue_comments if not _has_loop_marker((c or {}).get("body") or "")]
|
|
269
|
-
|
|
270
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
271
|
-
payload = {
|
|
272
|
-
"repo": owner_repo,
|
|
273
|
-
"pr": pr_number,
|
|
274
|
-
"round": round_num,
|
|
275
|
-
"runId": run_id,
|
|
276
|
-
"generatedAt": now,
|
|
277
|
-
"reviewThreads": threads,
|
|
278
|
-
"reviews": reviews if isinstance(reviews, list) else [],
|
|
279
|
-
"issueComments": issue_comments if isinstance(issue_comments, list) else [],
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
raw_path.write_text(json.dumps(payload, ensure_ascii=True), encoding="utf-8", newline="\n")
|
|
283
|
-
except Exception as e:
|
|
284
|
-
_json_out({"error": "HARVEST_FAILED", "detail": str(e)[:800]})
|
|
285
|
-
return 1
|
|
286
|
-
|
|
287
|
-
_json_out({"rawFile": _repo_relpath(REPO_ROOT, raw_path)})
|
|
288
|
-
return 0
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if __name__ == "__main__":
|
|
292
|
-
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: build PR context file
|
|
3
|
-
mode: subagent
|
|
4
|
-
model: openai/gpt-5.3-codex
|
|
5
|
-
temperature: 0.1
|
|
6
|
-
tools:
|
|
7
|
-
bash: true
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# PR Context Builder
|
|
11
|
-
|
|
12
|
-
为 PR Review Loop 构建上下文文件(Markdown)。确定性工作由脚本完成。
|
|
13
|
-
|
|
14
|
-
## 输入要求(强制)
|
|
15
|
-
|
|
16
|
-
调用者必须在 prompt 中明确提供:
|
|
17
|
-
|
|
18
|
-
- PR 编号(如:`PR #123` 或 `prNumber: 123`)
|
|
19
|
-
- round(如:`round: 1`;无则默认 1)
|
|
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
|
-
|
|
31
|
-
## 输出(强制)
|
|
32
|
-
|
|
33
|
-
脚本会写入项目内 `./.cache/`,stdout 只输出单一 JSON(可 `JSON.parse()`)。
|
|
34
|
-
|
|
35
|
-
## Cache 约定(强制)
|
|
36
|
-
|
|
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>`
|
|
40
|
-
|
|
41
|
-
## 调用脚本(强制)
|
|
42
|
-
|
|
43
|
-
脚本位置:`~/.opencode/agents/pr_context.py`
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
python3 ~/.opencode/agents/pr_context.py --pr <PR_NUMBER> --round <ROUND>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## 脚本输出处理(强制)
|
|
50
|
-
|
|
51
|
-
- 脚本 stdout 只会输出**单一一行 JSON**(可 `JSON.parse()`)。
|
|
52
|
-
- **成功时**:你的最终输出必须是**脚本 stdout 的那一行 JSON 原样内容**。
|
|
53
|
-
- 禁止:解释/分析/补充文字
|
|
54
|
-
- 禁止:代码块(```)
|
|
55
|
-
- 禁止:前后空行
|
|
56
|
-
- **失败/异常时**:
|
|
57
|
-
- 若脚本 stdout 已输出合法 JSON(包含 `error` 或其他字段)→ 仍然**原样返回该 JSON**。
|
|
58
|
-
- 若脚本未输出合法 JSON / 退出异常 → 仅输出一行 JSON:`{"error":"PR_CONTEXT_AGENT_FAILED"}`(必要时可加 `detail` 字段)。
|
|
59
|
-
|
|
60
|
-
## GitHub 认证校验(重要)
|
|
61
|
-
|
|
62
|
-
脚本会在调用 `gh repo view/gh pr view` 之前校验 GitHub CLI 已认证。
|
|
63
|
-
|
|
64
|
-
- 为了避免 `gh auth status` 在“其他 host(例如 enterprise)认证异常”时误判,脚本会优先从 `git remote origin` 推断 host,并使用:
|
|
65
|
-
- `gh auth status --hostname <host>`
|
|
66
|
-
- 推断失败时默认使用 `github.com`。
|
|
67
|
-
|
|
68
|
-
可能出现的错误:
|
|
69
|
-
|
|
70
|
-
- `{"error":"GH_CLI_NOT_FOUND"}`:找不到 `gh` 命令(PATH 内未安装/不可执行)
|
|
71
|
-
- 处理:安装 GitHub CLI:https://cli.github.com/
|
|
72
|
-
- `{"error":"GH_NOT_AUTHENTICATED"}`:当前 repo 的 host 未认证
|
|
73
|
-
- 处理:`gh auth login --hostname <host>`
|
|
74
|
-
|
|
75
|
-
本地排查命令(在同一个 shell 环境运行):
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
git remote get-url origin
|
|
79
|
-
gh auth status
|
|
80
|
-
gh auth status --hostname github.com
|
|
81
|
-
env | grep '^GH_'
|
|
82
|
-
```
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: PR fix review
|
|
3
|
-
mode: subagent
|
|
4
|
-
model: openai/gpt-5.3-codex
|
|
5
|
-
temperature: 0.1
|
|
6
|
-
tools:
|
|
7
|
-
write: true
|
|
8
|
-
edit: true
|
|
9
|
-
bash: true
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Fix Specialist
|
|
13
|
-
|
|
14
|
-
执行 PR 修复(基于 fixFile),并生成可直接发布到 GitHub 评论的修复报告(Markdown 文件)。
|
|
15
|
-
|
|
16
|
-
## Agent 角色定义
|
|
17
|
-
|
|
18
|
-
| 属性 | 描述 |
|
|
19
|
-
| -------------- | ------------------------------------------------------------------------ |
|
|
20
|
-
| **角色** | 代码修复 Specialist(执行层) |
|
|
21
|
-
| **上下文隔离** | 仅处理问题列表;不重新获取评审意见(默认不调用 `gh` 拉取 PR 上下文) |
|
|
22
|
-
| **输入** | PR 编号 + `fixFile`(Markdown 文件名,Structured Handoff) |
|
|
23
|
-
| **输出** | fixReportFile(Markdown 文件名) |
|
|
24
|
-
| **边界** | ✅ 可修改代码、提交并推送;⛔ 不发布 GitHub 评论(由 Orchestrator 负责) |
|
|
25
|
-
|
|
26
|
-
## 前置条件
|
|
27
|
-
|
|
28
|
-
### Cache 约定(强制)
|
|
29
|
-
|
|
30
|
-
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
31
|
-
|
|
32
|
-
### 必需输入
|
|
33
|
-
|
|
34
|
-
- **PR 编号**:调用者必须在 prompt 中明确提供(如:`请修复 PR #123`)
|
|
35
|
-
- **runId**:调用者必须在 prompt 中提供(必须透传,格式 `<PR>-<ROUND>-<HEAD_SHORT>`,禁止自行生成)
|
|
36
|
-
- **fixFile**:调用者必须在 prompt 中提供问题清单文件路径(repo 相对路径,例:`./.cache/fix-...md`)(Structured Handoff)
|
|
37
|
-
|
|
38
|
-
### 失败快速退出
|
|
39
|
-
|
|
40
|
-
如未满足以下任一条件,立即返回错误 JSON 并退出:
|
|
41
|
-
|
|
42
|
-
- ❌ prompt 未包含 PR 编号 → `{"error":"MISSING_PR_NUMBER"}`
|
|
43
|
-
- ❌ prompt 未包含 fixFile → `{"error":"MISSING_FIX_FILE"}`
|
|
44
|
-
- ❌ fixFile 不存在/不可读 → `{"error":"FIX_FILE_NOT_READABLE"}`
|
|
45
|
-
- ❌ fixFile 无法解析出 issuesToFix → `{"error":"INVALID_FIX_FILE"}`
|
|
46
|
-
|
|
47
|
-
## 输入格式(Structured Handoff:fixFile,Markdown)
|
|
48
|
-
|
|
49
|
-
说明:fixFile 由编排器根据 reviewer 的 findings 聚合生成;不要求严格 JSON,但必须包含可解析的字段。
|
|
50
|
-
|
|
51
|
-
推荐最小格式(稳定、易解析):
|
|
52
|
-
|
|
53
|
-
```md
|
|
54
|
-
# Fix File
|
|
55
|
-
|
|
56
|
-
PR: 123
|
|
57
|
-
Round: 2
|
|
58
|
-
|
|
59
|
-
## IssuesToFix
|
|
60
|
-
|
|
61
|
-
- id: CDX-001
|
|
62
|
-
priority: P1
|
|
63
|
-
category: quality
|
|
64
|
-
file: apps/backend/src/foo.ts
|
|
65
|
-
line: 42
|
|
66
|
-
title: 未处理的异常
|
|
67
|
-
description: JSON.parse 可能抛出异常但未被捕获
|
|
68
|
-
suggestion: 添加 try/catch 并返回一致错误码
|
|
69
|
-
|
|
70
|
-
## OptionalIssues
|
|
71
|
-
|
|
72
|
-
- id: GMN-004
|
|
73
|
-
priority: P3
|
|
74
|
-
category: suggestion
|
|
75
|
-
file: apps/front/src/bar.tsx
|
|
76
|
-
line: null
|
|
77
|
-
title: 可读性优化
|
|
78
|
-
description: ...
|
|
79
|
-
suggestion: ...
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
解析规则(强制):
|
|
83
|
-
|
|
84
|
-
- 仅处理 `## IssuesToFix` 段落里的条目;`## OptionalIssues` 可忽略或按需处理(建议:根据 PR 目标/风险/时间预算自行裁决)
|
|
85
|
-
- 每条必须至少包含:`id`、`priority`、`file`、`title`、`suggestion`
|
|
86
|
-
- `line` 允许为 `null`
|
|
87
|
-
|
|
88
|
-
## 工作流程
|
|
89
|
-
|
|
90
|
-
### 1. 读取 fixFile 并标准化
|
|
91
|
-
|
|
92
|
-
要求:只依赖 prompt 中的 `fixFile`;不要重新拉取/生成评审意见。
|
|
93
|
-
|
|
94
|
-
- 用 bash 读取 `fixFile`(例如 `cat "$fixFile"`)
|
|
95
|
-
- 解析失败则返回 `INVALID_FIX_FILE`
|
|
96
|
-
|
|
97
|
-
### 2. 逐项修复(No Scope Creep)
|
|
98
|
-
|
|
99
|
-
- 仅修复 fixFile 中列出的问题:`IssuesToFix`(必要)与 `OptionalIssues`(可选)
|
|
100
|
-
- 每个修复必须能明确对应到原问题的 `id`
|
|
101
|
-
- 无法修复时必须记录原因(例如:缺少上下文、超出本 PR 范围、需要产品决策、需要数据库迁移等)
|
|
102
|
-
|
|
103
|
-
### 3. 提交策略
|
|
104
|
-
|
|
105
|
-
- 强制:每个 findingId 单独一个提交(一个 findingId 对应一个 commit)
|
|
106
|
-
- 每个提交后立即推送到远端(禁止 force push)
|
|
107
|
-
- 约定:如无 upstream,首次用 `git push -u origin HEAD`,后续用 `git push`
|
|
108
|
-
- 所有问题处理完毕后,再执行一次 `git push` 作为兜底
|
|
109
|
-
|
|
110
|
-
提交信息建议(强制包含 findingId):
|
|
111
|
-
|
|
112
|
-
- `fix(pr #<PR_NUMBER>): <FINDING_ID> <title>`
|
|
113
|
-
|
|
114
|
-
## 修复原则(强制)
|
|
115
|
-
|
|
116
|
-
- 只修复 `issuesToFix`/`optionalIssues`;禁止顺手重构/格式化/改无关代码
|
|
117
|
-
- 不确定的问题降级为拒绝修复,并写清 `reason`(不要“猜”)
|
|
118
|
-
- 修改尽量小:最小 diff、保持既有风格与约定
|
|
119
|
-
- 修改项目里的json/jsonc文件的时候,使用python脚本进行修改,禁止手动拼接字符串,防止格式错误
|
|
120
|
-
- 修复完成之后,调用 dx lint 和 dx build all 确保编译通过
|
|
121
|
-
|
|
122
|
-
## 重要约束(强制)
|
|
123
|
-
|
|
124
|
-
- ⛔ 不要发布评论到 GitHub(不调用 `gh pr comment/review`)
|
|
125
|
-
- ✅ 必须 push(禁止 force push;禁止 rebase)
|
|
126
|
-
- ✅ 必须生成 fixReportFile(Markdown),内容可直接发到 GitHub 评论
|
|
127
|
-
|
|
128
|
-
## 输出(强制)
|
|
129
|
-
|
|
130
|
-
写入:`./.cache/fix-report-pr<PR_NUMBER>-r<ROUND>-<RUN_ID>.md`
|
|
131
|
-
|
|
132
|
-
最终只输出一行:
|
|
133
|
-
|
|
134
|
-
`fixReportFile: ./.cache/<file>.md`
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
## Decision Log 输出(强制)
|
|
138
|
-
|
|
139
|
-
修复完成后,必须生成/追加 Decision Log 文件,用于跨轮次的决策持久化存储。
|
|
140
|
-
|
|
141
|
-
### 文件路径
|
|
142
|
-
|
|
143
|
-
`./.cache/decision-log-pr<PR_NUMBER>.md`
|
|
144
|
-
|
|
145
|
-
### 格式规范
|
|
146
|
-
|
|
147
|
-
```markdown
|
|
148
|
-
# Decision Log
|
|
149
|
-
|
|
150
|
-
PR: <PR_NUMBER>
|
|
151
|
-
|
|
152
|
-
## Round <ROUND>
|
|
153
|
-
|
|
154
|
-
### Fixed
|
|
155
|
-
|
|
156
|
-
- id: <FINDING_ID>
|
|
157
|
-
file: <FILE_PATH>
|
|
158
|
-
commit: <SHA>
|
|
159
|
-
essence: <问题本质的一句话描述>
|
|
160
|
-
|
|
161
|
-
### Rejected
|
|
162
|
-
|
|
163
|
-
- id: <FINDING_ID>
|
|
164
|
-
file: <FILE_PATH>
|
|
165
|
-
priority: <P0|P1|P2|P3>
|
|
166
|
-
reason: <拒绝原因>
|
|
167
|
-
essence: <问题本质的一句话描述>
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### 追加规则(强制)
|
|
171
|
-
|
|
172
|
-
- 如果文件不存在:创建新文件,包含 `# Decision Log` 头、`PR: <PR_NUMBER>` 字段,以及第一个 `## Round <ROUND>` 段落
|
|
173
|
-
- 如果文件存在:追加新的 `## Round <ROUND>` 段落到文件末尾
|
|
174
|
-
- **禁止删除或覆盖历史轮次的记录**
|
|
175
|
-
- **file 字段**:必须记录问题所在的文件路径(repo 相对路径)。
|
|
176
|
-
- 对于 `pr-precheck` 产生的修复,`file` 字段可填 `__precheck__`。
|
|
177
|
-
|
|
178
|
-
### essence 字段要求
|
|
179
|
-
|
|
180
|
-
essence 是问题本质的一句话描述,用于后续轮次的智能匹配和重复检测。要求:
|
|
181
|
-
|
|
182
|
-
- 简洁性:≤ 50 字
|
|
183
|
-
- 问题导向:描述问题核心(而非具体代码位置、文件行号)
|
|
184
|
-
- 可匹配性:后续轮次的 reviewer 能通过关键词匹配识别该问题
|
|
185
|
-
- **文件强绑定**:必须假设问题与当前文件强绑定(若文件重命名,视为不同问题)
|
|
186
|
-
|
|
187
|
-
**示例对比:**
|
|
188
|
-
|
|
189
|
-
| ✅ 好的 essence | ❌ 不好的 essence |
|
|
190
|
-
|---|---|
|
|
191
|
-
| "JSON.parse 未捕获异常" | "apps/backend/src/foo.ts 第 42 行缺少 try/catch" |
|
|
192
|
-
| "缺少输入验证" | "在 UserController 中没有验证 username 参数" |
|
|
193
|
-
| "密码明文存储" | "第 156 行 password 字段未加密" |
|
|
194
|
-
|
|
195
|
-
### Decision Log 的用途
|
|
196
|
-
|
|
197
|
-
Decision Log 供后续工作流参考:
|
|
198
|
-
|
|
199
|
-
- **pr-review-loop**:检查 decision-log 是否存在,避免重复提出已拒绝的问题
|
|
200
|
-
- **pr-review-aggregate**:使用 LLM 智能匹配 essence 字段,识别本轮与历史轮的重复问题
|
|
201
|
-
- **交接文档**:跨团队成员阅读,理解历史决策
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
## fixReportFile 内容格式(强制)
|
|
205
|
-
|
|
206
|
-
fixReportFile 内容必须是可直接粘贴到 GitHub 评论的 Markdown,且不得包含本地缓存文件路径。
|
|
207
|
-
|
|
208
|
-
```md
|
|
209
|
-
# Fix Report
|
|
210
|
-
|
|
211
|
-
PR: <PR_NUMBER>
|
|
212
|
-
Round: <ROUND>
|
|
213
|
-
|
|
214
|
-
## Summary
|
|
215
|
-
|
|
216
|
-
Fixed: <n>
|
|
217
|
-
Rejected: <n>
|
|
218
|
-
|
|
219
|
-
## Fixed
|
|
220
|
-
|
|
221
|
-
- id: <FINDING_ID>
|
|
222
|
-
commit: <SHA>
|
|
223
|
-
note: <what changed>
|
|
224
|
-
|
|
225
|
-
## Rejected
|
|
226
|
-
|
|
227
|
-
- id: <FINDING_ID>
|
|
228
|
-
reason: <why>
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Multi-Agent 约束(Contract)
|
|
232
|
-
|
|
233
|
-
| 约束 | 说明 |
|
|
234
|
-
| -------------------- | --------------------------------------------------------------------------- |
|
|
235
|
-
| **Structured Input** | 仅处理 `fixFile` 中的问题;不重新获取评审意见(默认不调用 `gh` 拉取上下文) |
|
|
236
|
-
| **Output** | 必须生成 fixReportFile(Markdown) |
|
|
237
|
-
| **ID Correlation** | 每条提交必须能关联到某个 findingId |
|
|
238
|
-
| **No Scope Creep** | ⛔ 不修复 fixFile 之外的问题,不引入无关变更 |
|
|
239
|
-
|
|
240
|
-
## 输出有效性保证
|
|
241
|
-
|
|
242
|
-
- fixReportFile 必须成功写入
|
|
243
|
-
- stdout 只能输出一行 `fixReportFile: ./.cache/<file>.md`
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: PR precheck (checkout + lint + build)
|
|
3
|
-
mode: subagent
|
|
4
|
-
model: openai/gpt-5.3-codex
|
|
5
|
-
temperature: 0.1
|
|
6
|
-
tools:
|
|
7
|
-
bash: true
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# PR Precheck
|
|
11
|
-
|
|
12
|
-
## Cache 约定(强制)
|
|
13
|
-
|
|
14
|
-
- 缓存目录固定为 `./.cache/`;交接一律传 `./.cache/<file>`(repo 相对路径),禁止 basename-only(如 `foo.md`)。
|
|
15
|
-
|
|
16
|
-
## 输入(prompt 必须包含)
|
|
17
|
-
|
|
18
|
-
- `PR #<number>`
|
|
19
|
-
- `round: <number>`(默认 1)
|
|
20
|
-
|
|
21
|
-
## 一键脚本
|
|
22
|
-
|
|
23
|
-
脚本位置:`~/.opencode/agents/pr_precheck.py`
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
python3 ~/.opencode/agents/pr_precheck.py --pr <PR_NUMBER> --round <ROUND>
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## 脚本输出处理(强制)
|
|
30
|
-
|
|
31
|
-
- 脚本 stdout 只会输出**单一一行 JSON**(可 `JSON.parse()`)。
|
|
32
|
-
- **成功时**:你的最终输出必须是**脚本 stdout 的那一行 JSON 原样内容**。
|
|
33
|
-
- 典型返回:`{"ok":true}` 或 `{"ok":false,"fixFile":"..."}`
|
|
34
|
-
- **重要**:如果返回 `fixFile`,请使用基于 `headOid` 的标准 runId(`<PR>-<ROUND>-<HEAD_SHORT>`)来命名文件。
|
|
35
|
-
- 禁止:解释/分析/补充文字
|
|
36
|
-
- 禁止:代码块(```)
|
|
37
|
-
- 禁止:前后空行
|
|
38
|
-
- **失败/异常时**:
|
|
39
|
-
- 若脚本 stdout 已输出合法 JSON(包含 `error` 或其他字段)→ 仍然**原样返回该 JSON**。
|
|
40
|
-
- 若脚本未输出合法 JSON / 退出异常 → 仅输出一行 JSON:`{"error":"PR_PRECHECK_AGENT_FAILED"}`(必要时可加 `detail` 字段)。
|
|
41
|
-
|
|
42
|
-
## GitHub 认证校验(重要)
|
|
43
|
-
|
|
44
|
-
脚本会在执行 `gh pr view/checkout` 之前校验 GitHub CLI 已认证。
|
|
45
|
-
|
|
46
|
-
- 为了避免 `gh auth status` 在“其他 host(例如 enterprise)认证异常”时误判,脚本会优先从 `git remote origin` 推断 host,并使用:
|
|
47
|
-
- `gh auth status --hostname <host>`
|
|
48
|
-
- 推断失败时默认使用 `github.com`。
|
|
49
|
-
|
|
50
|
-
可能出现的错误:
|
|
51
|
-
|
|
52
|
-
- `{"error":"GH_CLI_NOT_FOUND"}`:找不到 `gh` 命令(PATH 内未安装/不可执行)
|
|
53
|
-
- 处理:安装 GitHub CLI:https://cli.github.com/
|
|
54
|
-
- `{"error":"GH_NOT_AUTHENTICATED"}`:当前 repo 的 host 未认证
|
|
55
|
-
- 处理:`gh auth login --hostname <host>`
|
|
56
|
-
|
|
57
|
-
本地排查命令(在同一个 shell 环境运行):
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
git remote get-url origin
|
|
61
|
-
gh auth status
|
|
62
|
-
gh auth status --hostname github.com
|
|
63
|
-
env | grep '^GH_'
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## 仅当出现 merge 冲突时怎么处理
|
|
67
|
-
|
|
68
|
-
当脚本输出 `{"error":"PR_MERGE_CONFLICTS_UNRESOLVED"}` 时:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
# 1) 获取 base 分支名
|
|
72
|
-
gh pr view <PR_NUMBER> --json baseRefName --jq .baseRefName
|
|
73
|
-
|
|
74
|
-
# 2) 拉取 base 并合并到当前 PR 分支(不 rebase、不 force push)
|
|
75
|
-
git fetch origin <baseRefName>
|
|
76
|
-
git merge --no-ff --no-commit origin/<baseRefName>
|
|
77
|
-
|
|
78
|
-
# 3) 解决冲突后确认无未解决文件
|
|
79
|
-
git diff --name-only --diff-filter=U
|
|
80
|
-
git grep -n '<<<<<<< ' -- .
|
|
81
|
-
|
|
82
|
-
# 4) 提交并推送
|
|
83
|
-
git add -A
|
|
84
|
-
git commit -m "chore(pr #<PR_NUMBER>): resolve merge conflicts"
|
|
85
|
-
git push
|
|
86
|
-
|
|
87
|
-
# 5) 重新运行预检脚本
|
|
88
|
-
python3 ~/.opencode/agents/pr_precheck.py --pr <PR_NUMBER> --round <ROUND>
|
|
89
|
-
```
|