@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.
- 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 +6 -2
- package/@opencode/agents/codex-reviewer.md +6 -2
- package/@opencode/agents/gemini-reviewer.md +6 -2
- package/@opencode/agents/gh-thread-reviewer.md +6 -1
- package/@opencode/agents/pr-context.md +12 -0
- package/@opencode/agents/pr-fix.md +6 -1
- package/@opencode/agents/pr-precheck.md +4 -2
- package/@opencode/agents/pr-review-aggregate.md +18 -10
- package/@opencode/agents/pr_context.py +35 -19
- package/@opencode/agents/pr_precheck.py +210 -42
- package/@opencode/agents/test_pr_review_aggregate.py +234 -33
- package/@opencode/commands/pr-review-loop.md +61 -11
- package/package.json +1 -1
|
@@ -15,19 +15,70 @@
|
|
|
15
15
|
# Stdout contract: print exactly one JSON object and nothing else.
|
|
16
16
|
|
|
17
17
|
import json
|
|
18
|
-
import os
|
|
19
18
|
import re
|
|
20
|
-
import secrets
|
|
21
19
|
import subprocess
|
|
22
20
|
import sys
|
|
23
21
|
from urllib.parse import urlparse
|
|
24
22
|
from pathlib import Path
|
|
25
23
|
|
|
26
24
|
|
|
25
|
+
_last_pr_number = None
|
|
26
|
+
_last_round = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def emit_json(obj):
|
|
30
|
+
# Stdout contract: exactly one JSON line.
|
|
31
|
+
_ = sys.stdout.write(json.dumps(obj, separators=(",", ":"), ensure_ascii=True) + "\n")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_args(argv):
|
|
35
|
+
pr = None
|
|
36
|
+
round_n = 1
|
|
37
|
+
|
|
38
|
+
positional = []
|
|
39
|
+
i = 1
|
|
40
|
+
while i < len(argv):
|
|
41
|
+
a = argv[i]
|
|
42
|
+
if a == "--pr":
|
|
43
|
+
i += 1
|
|
44
|
+
if i >= len(argv):
|
|
45
|
+
return None, None, "PR_NUMBER_NOT_PROVIDED"
|
|
46
|
+
pr = argv[i]
|
|
47
|
+
elif a.startswith("--pr="):
|
|
48
|
+
pr = a.split("=", 1)[1]
|
|
49
|
+
elif a == "--round":
|
|
50
|
+
i += 1
|
|
51
|
+
if i >= len(argv):
|
|
52
|
+
return None, None, "ROUND_INVALID"
|
|
53
|
+
round_n = argv[i]
|
|
54
|
+
elif a.startswith("--round="):
|
|
55
|
+
round_n = a.split("=", 1)[1]
|
|
56
|
+
elif a.startswith("-"):
|
|
57
|
+
return None, None, "INVALID_ARGS"
|
|
58
|
+
else:
|
|
59
|
+
positional.append(a)
|
|
60
|
+
i += 1
|
|
61
|
+
|
|
62
|
+
if pr is None and positional:
|
|
63
|
+
pr = positional[0]
|
|
64
|
+
|
|
65
|
+
pr = (pr or "").strip()
|
|
66
|
+
if not pr.isdigit():
|
|
67
|
+
return None, None, "PR_NUMBER_NOT_PROVIDED"
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
round_int = int(str(round_n).strip())
|
|
71
|
+
except Exception:
|
|
72
|
+
return int(pr), None, "ROUND_INVALID"
|
|
73
|
+
if round_int < 1:
|
|
74
|
+
return int(pr), None, "ROUND_INVALID"
|
|
75
|
+
|
|
76
|
+
return int(pr), round_int, None
|
|
77
|
+
|
|
27
78
|
def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
|
|
28
79
|
try:
|
|
29
80
|
return _run(cmd, cwd=cwd, stdout_path=stdout_path, stderr_path=stderr_path)
|
|
30
|
-
except FileNotFoundError
|
|
81
|
+
except FileNotFoundError:
|
|
31
82
|
# Match common shell semantics for "command not found".
|
|
32
83
|
return 127
|
|
33
84
|
|
|
@@ -156,96 +207,197 @@ def write_fixfile(path, issues):
|
|
|
156
207
|
sugg = it["suggestion"].replace("\n", "\\n")
|
|
157
208
|
out.append(f" description: {desc}")
|
|
158
209
|
out.append(f" suggestion: {sugg}")
|
|
159
|
-
p.write_text("\n".join(out) + "\n")
|
|
210
|
+
_ = p.write_text("\n".join(out) + "\n")
|
|
160
211
|
|
|
161
212
|
|
|
162
213
|
def main():
|
|
163
|
-
|
|
164
|
-
|
|
214
|
+
global _last_pr_number
|
|
215
|
+
global _last_round
|
|
216
|
+
|
|
217
|
+
pr_number, round_n, arg_err = parse_args(sys.argv)
|
|
218
|
+
if arg_err:
|
|
219
|
+
err_obj: dict[str, object] = {"error": arg_err}
|
|
220
|
+
if pr_number is not None:
|
|
221
|
+
err_obj["prNumber"] = pr_number
|
|
222
|
+
if round_n is not None:
|
|
223
|
+
err_obj["round"] = round_n
|
|
224
|
+
emit_json(err_obj)
|
|
165
225
|
return 1
|
|
166
226
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
print(json.dumps({"error": "PR_NUMBER_NOT_PROVIDED"}))
|
|
170
|
-
return 1
|
|
227
|
+
_last_pr_number = pr_number
|
|
228
|
+
_last_round = round_n
|
|
171
229
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
230
|
+
pr = str(pr_number)
|
|
231
|
+
base_payload: dict[str, object] = {
|
|
232
|
+
"prNumber": pr_number,
|
|
233
|
+
"round": round_n,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
rc, git_out, _ = run_capture(["git", "rev-parse", "--is-inside-work-tree"])
|
|
237
|
+
if rc != 0 or git_out.strip() != "true":
|
|
238
|
+
emit_json({
|
|
239
|
+
**base_payload,
|
|
240
|
+
"error": "NOT_A_GIT_REPO",
|
|
241
|
+
})
|
|
175
242
|
return 1
|
|
176
243
|
|
|
177
244
|
host = _detect_git_remote_host() or "github.com"
|
|
245
|
+
|
|
246
|
+
auth_host_used = None
|
|
178
247
|
rc, gh_out, gh_err = run_capture(["gh", "auth", "status", "--hostname", host])
|
|
179
248
|
if rc == 127:
|
|
180
|
-
|
|
249
|
+
emit_json({
|
|
250
|
+
**base_payload,
|
|
181
251
|
"error": "GH_CLI_NOT_FOUND",
|
|
182
252
|
"detail": "gh not found in PATH",
|
|
183
253
|
"suggestion": "Install GitHub CLI: https://cli.github.com/",
|
|
184
|
-
})
|
|
254
|
+
})
|
|
185
255
|
return 1
|
|
256
|
+
|
|
257
|
+
if rc == 0:
|
|
258
|
+
auth_host_used = host
|
|
259
|
+
else:
|
|
260
|
+
# If hostname auth fails (e.g. SSH host alias), fall back to default host.
|
|
261
|
+
rc_default, gh_out_default, gh_err_default = run_capture(["gh", "auth", "status"])
|
|
262
|
+
if rc_default == 0:
|
|
263
|
+
# Proceed using default gh auth context; avoid false GH_NOT_AUTHENTICATED.
|
|
264
|
+
auth_host_used = "default"
|
|
265
|
+
rc, gh_out, gh_err = rc_default, gh_out_default, gh_err_default
|
|
266
|
+
|
|
186
267
|
if rc != 0:
|
|
187
268
|
detail = (gh_err or gh_out or "").strip()
|
|
188
269
|
if len(detail) > 4000:
|
|
189
270
|
detail = detail[-4000:]
|
|
190
|
-
|
|
271
|
+
emit_json({
|
|
272
|
+
**base_payload,
|
|
191
273
|
"error": "GH_NOT_AUTHENTICATED",
|
|
192
274
|
"host": host,
|
|
193
275
|
"detail": detail,
|
|
194
276
|
"suggestion": f"Run: gh auth login --hostname {host}",
|
|
195
|
-
})
|
|
277
|
+
})
|
|
196
278
|
return 1
|
|
197
279
|
|
|
198
|
-
|
|
280
|
+
if auth_host_used == "default":
|
|
281
|
+
base_payload["authHostUsed"] = auth_host_used
|
|
282
|
+
|
|
283
|
+
rc, pr_json, _ = run_capture([
|
|
284
|
+
"gh",
|
|
285
|
+
"pr",
|
|
286
|
+
"view",
|
|
287
|
+
pr,
|
|
288
|
+
"--json",
|
|
289
|
+
"headRefName,baseRefName,mergeable,headRefOid",
|
|
290
|
+
])
|
|
199
291
|
if rc != 0:
|
|
200
|
-
|
|
292
|
+
emit_json({
|
|
293
|
+
**base_payload,
|
|
294
|
+
"error": "PR_NOT_FOUND_OR_NO_ACCESS",
|
|
295
|
+
})
|
|
201
296
|
return 1
|
|
202
297
|
try:
|
|
203
298
|
pr_info = json.loads(pr_json)
|
|
204
299
|
except Exception:
|
|
205
|
-
|
|
300
|
+
emit_json({
|
|
301
|
+
**base_payload,
|
|
302
|
+
"error": "PR_NOT_FOUND_OR_NO_ACCESS",
|
|
303
|
+
})
|
|
206
304
|
return 1
|
|
207
305
|
|
|
208
306
|
head = (pr_info.get("headRefName") or "").strip()
|
|
209
307
|
base = (pr_info.get("baseRefName") or "").strip()
|
|
210
308
|
mergeable = (pr_info.get("mergeable") or "").strip()
|
|
211
309
|
|
|
310
|
+
head_oid = (pr_info.get("headRefOid") or "").strip()
|
|
311
|
+
if not head_oid:
|
|
312
|
+
emit_json({
|
|
313
|
+
**base_payload,
|
|
314
|
+
"error": "PR_HEAD_OID_NOT_FOUND",
|
|
315
|
+
"headRefName": head,
|
|
316
|
+
"baseRefName": base,
|
|
317
|
+
"mergeable": mergeable,
|
|
318
|
+
})
|
|
319
|
+
return 1
|
|
320
|
+
|
|
321
|
+
head_short = head_oid[:7]
|
|
322
|
+
run_id = f"{pr_number}-{round_n}-{head_short}"
|
|
323
|
+
|
|
324
|
+
payload: dict[str, object] = {
|
|
325
|
+
**base_payload,
|
|
326
|
+
"runId": run_id,
|
|
327
|
+
"headOid": head_oid,
|
|
328
|
+
"headShort": head_short,
|
|
329
|
+
"headRefName": head,
|
|
330
|
+
"baseRefName": base,
|
|
331
|
+
"mergeable": mergeable,
|
|
332
|
+
}
|
|
333
|
+
|
|
212
334
|
rc, cur_branch, _ = run_capture(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
|
213
335
|
if rc != 0:
|
|
214
|
-
|
|
336
|
+
emit_json({
|
|
337
|
+
**payload,
|
|
338
|
+
"error": "PR_CHECKOUT_FAILED",
|
|
339
|
+
})
|
|
215
340
|
return 1
|
|
216
341
|
if head and cur_branch.strip() != head:
|
|
217
342
|
if run(["gh", "pr", "checkout", pr]) != 0:
|
|
218
|
-
|
|
343
|
+
emit_json({
|
|
344
|
+
**payload,
|
|
345
|
+
"error": "PR_CHECKOUT_FAILED",
|
|
346
|
+
})
|
|
219
347
|
return 1
|
|
220
348
|
|
|
221
349
|
if not base:
|
|
222
|
-
rc,
|
|
350
|
+
rc, default_branch_out, _ = run_capture([
|
|
351
|
+
"gh",
|
|
352
|
+
"repo",
|
|
353
|
+
"view",
|
|
354
|
+
"--json",
|
|
355
|
+
"defaultBranchRef",
|
|
356
|
+
"--jq",
|
|
357
|
+
".defaultBranchRef.name",
|
|
358
|
+
])
|
|
223
359
|
if rc == 0:
|
|
224
|
-
base =
|
|
360
|
+
base = default_branch_out.strip()
|
|
225
361
|
if not base:
|
|
226
|
-
|
|
362
|
+
emit_json({
|
|
363
|
+
**payload,
|
|
364
|
+
"error": "PR_BASE_REF_NOT_FOUND",
|
|
365
|
+
})
|
|
227
366
|
return 1
|
|
228
367
|
|
|
368
|
+
# baseRefName can be resolved from default branch; keep payload in sync.
|
|
369
|
+
payload["baseRefName"] = base
|
|
370
|
+
|
|
229
371
|
if run(["git", "fetch", "origin", base]) != 0:
|
|
230
|
-
|
|
372
|
+
emit_json({
|
|
373
|
+
**payload,
|
|
374
|
+
"error": "PR_BASE_REF_FETCH_FAILED",
|
|
375
|
+
"baseRefName": base,
|
|
376
|
+
})
|
|
231
377
|
return 1
|
|
232
378
|
|
|
233
379
|
if mergeable == "CONFLICTING":
|
|
234
|
-
|
|
380
|
+
emit_json({
|
|
381
|
+
**payload,
|
|
382
|
+
"error": "PR_MERGE_CONFLICTS_UNRESOLVED",
|
|
383
|
+
})
|
|
235
384
|
return 1
|
|
236
385
|
|
|
237
|
-
run_id = secrets.token_hex(4)
|
|
238
386
|
root = repo_root()
|
|
239
387
|
cache = cache_dir(root)
|
|
240
388
|
cache.mkdir(parents=True, exist_ok=True)
|
|
241
389
|
|
|
242
|
-
cache_clear_log = cache / f"precheck-
|
|
243
|
-
lint_log = cache / f"precheck-
|
|
244
|
-
build_log = cache / f"precheck-
|
|
245
|
-
meta_log = cache / f"precheck-
|
|
246
|
-
|
|
247
|
-
meta_log.write_text(json.dumps({
|
|
248
|
-
"
|
|
390
|
+
cache_clear_log = cache / f"precheck-{run_id}-cache-clear.log"
|
|
391
|
+
lint_log = cache / f"precheck-{run_id}-lint.log"
|
|
392
|
+
build_log = cache / f"precheck-{run_id}-build.log"
|
|
393
|
+
meta_log = cache / f"precheck-{run_id}-meta.json"
|
|
394
|
+
|
|
395
|
+
_ = meta_log.write_text(json.dumps({
|
|
396
|
+
"prNumber": pr_number,
|
|
397
|
+
"round": round_n,
|
|
398
|
+
"runId": run_id,
|
|
399
|
+
"headOid": head_oid,
|
|
400
|
+
"headShort": head_short,
|
|
249
401
|
"headRefName": head,
|
|
250
402
|
"baseRefName": base,
|
|
251
403
|
"mergeable": mergeable,
|
|
@@ -256,7 +408,7 @@ def main():
|
|
|
256
408
|
|
|
257
409
|
cache_rc = run(["dx", "cache", "clear"], stdout_path=str(cache_clear_log), stderr_path=str(cache_clear_log))
|
|
258
410
|
if cache_rc != 0:
|
|
259
|
-
fix_file = f"precheck-fix-
|
|
411
|
+
fix_file = f"precheck-fix-{run_id}.md"
|
|
260
412
|
fix_path = cache / fix_file
|
|
261
413
|
log_tail = tail_text(cache_clear_log)
|
|
262
414
|
issues = [{
|
|
@@ -270,7 +422,11 @@ def main():
|
|
|
270
422
|
"suggestion": f"Open log: {repo_relpath(root, cache_clear_log)}",
|
|
271
423
|
}]
|
|
272
424
|
write_fixfile(str(fix_path), issues)
|
|
273
|
-
|
|
425
|
+
emit_json({
|
|
426
|
+
**payload,
|
|
427
|
+
"ok": False,
|
|
428
|
+
"fixFile": repo_relpath(root, fix_path),
|
|
429
|
+
})
|
|
274
430
|
return 1
|
|
275
431
|
|
|
276
432
|
import threading
|
|
@@ -288,10 +444,13 @@ def main():
|
|
|
288
444
|
t2.join()
|
|
289
445
|
|
|
290
446
|
if results.get("lint", 1) == 0 and results.get("build", 1) == 0:
|
|
291
|
-
|
|
447
|
+
emit_json({
|
|
448
|
+
**payload,
|
|
449
|
+
"ok": True,
|
|
450
|
+
})
|
|
292
451
|
return 0
|
|
293
452
|
|
|
294
|
-
fix_file = f"precheck-fix-
|
|
453
|
+
fix_file = f"precheck-fix-{run_id}.md"
|
|
295
454
|
fix_path = cache / fix_file
|
|
296
455
|
|
|
297
456
|
issues = []
|
|
@@ -325,13 +484,22 @@ def main():
|
|
|
325
484
|
})
|
|
326
485
|
|
|
327
486
|
write_fixfile(str(fix_path), issues)
|
|
328
|
-
|
|
487
|
+
emit_json({
|
|
488
|
+
**payload,
|
|
489
|
+
"ok": False,
|
|
490
|
+
"fixFile": repo_relpath(root, fix_path),
|
|
491
|
+
})
|
|
329
492
|
return 1
|
|
330
493
|
|
|
331
494
|
|
|
332
495
|
if __name__ == "__main__":
|
|
333
496
|
try:
|
|
334
497
|
sys.exit(main())
|
|
335
|
-
except Exception
|
|
336
|
-
|
|
498
|
+
except Exception:
|
|
499
|
+
err_obj: dict[str, object] = {"error": "PRECHECK_SCRIPT_FAILED"}
|
|
500
|
+
if _last_pr_number is not None:
|
|
501
|
+
err_obj["prNumber"] = _last_pr_number
|
|
502
|
+
if _last_round is not None:
|
|
503
|
+
err_obj["round"] = _last_round
|
|
504
|
+
emit_json(err_obj)
|
|
337
505
|
sys.exit(1)
|