claude-dev-env 1.93.1 → 1.95.0

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.
Files changed (262) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +35 -27
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +3 -2
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +23 -15
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +81 -17
  6. package/_shared/pr-loop/CLAUDE.md +1 -0
  7. package/_shared/pr-loop/audit-contract.md +1 -1
  8. package/_shared/pr-loop/gh-payloads.md +3 -3
  9. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  10. package/_shared/pr-loop/scripts/README.md +2 -2
  11. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  13. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  14. package/_shared/pr-loop/state-schema.md +3 -22
  15. package/bin/CLAUDE.md +11 -2
  16. package/bin/ever-shipped-skills.mjs +70 -0
  17. package/bin/install.mjs +138 -13
  18. package/bin/install.prune.test.mjs +457 -0
  19. package/bin/install.test.mjs +10 -4
  20. package/docs/CODE_RULES.md +1 -1
  21. package/hooks/blocking/CLAUDE.md +2 -0
  22. package/hooks/blocking/code_rules_enforcer.py +4 -0
  23. package/hooks/blocking/code_rules_shared.py +82 -0
  24. package/hooks/blocking/code_rules_test_layout.py +9 -3
  25. package/hooks/blocking/pii_commit_command.py +398 -0
  26. package/hooks/blocking/pii_payload_scan.py +133 -0
  27. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  28. package/hooks/blocking/plain_language_blocker.py +138 -4
  29. package/hooks/blocking/sensitive_file_protector.py +114 -48
  30. package/hooks/blocking/tdd_enforcer.py +9 -2
  31. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  32. package/hooks/blocking/test_code_rules_shared.py +181 -0
  33. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  34. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  35. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  36. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  37. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  38. package/hooks/hooks_constants/CLAUDE.md +4 -2
  39. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  40. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  41. package/hooks/hooks_constants/local_identity.py +65 -15
  42. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  43. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  44. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  45. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  46. package/hooks/hooks_constants/test_local_identity.py +68 -0
  47. package/hooks/pyproject.toml +75 -4
  48. package/hooks/validators/CLAUDE.md +1 -1
  49. package/hooks/validators/README.md +2 -0
  50. package/hooks/validators/python_style_checks.py +114 -136
  51. package/hooks/validators/python_style_helpers.py +95 -0
  52. package/hooks/validators/test_python_style_checks.py +0 -164
  53. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  54. package/hooks/validators/test_python_style_fixes.py +251 -0
  55. package/hooks/validators/test_python_style_helpers.py +125 -0
  56. package/package.json +1 -1
  57. package/rules/CLAUDE.md +1 -0
  58. package/rules/anti-corollary-tests.md +69 -0
  59. package/rules/bdd.md +1 -3
  60. package/rules/code-reviews.md +1 -1
  61. package/rules/gh-paginate.md +1 -1
  62. package/rules/plain-language.md +2 -0
  63. package/scripts/CLAUDE.md +10 -1
  64. package/scripts/Get-SessionAccount.ps1 +210 -0
  65. package/scripts/tests/CLAUDE.md +15 -1
  66. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  67. package/skills/CLAUDE.md +6 -27
  68. package/skills/anthropic-plan/SKILL.md +1 -13
  69. package/skills/autoconverge/CLAUDE.md +5 -2
  70. package/skills/autoconverge/SKILL.md +112 -387
  71. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  72. package/skills/autoconverge/reference/closing-report.md +9 -2
  73. package/skills/autoconverge/reference/convergence.md +26 -22
  74. package/skills/autoconverge/reference/headless-safety.md +44 -0
  75. package/skills/autoconverge/reference/multi-pr.md +88 -0
  76. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  77. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  78. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  79. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  80. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  81. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  82. package/skills/autoconverge/workflow/converge.mjs +286 -96
  83. package/skills/bugteam/CLAUDE.md +1 -1
  84. package/skills/bugteam/CONSTRAINTS.md +3 -3
  85. package/skills/bugteam/PROMPTS.md +8 -19
  86. package/skills/bugteam/SKILL.md +13 -14
  87. package/skills/bugteam/reference/CLAUDE.md +1 -1
  88. package/skills/bugteam/reference/README.md +2 -3
  89. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  90. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  91. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  92. package/skills/bugteam/reference/team-setup.md +11 -11
  93. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  94. package/skills/bugteam/scripts/README.md +20 -8
  95. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  96. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  97. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  98. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  99. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  100. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  101. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  102. package/skills/closeout/SKILL.md +153 -0
  103. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  104. package/skills/closeout/reference/issue-body-templates.md +108 -0
  105. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  106. package/skills/copilot-finding-triage/SKILL.md +80 -28
  107. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  108. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  109. package/skills/everything-search/SKILL.md +1 -2
  110. package/skills/grokify/SKILL.md +58 -0
  111. package/skills/grokify/templates/handoff-template.md +92 -0
  112. package/skills/orchestrator/SKILL.md +35 -25
  113. package/skills/orchestrator-refresh/SKILL.md +15 -11
  114. package/skills/pr-converge/CLAUDE.md +1 -1
  115. package/skills/pr-converge/SKILL.md +132 -233
  116. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  117. package/skills/pr-converge/reference/examples.md +37 -25
  118. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  119. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  120. package/skills/pr-converge/reference/per-tick.md +64 -41
  121. package/skills/pr-converge/reference/state-schema.md +27 -2
  122. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  123. package/skills/pr-converge/scripts/README.md +4 -3
  124. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  125. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  126. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  127. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  128. package/skills/pr-converge/scripts/conftest.py +8 -0
  129. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  130. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  131. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  132. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  133. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  134. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  135. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  136. package/skills/pr-fix-protocol/SKILL.md +20 -20
  137. package/skills/privacy-hygiene/SKILL.md +4 -3
  138. package/skills/reviewer-gates/SKILL.md +26 -8
  139. package/skills/session-log/SKILL.md +11 -12
  140. package/skills/session-tidy/SKILL.md +40 -30
  141. package/skills/skill-builder/CLAUDE.md +13 -10
  142. package/skills/skill-builder/SKILL.md +31 -31
  143. package/skills/skill-builder/references/CLAUDE.md +10 -6
  144. package/skills/skill-builder/references/delegation-map.md +13 -4
  145. package/skills/skill-builder/references/description-field.md +113 -0
  146. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  147. package/skills/skill-builder/references/skill-modularity.md +113 -0
  148. package/skills/skill-builder/references/skill-types.md +4 -0
  149. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  150. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  151. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  152. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  153. package/skills/skill-builder/workflows/new-skill.md +34 -10
  154. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  155. package/skills/team-advisor/SKILL.md +15 -11
  156. package/skills/usage-pause/SKILL.md +10 -14
  157. package/system-prompts/software-engineer.xml +7 -6
  158. package/hooks/validators/test_verify_paths.py +0 -32
  159. package/hooks/validators/verify_paths.py +0 -57
  160. package/skills/bdd-protocol/CLAUDE.md +0 -26
  161. package/skills/bdd-protocol/SKILL.md +0 -30
  162. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  163. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  164. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  165. package/skills/bg-agent/CLAUDE.md +0 -17
  166. package/skills/bg-agent/SKILL.md +0 -69
  167. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  168. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  169. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  170. package/skills/caveman/CLAUDE.md +0 -15
  171. package/skills/caveman/SKILL.md +0 -38
  172. package/skills/code/CLAUDE.md +0 -17
  173. package/skills/code/SKILL.md +0 -178
  174. package/skills/deep-research/CLAUDE.md +0 -17
  175. package/skills/deep-research/SKILL.md +0 -80
  176. package/skills/findbugs/CLAUDE.md +0 -20
  177. package/skills/findbugs/SKILL.md +0 -241
  178. package/skills/fixbugs/CLAUDE.md +0 -19
  179. package/skills/fixbugs/SKILL.md +0 -142
  180. package/skills/gh-paginate/CLAUDE.md +0 -18
  181. package/skills/gh-paginate/SKILL.md +0 -84
  182. package/skills/gotcha/CLAUDE.md +0 -33
  183. package/skills/gotcha/SKILL.md +0 -73
  184. package/skills/implement/CLAUDE.md +0 -27
  185. package/skills/implement/SKILL.md +0 -66
  186. package/skills/implement/scripts/CLAUDE.md +0 -22
  187. package/skills/implement/scripts/append_note.py +0 -133
  188. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  189. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  190. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  191. package/skills/implement/scripts/test_append_note.py +0 -191
  192. package/skills/log-audit/CLAUDE.md +0 -20
  193. package/skills/log-audit/SKILL.md +0 -66
  194. package/skills/log-audit/reference/CLAUDE.md +0 -9
  195. package/skills/log-audit/reference/charter.md +0 -52
  196. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  197. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  198. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  199. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  200. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  201. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  202. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  203. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  204. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  205. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  206. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  207. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  208. package/skills/logifix/CLAUDE.md +0 -36
  209. package/skills/logifix/SKILL.md +0 -69
  210. package/skills/logifix/scripts/CLAUDE.md +0 -16
  211. package/skills/logifix/scripts/logifix.ps1 +0 -205
  212. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  213. package/skills/monitor-open-prs/SKILL.md +0 -88
  214. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  215. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  216. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  217. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  218. package/skills/post-audit-findings/SKILL.md +0 -80
  219. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  220. package/skills/pr-consistency-audit/SKILL.md +0 -116
  221. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  222. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  223. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  224. package/skills/pr-review-responder/CLAUDE.md +0 -35
  225. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  226. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  227. package/skills/pr-review-responder/README.md +0 -168
  228. package/skills/pr-review-responder/SKILL.md +0 -203
  229. package/skills/pr-review-responder/TESTING.md +0 -350
  230. package/skills/pr-scope-resolve/SKILL.md +0 -45
  231. package/skills/pre-compact/CLAUDE.md +0 -24
  232. package/skills/pre-compact/SKILL.md +0 -134
  233. package/skills/qbug/CLAUDE.md +0 -40
  234. package/skills/qbug/SKILL.md +0 -387
  235. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  236. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  237. package/skills/refine/CLAUDE.md +0 -44
  238. package/skills/refine/SKILL.md +0 -262
  239. package/skills/refine/templates/CLAUDE.md +0 -17
  240. package/skills/refine/templates/implementation-notes-template.html +0 -56
  241. package/skills/refine/templates/plan-template.md +0 -60
  242. package/skills/research-mode/CLAUDE.md +0 -35
  243. package/skills/research-mode/SKILL.md +0 -53
  244. package/skills/structure-prompt/CLAUDE.md +0 -42
  245. package/skills/structure-prompt/SKILL.md +0 -44
  246. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  247. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  248. package/skills/structure-prompt/reference/block-classification.md +0 -27
  249. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  250. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  251. package/skills/structure-prompt/reference/cleanup.md +0 -33
  252. package/skills/structure-prompt/reference/constraints.md +0 -33
  253. package/skills/structure-prompt/reference/directives.md +0 -37
  254. package/skills/structure-prompt/reference/examples.md +0 -72
  255. package/skills/structure-prompt/reference/instantiation.md +0 -51
  256. package/skills/structure-prompt/reference/output-contract.md +0 -72
  257. package/skills/structure-prompt/reference/per-category.md +0 -23
  258. package/skills/structure-prompt/reference/persona.md +0 -38
  259. package/skills/structure-prompt/reference/research.md +0 -33
  260. package/skills/structure-prompt/reference/structure.md +0 -28
  261. package/skills/verified-build/CLAUDE.md +0 -33
  262. package/skills/verified-build/SKILL.md +0 -33
@@ -0,0 +1,235 @@
1
+ """Review and Bugbot IO leaves for the convergence gate checks.
2
+
3
+ Each function issues one ``gh`` REST call and returns a ``(passed, detail)``
4
+ pair the convergence checker turns into a PASS/FAIL line, or a raw
5
+ ``(returncode, stdout)`` pair for the low-level callers. The checker re-imports
6
+ these names, so a test patches them on the checker module.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+
16
+ import _pr_converge_path_setup # noqa: F401
17
+ from pr_converge_skill_constants.constants import (
18
+ ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS,
19
+ BUGBOT_CHECK_RUN_NAME_SUBSTRING,
20
+ BUGBOT_DIRTY_BODY_REGEX,
21
+ CHECK_RUNS_PER_PAGE,
22
+ CURSOR_LOGIN_FILTER_SUBSTRING,
23
+ EXIT_CODE_GH_ERROR,
24
+ GH_CHECK_RUNS_PATH_TEMPLATE,
25
+ GH_PR_OBJECT_PATH_TEMPLATE,
26
+ GH_REVIEWS_PATH_TEMPLATE,
27
+ REVIEWS_PER_PAGE,
28
+ )
29
+ from pr_converge_scripts_constants.convergence_gate_constants import SHORT_SHA_LENGTH
30
+
31
+ JsonObject = dict[str, object]
32
+ ReviewStateGroup = tuple[str, ...]
33
+
34
+
35
+ def _run_gh_command(all_arguments: list[str]) -> tuple[int, str]:
36
+ completed_process = subprocess.run(
37
+ all_arguments,
38
+ capture_output=True,
39
+ text=True,
40
+ encoding="utf-8",
41
+ errors="replace",
42
+ check=False,
43
+ )
44
+ return completed_process.returncode, completed_process.stdout
45
+
46
+
47
+ def _gh_api(endpoint_path: str) -> tuple[int, str]:
48
+ return _run_gh_command(["gh", "api", endpoint_path])
49
+
50
+
51
+ def _gh_api_paginated(endpoint_path: str) -> tuple[int, str]:
52
+ return _run_gh_command(["gh", "api", endpoint_path, "--paginate", "--slurp"])
53
+
54
+
55
+ def _short_sha(commit_id: str) -> str:
56
+ return commit_id[:SHORT_SHA_LENGTH]
57
+
58
+
59
+ def _get_pr_head_sha(*, owner: str, repo: str, number: int) -> str:
60
+ """Return the PR HEAD SHA, exiting with the gh-error code when the call fails."""
61
+ endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
62
+ returncode, stdout = _gh_api(endpoint)
63
+ if returncode != 0:
64
+ sys.stderr.write(f"gh api error fetching PR object: {stdout}\n")
65
+ raise SystemExit(EXIT_CODE_GH_ERROR)
66
+ pr_object = json.loads(stdout)
67
+ head_sha: object = pr_object.get("head", {}).get("sha")
68
+ if not isinstance(head_sha, str):
69
+ raise SystemExit(EXIT_CODE_GH_ERROR)
70
+ return head_sha
71
+
72
+
73
+ def _get_mergeable(*, owner: str, repo: str, number: int) -> tuple[bool, str]:
74
+ """Return whether the PR is cleanly mergeable, with the mergeable-state detail."""
75
+ endpoint = GH_PR_OBJECT_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
76
+ returncode, stdout = _gh_api(endpoint)
77
+ if returncode != 0:
78
+ return False, f"gh api error: {stdout}"
79
+ pr_object = json.loads(stdout)
80
+ mergeable: object = pr_object.get("mergeable")
81
+ mergeable_state: object = pr_object.get("mergeable_state", "unknown")
82
+ state_text = str(mergeable_state)
83
+ if mergeable is True and state_text == "clean":
84
+ return True, "clean"
85
+ return False, state_text
86
+
87
+
88
+ def _bugbot_check_runs(
89
+ *, owner: str, repo: str, sha: str
90
+ ) -> tuple[int, list[JsonObject]]:
91
+ """Fetch the check runs for a SHA and return the returncode and run dicts."""
92
+ endpoint = GH_CHECK_RUNS_PATH_TEMPLATE.format(owner=owner, repo=repo, sha=sha)
93
+ returncode, stdout = _gh_api(f"{endpoint}?per_page={CHECK_RUNS_PER_PAGE}")
94
+ if returncode != 0:
95
+ return returncode, []
96
+ try:
97
+ payload = json.loads(stdout)
98
+ except json.JSONDecodeError:
99
+ return returncode, []
100
+ raw_runs = payload.get("check_runs") if isinstance(payload, dict) else None
101
+ if not isinstance(raw_runs, list):
102
+ return returncode, []
103
+ return returncode, [each_run for each_run in raw_runs if isinstance(each_run, dict)]
104
+
105
+
106
+ def _bugbot_run_conclusion_detail(check_run: JsonObject) -> tuple[bool, str]:
107
+ """Return the pass flag and detail for one bugbot check run."""
108
+ conclusion = check_run.get("conclusion", "")
109
+ if conclusion in ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS:
110
+ check_id = check_run.get("id", "?")
111
+ detail_url = check_run.get("html_url", "")
112
+ details_suffix = f" ({detail_url})" if detail_url else ""
113
+ return True, f"check run {check_id}, conclusion: {conclusion}{details_suffix}"
114
+ return (
115
+ False,
116
+ f"check run conclusion is '{conclusion}', expected {ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS}",
117
+ )
118
+
119
+
120
+ def _check_bugbot(*, owner: str, repo: str, sha: str) -> tuple[bool, str]:
121
+ """Return whether the Cursor Bugbot check run on a SHA completed successfully."""
122
+ returncode, check_runs = _bugbot_check_runs(owner=owner, repo=repo, sha=sha)
123
+ if returncode != 0:
124
+ return False, "gh api error fetching check runs"
125
+ for each_check_run in check_runs:
126
+ each_name = each_check_run.get("name", "")
127
+ if not isinstance(each_name, str):
128
+ continue
129
+ if BUGBOT_CHECK_RUN_NAME_SUBSTRING.lower() not in each_name.lower():
130
+ continue
131
+ return _bugbot_run_conclusion_detail(each_check_run)
132
+ return False, "no bugbot check run found"
133
+
134
+
135
+ def _flatten_paginated_reviews(stdout: str) -> list[JsonObject] | None:
136
+ """Flatten a slurped, paginated reviews payload into a newest-first list."""
137
+ try:
138
+ reviews_payload = json.loads(stdout)
139
+ except json.JSONDecodeError:
140
+ return None
141
+ if not isinstance(reviews_payload, list):
142
+ return None
143
+ all_flat: list[JsonObject] = [
144
+ each_entry
145
+ for each_page in reviews_payload
146
+ if isinstance(each_page, list)
147
+ for each_entry in each_page
148
+ if isinstance(each_entry, dict)
149
+ ]
150
+ all_flat.sort(
151
+ key=lambda each_review: str(each_review.get("submitted_at", "")), reverse=True
152
+ )
153
+ return all_flat
154
+
155
+
156
+ def _review_matches_login(review: JsonObject, login_substring: str) -> bool:
157
+ user_object = review.get("user")
158
+ if not isinstance(user_object, dict):
159
+ return False
160
+ login = user_object.get("login", "")
161
+ return isinstance(login, str) and login_substring in login.lower()
162
+
163
+
164
+ def _review_commit_starts_with(review: JsonObject, head_sha: str) -> bool:
165
+ commit_id = review.get("commit_id", "")
166
+ return isinstance(commit_id, str) and commit_id.startswith(head_sha)
167
+
168
+
169
+ def _check_bugbot_not_dirty(
170
+ *, owner: str, repo: str, number: int, head_sha: str
171
+ ) -> tuple[bool, str]:
172
+ """Return whether the newest Cursor Bugbot review on HEAD reports no findings."""
173
+ endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
174
+ returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
175
+ if returncode != 0:
176
+ return True, "bugbot reviews unavailable (non-fatal)"
177
+ all_flat = _flatten_paginated_reviews(stdout)
178
+ if all_flat is None:
179
+ return True, "bugbot reviews not valid JSON (non-fatal)"
180
+ dirty_pattern = re.compile(BUGBOT_DIRTY_BODY_REGEX, re.IGNORECASE)
181
+ for each_review in all_flat:
182
+ if not _review_matches_login(each_review, CURSOR_LOGIN_FILTER_SUBSTRING):
183
+ continue
184
+ if not _review_commit_starts_with(each_review, head_sha):
185
+ continue
186
+ body = each_review.get("body", "")
187
+ if isinstance(body, str) and dirty_pattern.search(body):
188
+ return False, "bugbot review body reports findings"
189
+ return True, "clean"
190
+ return True, "no bugbot review at HEAD"
191
+
192
+
193
+ def _bot_review_state_detail(
194
+ review: JsonObject,
195
+ clean_states: ReviewStateGroup,
196
+ dirty_states: ReviewStateGroup,
197
+ ) -> tuple[bool, str]:
198
+ """Return the pass flag and detail for one bot review's state."""
199
+ review_state = review.get("state", "")
200
+ commit_id = review.get("commit_id", "")
201
+ short_commit = _short_sha(commit_id) if isinstance(commit_id, str) else "?"
202
+ if review_state in clean_states:
203
+ review_id = review.get("id", "?")
204
+ return True, f"review {review_id}, state: {review_state}, commit {short_commit}"
205
+ if review_state in dirty_states:
206
+ return False, f"review state is '{review_state}' (dirty), commit {short_commit}"
207
+ return False, f"review state is '{review_state}', commit {short_commit}"
208
+
209
+
210
+ def _check_bot_review(
211
+ *,
212
+ owner: str,
213
+ repo: str,
214
+ number: int,
215
+ head_sha: str,
216
+ login_substring: str,
217
+ clean_states: ReviewStateGroup,
218
+ dirty_states: ReviewStateGroup,
219
+ label: str,
220
+ ) -> tuple[bool, str]:
221
+ """Return whether the newest matching bot review on HEAD is in a clean state."""
222
+ endpoint = GH_REVIEWS_PATH_TEMPLATE.format(owner=owner, repo=repo, number=number)
223
+ returncode, stdout = _gh_api_paginated(f"{endpoint}?per_page={REVIEWS_PER_PAGE}")
224
+ if returncode != 0:
225
+ return False, f"gh api error: {stdout}"
226
+ all_flat = _flatten_paginated_reviews(stdout)
227
+ if all_flat is None:
228
+ return False, f"no {label} review found"
229
+ for each_review in all_flat:
230
+ if not _review_matches_login(each_review, login_substring):
231
+ continue
232
+ if not _review_commit_starts_with(each_review, head_sha):
233
+ continue
234
+ return _bot_review_state_detail(each_review, clean_states, dirty_states)
235
+ return False, f"no {label} review found on {_short_sha(head_sha)}"
@@ -0,0 +1,219 @@
1
+ """Review-thread and pending-reviewer IO leaves for the convergence gate checks.
2
+
3
+ Each function issues one ``gh`` call (GraphQL or REST) and returns a
4
+ ``(passed, detail)`` pair the convergence checker turns into a PASS/FAIL line.
5
+ The checker re-imports these names, so a test patches them on the checker module.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+
12
+ import _pr_converge_path_setup # noqa: F401
13
+ from check_convergence_gates import (
14
+ JsonObject,
15
+ ReviewStateGroup,
16
+ _gh_api,
17
+ _run_gh_command,
18
+ )
19
+ from pr_converge_skill_constants.constants import (
20
+ CLAUDE_LOGIN_FILTER_SUBSTRING,
21
+ COPILOT_LOGIN_FILTER_SUBSTRING,
22
+ COPILOT_REVIEWER_LOGIN,
23
+ CURSOR_LOGIN_FILTER_SUBSTRING,
24
+ GH_REQUESTED_REVIEWERS_PATH_TEMPLATE,
25
+ GRAPHQL_REVIEW_THREADS_PAGE_SIZE,
26
+ UNRESOLVED_THREAD_DETAIL_MAX,
27
+ )
28
+ from pr_converge_scripts_constants.convergence_gate_constants import (
29
+ GRAPHQL_REVIEW_THREADS_QUERY,
30
+ PENDING_REVIEWER_JOIN_SEPARATOR,
31
+ THREAD_PATH_JOIN_SEPARATOR,
32
+ )
33
+
34
+
35
+ def _gh_graphql(query: str, variables: JsonObject) -> tuple[int, str]:
36
+ """Run a ``gh api graphql`` query with variables and return returncode and stdout."""
37
+ all_arguments: list[str] = ["gh", "api", "graphql", "-f", f"query={query}"]
38
+ for each_key, each_variable in variables.items():
39
+ if each_variable is None:
40
+ continue
41
+ flag = "-F" if isinstance(each_variable, int) else "-f"
42
+ all_arguments.extend([flag, f"{each_key}={each_variable}"])
43
+ return _run_gh_command(all_arguments)
44
+
45
+
46
+ def _thread_author_login(thread: JsonObject) -> str:
47
+ """Return the login of a review thread's first comment author, or empty string."""
48
+ comments_wrapper = thread.get("comments", {})
49
+ if not isinstance(comments_wrapper, dict):
50
+ return ""
51
+ comments_nodes = comments_wrapper.get("nodes", [])
52
+ if not isinstance(comments_nodes, list) or not comments_nodes:
53
+ return ""
54
+ first_comment = comments_nodes[0]
55
+ author_wrapper = (
56
+ first_comment.get("author") if isinstance(first_comment, dict) else None
57
+ )
58
+ if not isinstance(author_wrapper, dict):
59
+ return ""
60
+ login = author_wrapper.get("login", "")
61
+ return login if isinstance(login, str) else ""
62
+
63
+
64
+ def _is_unresolved_bot_thread(thread: JsonObject, bot_logins: ReviewStateGroup) -> bool:
65
+ """Return whether a review thread is unresolved, current, and bot-authored."""
66
+ if thread.get("isResolved") is True or thread.get("isOutdated") is True:
67
+ return False
68
+ login = _thread_author_login(thread).lower()
69
+ return any(each_bot in login for each_bot in bot_logins)
70
+
71
+
72
+ def _unresolved_bot_threads_in_page(
73
+ nodes: object, bot_logins: ReviewStateGroup
74
+ ) -> list[JsonObject]:
75
+ """Return the unresolved bot-authored threads among one page of thread nodes."""
76
+ if not isinstance(nodes, list):
77
+ return []
78
+ return [
79
+ each_thread
80
+ for each_thread in nodes
81
+ if isinstance(each_thread, dict)
82
+ and _is_unresolved_bot_thread(each_thread, bot_logins)
83
+ ]
84
+
85
+
86
+ def _review_threads_container(graphql_payload: object) -> JsonObject | None:
87
+ """Return the ``reviewThreads`` object from a GraphQL payload, or None."""
88
+ if not isinstance(graphql_payload, dict):
89
+ return None
90
+ envelope = graphql_payload.get("data", {})
91
+ repository = envelope.get("repository", {}) if isinstance(envelope, dict) else {}
92
+ pull_request = (
93
+ repository.get("pullRequest", {}) if isinstance(repository, dict) else {}
94
+ )
95
+ threads = (
96
+ pull_request.get("reviewThreads", {}) if isinstance(pull_request, dict) else {}
97
+ )
98
+ return threads if isinstance(threads, dict) else None
99
+
100
+
101
+ def _fetch_review_threads_page(
102
+ *, owner: str, repo: str, number: int, cursor: str | None
103
+ ) -> tuple[str, object, object]:
104
+ """Fetch one page of review threads and return an error text, nodes, and page info."""
105
+ variables: JsonObject = {
106
+ "owner": owner,
107
+ "repo": repo,
108
+ "number": number,
109
+ "first": GRAPHQL_REVIEW_THREADS_PAGE_SIZE,
110
+ "cursor": cursor,
111
+ }
112
+ returncode, stdout = _gh_graphql(GRAPHQL_REVIEW_THREADS_QUERY, variables)
113
+ if returncode != 0:
114
+ return f"gh api graphql error: {stdout}", [], {}
115
+ try:
116
+ graphql_payload = json.loads(stdout)
117
+ except json.JSONDecodeError:
118
+ return "gh api graphql response not valid JSON", [], {}
119
+ threads = _review_threads_container(graphql_payload)
120
+ if threads is None:
121
+ return "unexpected GraphQL response shape", [], {}
122
+ return "", threads.get("nodes", []), threads.get("pageInfo", {})
123
+
124
+
125
+ def _next_thread_cursor(page_info: object) -> str | None:
126
+ """Return the next pagination cursor, or None when there is no next page."""
127
+ if not isinstance(page_info, dict):
128
+ return None
129
+ if not page_info.get("hasNextPage"):
130
+ return None
131
+ next_cursor = page_info.get("endCursor")
132
+ return next_cursor if isinstance(next_cursor, str) else None
133
+
134
+
135
+ def _format_unresolved_detail(all_unresolved: list[JsonObject]) -> str:
136
+ """Format the unresolved-thread count and a bounded list of their paths."""
137
+ detail_parts = [
138
+ str(each_thread.get("path", "?"))
139
+ for each_thread in all_unresolved[:UNRESOLVED_THREAD_DETAIL_MAX]
140
+ ]
141
+ detail_text = THREAD_PATH_JOIN_SEPARATOR.join(detail_parts)
142
+ if len(all_unresolved) > UNRESOLVED_THREAD_DETAIL_MAX:
143
+ detail_text += (
144
+ f" ... and {len(all_unresolved) - UNRESOLVED_THREAD_DETAIL_MAX} more"
145
+ )
146
+ return f"{len(all_unresolved)} unresolved ({detail_text})"
147
+
148
+
149
+ def _count_unresolved_bot_threads(
150
+ *, owner: str, repo: str, number: int
151
+ ) -> tuple[bool, str]:
152
+ """Return whether zero unresolved bot review threads remain on the PR."""
153
+ bot_logins = (
154
+ CURSOR_LOGIN_FILTER_SUBSTRING,
155
+ CLAUDE_LOGIN_FILTER_SUBSTRING,
156
+ COPILOT_LOGIN_FILTER_SUBSTRING,
157
+ )
158
+ all_unresolved: list[JsonObject] = []
159
+ cursor: str | None = None
160
+ while True:
161
+ error_text, nodes, page_info = _fetch_review_threads_page(
162
+ owner=owner, repo=repo, number=number, cursor=cursor
163
+ )
164
+ if error_text:
165
+ return False, error_text
166
+ all_unresolved.extend(_unresolved_bot_threads_in_page(nodes, bot_logins))
167
+ cursor = _next_thread_cursor(page_info)
168
+ if cursor is None:
169
+ break
170
+ if not all_unresolved:
171
+ return True, "0 unresolved"
172
+ return False, _format_unresolved_detail(all_unresolved)
173
+
174
+
175
+ def _requested_reviewer_users(pending_payload: object) -> list[object]:
176
+ if isinstance(pending_payload, dict):
177
+ users = pending_payload.get("users", [])
178
+ elif isinstance(pending_payload, list):
179
+ users = pending_payload
180
+ else:
181
+ return []
182
+ return users if isinstance(users, list) else []
183
+
184
+
185
+ def _pending_copilot_logins(all_users: list[object]) -> list[str]:
186
+ """Return the Copilot reviewer logins still pending among requested reviewers."""
187
+ pending: list[str] = []
188
+ for each_user in all_users:
189
+ if not isinstance(each_user, dict):
190
+ continue
191
+ login = each_user.get("login", "")
192
+ if isinstance(login, str) and COPILOT_REVIEWER_LOGIN.lower() in login.lower():
193
+ pending.append(login)
194
+ return pending
195
+
196
+
197
+ def _check_no_pending_reviews(
198
+ *, owner: str, repo: str, number: int
199
+ ) -> tuple[bool, str]:
200
+ """Return whether no Copilot review request is still pending on the PR."""
201
+ endpoint = GH_REQUESTED_REVIEWERS_PATH_TEMPLATE.format(
202
+ owner=owner, repo=repo, number=number
203
+ )
204
+ returncode, stdout = _gh_api(endpoint)
205
+ if returncode != 0:
206
+ return False, f"gh api error: {stdout}"
207
+ try:
208
+ pending_payload = json.loads(stdout)
209
+ except json.JSONDecodeError:
210
+ return True, "no pending (empty response)"
211
+ copilot_pending = _pending_copilot_logins(
212
+ _requested_reviewer_users(pending_payload)
213
+ )
214
+ if copilot_pending:
215
+ return (
216
+ False,
217
+ f"pending: {PENDING_REVIEWER_JOIN_SEPARATOR.join(copilot_pending)}",
218
+ )
219
+ return True, "no pending reviewers"
@@ -0,0 +1,8 @@
1
+ """Put the pr-converge scripts directory on sys.path for the test suite."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _scripts_directory = Path(__file__).resolve().parent
7
+ if str(_scripts_directory) not in sys.path:
8
+ sys.path.insert(0, str(_scripts_directory))
@@ -9,6 +9,7 @@ Constants module for the `pr-converge` skill's Python scripts. Re-exports all ru
9
9
  | `__init__.py` | Package marker |
10
10
  | `pr_converge_constants.py` | Re-exports all constants from `pr_converge_skill_constants.constants` plus adds script-specific values |
11
11
  | `reflow_skill_md_constants.py` | Constants for `reflow_skill_md.py`: line width limit, list item patterns, `TARGET_SKILL_PATH` |
12
+ | `convergence_gate_constants.py` | Constants for `check_convergence_gates.py`: short-SHA length, join separators, and the review-threads GraphQL query |
12
13
 
13
14
  ## Conventions
14
15
 
@@ -0,0 +1,36 @@
1
+ """Named constants for the convergence gate IO leaves.
2
+
3
+ Holds the short-SHA length, the join separators for gate detail strings, and
4
+ the GraphQL query that lists a PR's review threads.
5
+ """
6
+
7
+ SHORT_SHA_LENGTH: int = 7
8
+
9
+ THREAD_PATH_JOIN_SEPARATOR: str = "; "
10
+
11
+ PENDING_REVIEWER_JOIN_SEPARATOR: str = ", "
12
+
13
+ GRAPHQL_REVIEW_THREADS_QUERY: str = """
14
+ query($owner: String!, $repo: String!, $number: Int!, $first: Int!, $cursor: String) {
15
+ repository(owner: $owner, name: $repo) {
16
+ pullRequest(number: $number) {
17
+ reviewThreads(first: $first, after: $cursor) {
18
+ nodes {
19
+ isResolved
20
+ isOutdated
21
+ path
22
+ comments(first: 1) {
23
+ nodes {
24
+ author { login }
25
+ }
26
+ }
27
+ }
28
+ pageInfo {
29
+ hasNextPage
30
+ endCursor
31
+ }
32
+ }
33
+ }
34
+ }
35
+ }
36
+ """
@@ -0,0 +1,26 @@
1
+ """Importing _pr_converge_path_setup registers the skill and shared directories.
2
+
3
+ ::
4
+
5
+ import _pr_converge_path_setup
6
+ ok: str(skill_directory) in sys.path
7
+ ok: str(shared_pr_loop_scripts_directory) in sys.path
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ import _pr_converge_path_setup # noqa: F401
16
+
17
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
18
+
19
+
20
+ def test_path_setup_registers_skill_and_shared_directories() -> None:
21
+ skill_directory = _SCRIPTS_DIRECTORY.parent
22
+ shared_directory = (
23
+ _SCRIPTS_DIRECTORY.parent.parent.parent / "_shared" / "pr-loop" / "scripts"
24
+ )
25
+ assert str(skill_directory) in sys.path
26
+ assert str(shared_directory) in sys.path