@ranger1/dx 0.1.35 → 0.1.36

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.
@@ -318,13 +318,66 @@ def _counts(findings):
318
318
  return c
319
319
 
320
320
 
321
- def _post_pr_comment(pr_number, body_ref):
321
+ def _check_existing_comment(pr_number, run_id, round_num, comment_type):
322
+ """
323
+ Check if a comment with same runId/round/type already exists.
324
+ Returns True if duplicate exists (should skip posting).
325
+
326
+ comment_type: "review-summary" or "fix-report" or "final-report"
327
+ """
328
+ try:
329
+ result = subprocess.run(
330
+ ["gh", "api", f"repos/:owner/:repo/issues/{pr_number}/comments", "--paginate"],
331
+ stdout=subprocess.PIPE,
332
+ stderr=subprocess.DEVNULL,
333
+ text=True,
334
+ )
335
+ if result.returncode != 0:
336
+ return False
337
+
338
+ comments = json.loads(result.stdout or "[]")
339
+
340
+ if comment_type == "review-summary":
341
+ type_header = f"## Review Summary (Round {round_num})"
342
+ elif comment_type == "fix-report":
343
+ type_header = f"## Fix Report (Round {round_num})"
344
+ elif comment_type == "final-report":
345
+ type_header = "## Final Report"
346
+ else:
347
+ return False
348
+
349
+ run_id_pattern = f"RunId: {run_id}"
350
+
351
+ for comment in comments:
352
+ body = comment.get("body", "")
353
+ if MARKER in body and type_header in body and run_id_pattern in body:
354
+ return True
355
+
356
+ return False
357
+ except Exception:
358
+ return False
359
+
360
+
361
+ def _post_pr_comment(pr_number, body_ref, run_id=None, round_num=None, comment_type=None):
362
+ """
363
+ Post a PR comment with idempotency check.
364
+
365
+ If run_id, round_num, and comment_type are provided, checks for existing
366
+ duplicate before posting and skips if already posted.
367
+
368
+ Returns: True if posted successfully or skipped (idempotent), False on error
369
+ """
322
370
  if isinstance(body_ref, Path):
323
371
  p = body_ref
324
372
  else:
325
373
  p = _resolve_ref(REPO_ROOT, CACHE_DIR, body_ref)
326
374
  if not p:
327
375
  return False
376
+
377
+ if run_id and round_num and comment_type:
378
+ if _check_existing_comment(pr_number, run_id, round_num, comment_type):
379
+ return True
380
+
328
381
  body_path = str(p)
329
382
  rc = subprocess.run(
330
383
  ["gh", "pr", "comment", str(pr_number), "--body-file", body_path],
@@ -397,6 +450,32 @@ def _render_mode_b_comment(pr_number, round_num, run_id, fix_report_md):
397
450
  return "\n".join(body)
398
451
 
399
452
 
453
+ def _render_final_comment(pr_number, round_num, run_id, status):
454
+ lines = []
455
+ lines.append(MARKER)
456
+ lines.append("")
457
+ lines.append("## Final Report")
458
+ lines.append("")
459
+ lines.append(f"- PR: #{pr_number}")
460
+ lines.append(f"- Total Rounds: {round_num}")
461
+ lines.append(f"- RunId: {run_id}")
462
+ lines.append("")
463
+
464
+ if status == "RESOLVED":
465
+ lines.append("### Status: ✅ All issues resolved")
466
+ lines.append("")
467
+ lines.append("All P0/P1/P2 issues from the automated review have been addressed.")
468
+ lines.append("The PR is ready for human review and merge.")
469
+ else:
470
+ lines.append("### Status: ⚠️ Max rounds reached")
471
+ lines.append("")
472
+ lines.append("The automated review loop has completed the maximum number of rounds (3).")
473
+ lines.append("Some issues may still remain. Please review the PR comments above for details.")
474
+
475
+ lines.append("")
476
+ return "\n".join(lines)
477
+
478
+
400
479
  def main(argv):
401
480
  class _ArgParser(argparse.ArgumentParser):
402
481
  def error(self, message):
@@ -409,6 +488,7 @@ def main(argv):
409
488
  parser.add_argument("--context-file")
410
489
  parser.add_argument("--review-file", action="append", default=[])
411
490
  parser.add_argument("--fix-report-file")
491
+ parser.add_argument("--final-report")
412
492
  parser.add_argument("--duplicate-groups-json")
413
493
  parser.add_argument("--duplicate-groups-b64")
414
494
 
@@ -422,6 +502,7 @@ def main(argv):
422
502
  round_num = args.round
423
503
  run_id = str(args.run_id)
424
504
 
505
+ final_report = (args.final_report or "").strip() or None
425
506
  fix_report_file = (args.fix_report_file or "").strip() or None
426
507
  context_file = (args.context_file or "").strip() or None
427
508
  review_files = []
@@ -430,6 +511,17 @@ def main(argv):
430
511
  if s:
431
512
  review_files.append(s)
432
513
 
514
+ if final_report:
515
+ body = _render_final_comment(pr_number, round_num, run_id, final_report)
516
+ body_basename = f"review-aggregate-final-pr{pr_number}-{run_id}.md"
517
+ body_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / body_basename)
518
+ _write_cache_text(body_ref, body)
519
+ if not _post_pr_comment(pr_number, body_ref, run_id=run_id, round_num=round_num, comment_type="final-report"):
520
+ _json_out({"error": "GH_PR_COMMENT_FAILED"})
521
+ return 1
522
+ _json_out({"ok": True, "final": True})
523
+ return 0
524
+
433
525
  if fix_report_file:
434
526
  fix_p = _resolve_ref(REPO_ROOT, CACHE_DIR, fix_report_file)
435
527
  if not fix_p or not fix_p.exists():
@@ -440,7 +532,7 @@ def main(argv):
440
532
  body_basename = f"review-aggregate-fix-comment-pr{pr_number}-r{round_num}-{run_id}.md"
441
533
  body_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / body_basename)
442
534
  _write_cache_text(body_ref, body)
443
- if not _post_pr_comment(pr_number, body_ref):
535
+ if not _post_pr_comment(pr_number, body_ref, run_id=run_id, round_num=round_num, comment_type="fix-report"):
444
536
  _json_out({"error": "GH_PR_COMMENT_FAILED"})
445
537
  return 1
446
538
  _json_out({"ok": True})
@@ -488,7 +580,7 @@ def main(argv):
488
580
  body_basename = f"review-aggregate-comment-pr{pr_number}-r{round_num}-{run_id}.md"
489
581
  body_ref = _repo_relpath(REPO_ROOT, CACHE_DIR / body_basename)
490
582
  _write_cache_text(body_ref, body)
491
- if not _post_pr_comment(pr_number, body_ref):
583
+ if not _post_pr_comment(pr_number, body_ref, run_id=run_id, round_num=round_num, comment_type="review-summary"):
492
584
  _json_out({"error": "GH_PR_COMMENT_FAILED"})
493
585
  return 1
494
586
 
@@ -41,6 +41,13 @@ agent: sisyphus
41
41
 
42
42
  ## 循环(最多 3 轮)
43
43
 
44
+ **⚠️ 严格串行执行要求(Critical)**:
45
+
46
+ - 每个 Step 必须完成(收到返回值)后才能开始下一个 Step
47
+ - **禁止任何步骤并行执行**(除了 Step 2 的三个 reviewer 可并行)
48
+ - 如果任何步骤失败或超时,必须立即终止当前轮次,不能跳过或重试
49
+ - 每个步骤的 Task 调用必须 await 返回结果,不能 fire-and-forget
50
+
44
51
  每轮按顺序执行:
45
52
 
46
53
  1. Task: `pr-context` **(必须先完成,不可与 Step 2 并行)**
@@ -70,6 +77,7 @@ agent: sisyphus
70
77
  - prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`contextFile: ./.cache/<file>.md`、三条 `reviewFile: ./.cache/<file>.md`
71
78
  - 输出:`{"stop":true}` 或 `{"stop":false,"fixFile":"..."}`
72
79
  - 若 `stop=true`:本轮结束并退出循环
80
+ - **唯一性约束**: 每轮只能发布一次 Review Summary;脚本内置幂等检查,重复调用不会重复发布
73
81
 
74
82
  4. Task: `pr-fix`
75
83
 
@@ -86,7 +94,49 @@ agent: sisyphus
86
94
 
87
95
  - prompt 必须包含:`PR #{{PR_NUMBER}}`、`round: <ROUND>`、`runId: <RUN_ID>`、`fixReportFile: ./.cache/<file>.md`
88
96
  - 输出:`{"ok":true}`
97
+ - **唯一性约束**: 每轮只能发布一次 Fix Report;脚本内置幂等检查,重复调用不会重复发布
89
98
 
90
99
  6. 下一轮
91
100
 
92
101
  - 回到 1(进入下一轮 reviewers)
102
+
103
+
104
+ ## 终止与收尾(强制)
105
+
106
+ 循环结束时,必须发布一个最终评论到 PR,格式如下:
107
+
108
+ ### 情况 A: 所有问题已解决(stop=true)
109
+
110
+ 当 Step 3 返回 `{"stop":true}` 时,调用 `pr-review-aggregate` 发布收尾评论:
111
+
112
+ - prompt 必须包含:
113
+ - `PR #{{PR_NUMBER}}`
114
+ - `round: <ROUND>`
115
+ - `runId: <RUN_ID>`
116
+ - `--final-report "RESOLVED"`(新增参数,表示所有问题已解决)
117
+
118
+ ### 情况 B: 达到最大轮次(3 轮后仍有问题)
119
+
120
+ 当循环完成 3 轮后仍未 stop,调用 `pr-review-aggregate` 发布收尾评论:
121
+
122
+ - prompt 必须包含:
123
+ - `PR #{{PR_NUMBER}}`
124
+ - `round: 3`
125
+ - `runId: <RUN_ID>`
126
+ - `--final-report "MAX_ROUNDS_REACHED"`(新增参数,表示达到最大轮次)
127
+
128
+ ### 最终评论格式(由脚本生成)
129
+
130
+ ```markdown
131
+ <!-- pr-review-loop-marker -->
132
+
133
+ ## Final Report
134
+
135
+ - PR: #<PR_NUMBER>
136
+ - Total Rounds: <N>
137
+ - Status: ✅ All issues resolved / ⚠️ Max rounds reached (some issues may remain)
138
+
139
+ ### Summary
140
+
141
+ [自动生成的总结]
142
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ranger1/dx",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {