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,321 @@
1
+ """Contract-pin and flag tests for check_convergence.
2
+
3
+ ::
4
+
5
+ check_all(all gates passing) -> exit 0, "All pre-conditions met ..."
6
+ check_all(one gate failing) -> exit 1, "One or more ... do not mark ready."
7
+ _get_pr_head_sha raises SystemExit -> exit 2 propagates
8
+ parse_arguments([... --bugteam-post-blocked]) -> bugteam_post_blocked True
9
+
10
+ The pin tests fix the stdout the four consumers parse (converge.mjs, pr-converge,
11
+ bugteam, qbug) byte-for-byte, plus the exit codes and CLI surface. The flag tests
12
+ drive the ``--bugteam-post-blocked`` bypass.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import importlib.util
18
+ import sys
19
+ from pathlib import Path
20
+ from types import ModuleType
21
+
22
+ import pytest
23
+
24
+ import _pr_converge_path_setup # noqa: F401
25
+ from pr_converge_skill_constants.constants import EXIT_CODE_GH_ERROR
26
+
27
+ _SCRIPTS_DIRECTORY = Path(__file__).absolute().parent
28
+ _PR_CONVERGE_DIRECTORY = _SCRIPTS_DIRECTORY.parent
29
+
30
+ CURRENT_HEAD_SHA = "abcdef1234567890abcdef1234567890abcdef12"
31
+
32
+ _ALL_LEAF_GATE_NAMES = [
33
+ "_check_bugbot",
34
+ "_check_bugbot_not_dirty",
35
+ "_check_bugteam_clean",
36
+ "_check_bot_review",
37
+ "_count_unresolved_bot_threads",
38
+ "_get_mergeable",
39
+ "_check_no_pending_reviews",
40
+ ]
41
+
42
+ EXPECTED_ALL_PASS_STDOUT = (
43
+ "HEAD: abcdef1\n\n"
44
+ "1. bugbot_clean_at == current_head: PASS — clean\n"
45
+ "2. bugbot review body clean: PASS — clean\n"
46
+ "3. bugteam_clean_at == current_head: PASS — clean\n"
47
+ "4. copilot_clean_at == current_head: PASS — clean\n"
48
+ "5. zero unresolved bot threads: PASS — clean\n"
49
+ "6. PR is mergeable: PASS — clean\n"
50
+ "7. no pending requested reviews: PASS — clean\n\n"
51
+ "All pre-conditions met — PR is ready to mark ready.\n"
52
+ )
53
+
54
+ EXPECTED_BUGBOT_DOWN_STDOUT = (
55
+ "HEAD: abcdef1\n\n"
56
+ "1. bugbot_clean_at == current_head: PASS — bypassed (bugbot_down)\n"
57
+ "2. bugteam_clean_at == current_head: PASS — clean\n"
58
+ "3. copilot_clean_at == current_head: PASS — clean\n"
59
+ "4. zero unresolved bot threads: PASS — clean\n"
60
+ "5. PR is mergeable: PASS — clean\n"
61
+ "6. no pending requested reviews: PASS — clean\n\n"
62
+ "All pre-conditions met — PR is ready to mark ready.\n"
63
+ )
64
+
65
+ EXPECTED_COPILOT_DOWN_STDOUT = (
66
+ "HEAD: abcdef1\n\n"
67
+ "1. bugbot_clean_at == current_head: PASS — clean\n"
68
+ "2. bugbot review body clean: PASS — clean\n"
69
+ "3. bugteam_clean_at == current_head: PASS — clean\n"
70
+ "4. copilot_clean_at == current_head: PASS — bypassed (copilot_down)\n"
71
+ "5. zero unresolved bot threads: PASS — clean\n"
72
+ "6. PR is mergeable: PASS — clean\n"
73
+ "7. no pending requested reviews: PASS — bypassed (copilot_down)\n\n"
74
+ "All pre-conditions met — PR is ready to mark ready.\n"
75
+ )
76
+
77
+ EXPECTED_FAIL_SUMMARY = "One or more pre-conditions not met — do not mark ready.\n"
78
+
79
+
80
+ def _load_check_convergence() -> ModuleType:
81
+ if str(_PR_CONVERGE_DIRECTORY) not in sys.path:
82
+ sys.path.insert(0, str(_PR_CONVERGE_DIRECTORY))
83
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
84
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
85
+ module_path = _SCRIPTS_DIRECTORY / "check_convergence.py"
86
+ spec = importlib.util.spec_from_file_location(
87
+ "check_convergence_contract_under_test", module_path
88
+ )
89
+ assert spec is not None
90
+ assert spec.loader is not None
91
+ module = importlib.util.module_from_spec(spec)
92
+ spec.loader.exec_module(module)
93
+ return module
94
+
95
+
96
+ check_convergence = _load_check_convergence()
97
+
98
+
99
+ def _clean_head(**_call_keywords: object) -> str:
100
+ return CURRENT_HEAD_SHA
101
+
102
+
103
+ def _clean_gate(**_call_keywords: object) -> tuple[bool, str]:
104
+ return True, "clean"
105
+
106
+
107
+ def _refuse_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
108
+ raise AssertionError(
109
+ "_check_bugteam_clean must not run when is_bugteam_post_blocked=True"
110
+ )
111
+
112
+
113
+ def _patch_gates_clean(monkeypatch: pytest.MonkeyPatch) -> None:
114
+ monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _clean_head)
115
+ for each_gate_name in _ALL_LEAF_GATE_NAMES:
116
+ monkeypatch.setattr(check_convergence, each_gate_name, _clean_gate)
117
+
118
+
119
+ def test_all_pass_stdout_and_exit_code_match_the_pinned_contract(
120
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
121
+ ) -> None:
122
+ _patch_gates_clean(monkeypatch)
123
+ exit_code = check_convergence.check_all(
124
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
125
+ )
126
+ assert capsys.readouterr().out == EXPECTED_ALL_PASS_STDOUT
127
+ assert exit_code == 0
128
+
129
+
130
+ def test_bugbot_down_bypass_line_matches_the_pinned_contract(
131
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
132
+ ) -> None:
133
+ _patch_gates_clean(monkeypatch)
134
+ exit_code = check_convergence.check_all(
135
+ owner="o", repo="r", number=1, is_bugbot_down=True, is_copilot_down=False
136
+ )
137
+ assert capsys.readouterr().out == EXPECTED_BUGBOT_DOWN_STDOUT
138
+ assert exit_code == 0
139
+
140
+
141
+ def test_copilot_down_bypass_lines_match_the_pinned_contract(
142
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
143
+ ) -> None:
144
+ _patch_gates_clean(monkeypatch)
145
+ exit_code = check_convergence.check_all(
146
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=True
147
+ )
148
+ assert capsys.readouterr().out == EXPECTED_COPILOT_DOWN_STDOUT
149
+ assert exit_code == 0
150
+
151
+
152
+ def test_a_failing_gate_yields_exit_one_and_the_fail_summary(
153
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
154
+ ) -> None:
155
+ _patch_gates_clean(monkeypatch)
156
+
157
+ def _blocked_mergeable(**_call_keywords: object) -> tuple[bool, str]:
158
+ return False, "blocked"
159
+
160
+ monkeypatch.setattr(check_convergence, "_get_mergeable", _blocked_mergeable)
161
+ exit_code = check_convergence.check_all(
162
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
163
+ )
164
+ captured_stdout = capsys.readouterr().out
165
+ assert "6. PR is mergeable: FAIL — blocked\n" in captured_stdout
166
+ assert captured_stdout.endswith(EXPECTED_FAIL_SUMMARY)
167
+ assert exit_code == 1
168
+
169
+
170
+ def test_gh_error_exit_code_is_two_and_propagates_from_head_fetch(
171
+ monkeypatch: pytest.MonkeyPatch,
172
+ ) -> None:
173
+ def _raise_gh_error(**_call_keywords: object) -> str:
174
+ raise SystemExit(EXIT_CODE_GH_ERROR)
175
+
176
+ monkeypatch.setattr(check_convergence, "_get_pr_head_sha", _raise_gh_error)
177
+ with pytest.raises(SystemExit) as raised:
178
+ check_convergence.check_all(
179
+ owner="o", repo="r", number=1, is_bugbot_down=False, is_copilot_down=False
180
+ )
181
+ assert EXIT_CODE_GH_ERROR == 2
182
+ assert raised.value.code == 2
183
+
184
+
185
+ def test_existing_cli_flags_stay_on_the_argument_surface() -> None:
186
+ arguments = check_convergence.parse_arguments(
187
+ [
188
+ "--owner",
189
+ "o",
190
+ "--repo",
191
+ "r",
192
+ "--pr-number",
193
+ "1",
194
+ "--bugbot-down",
195
+ "--copilot-down",
196
+ ]
197
+ )
198
+ assert arguments.owner == "o"
199
+ assert arguments.repo == "r"
200
+ assert getattr(arguments, "pr_number") == 1
201
+ assert arguments.bugbot_down is True
202
+ assert arguments.copilot_down is True
203
+
204
+
205
+ def test_skip_bugteam_gate_when_bugteam_post_blocked_is_true(
206
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
207
+ ) -> None:
208
+ _patch_gates_clean(monkeypatch)
209
+ monkeypatch.setattr(
210
+ check_convergence, "_check_bugteam_clean", _refuse_bugteam_clean
211
+ )
212
+ exit_code = check_convergence.check_all(
213
+ owner="o",
214
+ repo="r",
215
+ number=1,
216
+ is_bugbot_down=False,
217
+ is_copilot_down=False,
218
+ is_bugteam_post_blocked=True,
219
+ )
220
+ assert "bypassed (bugteam_post_blocked)" in capsys.readouterr().out
221
+ assert exit_code == 0
222
+
223
+
224
+ def test_run_bugteam_gate_when_bugteam_post_blocked_is_false(
225
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
226
+ ) -> None:
227
+ invoked_gate_names: list[str] = []
228
+
229
+ def _recording_bugteam_clean(**_call_keywords: object) -> tuple[bool, str]:
230
+ invoked_gate_names.append("_check_bugteam_clean")
231
+ return True, "clean"
232
+
233
+ _patch_gates_clean(monkeypatch)
234
+ monkeypatch.setattr(
235
+ check_convergence, "_check_bugteam_clean", _recording_bugteam_clean
236
+ )
237
+ exit_code = check_convergence.check_all(
238
+ owner="o",
239
+ repo="r",
240
+ number=1,
241
+ is_bugbot_down=False,
242
+ is_copilot_down=False,
243
+ is_bugteam_post_blocked=False,
244
+ )
245
+ assert "_check_bugteam_clean" in invoked_gate_names
246
+ assert "bypassed (bugteam_post_blocked)" not in capsys.readouterr().out
247
+ assert exit_code == 0
248
+
249
+
250
+ def test_bugteam_post_blocked_flag_round_trips_through_parse_arguments() -> None:
251
+ arguments = check_convergence.parse_arguments(
252
+ ["--owner", "o", "--repo", "r", "--pr-number", "1", "--bugteam-post-blocked"]
253
+ )
254
+ assert arguments.bugteam_post_blocked is True
255
+
256
+
257
+ def test_bugteam_post_blocked_defaults_to_false_when_flag_absent() -> None:
258
+ arguments = check_convergence.parse_arguments(
259
+ ["--owner", "o", "--repo", "r", "--pr-number", "1"]
260
+ )
261
+ assert arguments.bugteam_post_blocked is False
262
+
263
+
264
+ def test_resolve_bugteam_post_blocked_true_when_flag_set(
265
+ monkeypatch: pytest.MonkeyPatch,
266
+ ) -> None:
267
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
268
+ assert check_convergence._resolve_bugteam_post_blocked(True) is True
269
+
270
+
271
+ def test_resolve_bugteam_post_blocked_true_when_env_disables_bugteam(
272
+ monkeypatch: pytest.MonkeyPatch,
273
+ ) -> None:
274
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
275
+ assert check_convergence._resolve_bugteam_post_blocked(False) is True
276
+
277
+
278
+ def test_resolve_bugteam_post_blocked_false_when_flag_unset_and_env_empty(
279
+ monkeypatch: pytest.MonkeyPatch,
280
+ ) -> None:
281
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
282
+ assert check_convergence._resolve_bugteam_post_blocked(False) is False
283
+
284
+
285
+ def test_resolve_bugteam_post_blocked_false_when_env_disables_only_copilot(
286
+ monkeypatch: pytest.MonkeyPatch,
287
+ ) -> None:
288
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "copilot")
289
+ assert check_convergence._resolve_bugteam_post_blocked(False) is False
290
+
291
+
292
+ def test_env_bugteam_token_does_not_disable_copilot_or_bugbot_opt_out(
293
+ monkeypatch: pytest.MonkeyPatch,
294
+ ) -> None:
295
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
296
+ assert check_convergence._resolve_copilot_down(False) is False
297
+ assert check_convergence._resolve_bugteam_post_blocked(False) is True
298
+
299
+
300
+ def test_main_derives_bugteam_post_blocked_from_env_when_flag_omitted(
301
+ monkeypatch: pytest.MonkeyPatch,
302
+ ) -> None:
303
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugteam")
304
+ captured_bugteam_post_blocked: list[bool] = []
305
+
306
+ def stub_check_all(
307
+ *,
308
+ owner: str,
309
+ repo: str,
310
+ number: int,
311
+ is_bugbot_down: bool,
312
+ is_copilot_down: bool,
313
+ is_bugteam_post_blocked: bool = False,
314
+ ) -> int:
315
+ captured_bugteam_post_blocked.append(is_bugteam_post_blocked)
316
+ return 0
317
+
318
+ monkeypatch.setattr(check_convergence, "check_all", stub_check_all)
319
+ exit_code = check_convergence.main(["--owner", "o", "--repo", "r", "--pr-number", "1"])
320
+ assert exit_code == 0
321
+ assert captured_bugteam_post_blocked == [True]
@@ -0,0 +1,68 @@
1
+ """Behavioral tests for the review and Bugbot convergence gate leaves.
2
+
3
+ ::
4
+
5
+ _flatten_paginated_reviews(two pages) -> newest-first flat list
6
+ _bugbot_run_conclusion_detail(success) -> (True, "check run ...")
7
+ _check_bugbot(named complete run) -> (True, "check run ...")
8
+
9
+ Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ import pytest
17
+
18
+ import check_convergence_gates as gates
19
+
20
+
21
+ def test_flatten_paginated_reviews_flattens_and_sorts_newest_first() -> None:
22
+ pages = [
23
+ [{"id": 1, "submitted_at": "2026-01-01T00:00:00Z"}],
24
+ [{"id": 2, "submitted_at": "2026-03-01T00:00:00Z"}],
25
+ ]
26
+ flattened = gates._flatten_paginated_reviews(json.dumps(pages))
27
+ assert flattened is not None
28
+ assert [each_review["id"] for each_review in flattened] == [2, 1]
29
+
30
+
31
+ def test_flatten_paginated_reviews_returns_none_for_non_list_payload() -> None:
32
+ assert (
33
+ gates._flatten_paginated_reviews(json.dumps({"message": "Not Found"})) is None
34
+ )
35
+
36
+
37
+ def test_bugbot_run_conclusion_detail_passes_on_a_complete_conclusion() -> None:
38
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
39
+ passed, detail = gates._bugbot_run_conclusion_detail(
40
+ {"id": 55, "conclusion": complete_conclusion, "html_url": ""}
41
+ )
42
+ assert passed is True
43
+ assert "check run 55" in detail
44
+
45
+
46
+ def test_check_bugbot_reports_success_when_the_named_run_is_complete(
47
+ monkeypatch: pytest.MonkeyPatch,
48
+ ) -> None:
49
+ complete_conclusion = gates.ALL_BUGBOT_CHECK_RUN_COMPLETE_CONCLUSIONS[0]
50
+ run_name = f"x {gates.BUGBOT_CHECK_RUN_NAME_SUBSTRING} y"
51
+ payload = {
52
+ "check_runs": [
53
+ {
54
+ "name": run_name,
55
+ "id": 9,
56
+ "conclusion": complete_conclusion,
57
+ "html_url": "",
58
+ }
59
+ ]
60
+ }
61
+
62
+ def _stub_gh_api(endpoint_path: str) -> tuple[int, str]:
63
+ return 0, json.dumps(payload)
64
+
65
+ monkeypatch.setattr(gates, "_gh_api", _stub_gh_api)
66
+ passed, detail = gates._check_bugbot(owner="o", repo="r", sha="abc")
67
+ assert passed is True
68
+ assert "check run 9" in detail
@@ -0,0 +1,75 @@
1
+ """Behavioral tests for the review-thread and pending-reviewer gate leaves.
2
+
3
+ ::
4
+
5
+ _is_unresolved_bot_thread(cursor, unresolved) -> True
6
+ _count_unresolved_bot_threads(one cursor thread) -> (False, "1 unresolved ...")
7
+ _format_unresolved_detail(over limit) -> "... and N more"
8
+
9
+ Each test drives the real leaf, stubbing only the ``gh`` transport helpers.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+
16
+ import pytest
17
+
18
+ import check_convergence_thread_gates as thread_gates
19
+
20
+
21
+ def test_is_unresolved_bot_thread_true_for_cursor_authored_open_thread() -> None:
22
+ thread = {
23
+ "isResolved": False,
24
+ "isOutdated": False,
25
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
26
+ }
27
+ logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
28
+ assert thread_gates._is_unresolved_bot_thread(thread, logins) is True
29
+
30
+
31
+ def test_is_unresolved_bot_thread_false_for_resolved_thread() -> None:
32
+ thread = {
33
+ "isResolved": True,
34
+ "isOutdated": False,
35
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
36
+ }
37
+ logins = (thread_gates.CURSOR_LOGIN_FILTER_SUBSTRING,)
38
+ assert thread_gates._is_unresolved_bot_thread(thread, logins) is False
39
+
40
+
41
+ def test_count_unresolved_bot_threads_counts_a_single_cursor_thread(
42
+ monkeypatch: pytest.MonkeyPatch,
43
+ ) -> None:
44
+ thread = {
45
+ "isResolved": False,
46
+ "isOutdated": False,
47
+ "path": "src/app.py",
48
+ "comments": {"nodes": [{"author": {"login": "cursor[bot]"}}]},
49
+ }
50
+ review_threads = {
51
+ "nodes": [thread],
52
+ "pageInfo": {"hasNextPage": False, "endCursor": None},
53
+ }
54
+ graphql_payload = {
55
+ "data": {"repository": {"pullRequest": {"reviewThreads": review_threads}}}
56
+ }
57
+
58
+ def _stub_gh_graphql(query: str, variables: dict[str, object]) -> tuple[int, str]:
59
+ return 0, json.dumps(graphql_payload)
60
+
61
+ monkeypatch.setattr(thread_gates, "_gh_graphql", _stub_gh_graphql)
62
+ passed, detail = thread_gates._count_unresolved_bot_threads(
63
+ owner="o", repo="r", number=1
64
+ )
65
+ assert passed is False
66
+ assert detail.startswith("1 unresolved")
67
+ assert "src/app.py" in detail
68
+
69
+
70
+ def test_format_unresolved_detail_bounds_the_listed_paths() -> None:
71
+ over_limit = thread_gates.UNRESOLVED_THREAD_DETAIL_MAX + 2
72
+ all_threads = [{"path": f"file{each_index}.py"} for each_index in range(over_limit)]
73
+ detail = thread_gates._format_unresolved_detail(all_threads)
74
+ assert detail.startswith(f"{over_limit} unresolved")
75
+ assert "more" in detail
@@ -9,56 +9,56 @@ description: >-
9
9
 
10
10
  # PR Fix Protocol
11
11
 
12
- **Core principle:** a finding is addressed only when the fix is pushed, the thread carries a reply, and the thread is resolved — as one unit of work. Anything less leaves a convergence gate stalled.
12
+ **Core principle:** a finding is addressed only when the fix is pushed, the thread carries a reply, and the thread is resolved — as one unit of work. Anything short of all three leaves a convergence gate stalled.
13
13
 
14
14
  ## How callers invoke this
15
15
 
16
- - **Skill-capable contexts** (a lead session with the `Skill` tool): `Skill({skill: "pr-fix-protocol", args: "--skill <caller> --pr <URL> --worktree <path> [findings payload or 'sweep']"})`.
17
- - **Fallback** (a subagent or teammate without the `Skill` tool): the caller's spawn prompt says "Read `~/.claude/skills/pr-fix-protocol/SKILL.md` and apply it with the parameters below."
16
+ - **Skill-capable contexts** (a lead session that holds the `Skill` tool): `Skill({skill: "pr-fix-protocol", args: "--skill <caller> --pr <URL> --worktree <path> [findings payload or 'sweep']"})`.
17
+ - **Fallback** (a subagent or teammate without the `Skill` tool): the caller's spawn prompt says to read `~/.claude/skills/pr-fix-protocol/SKILL.md` and apply it with the parameters below.
18
18
 
19
- The caller passes: its identity, the PR scope, the PR worktree path, this round's findings (or `sweep` for a thread sweep with no fresh findings), and its post-push obligations (which clean-SHA state fields to reset, which reviewers to re-trigger).
19
+ The caller passes: its identity, the PR scope, the PR worktree path, this round's findings (or `sweep` for a thread sweep with no fresh findings), and its post-push obligations which clean-SHA state fields to reset and which reviewers to re-trigger.
20
20
 
21
21
  ## Executor choice
22
22
 
23
- - **Single-PR loops** (no shared `state.json`): the lead spawns `Agent(subagent_type: "clean-coder")` to write the fix. Full-stop if `Agent` is unavailable. A spawned clean-coder does not inherit the lead's working directory name the PR worktree path in its prompt and direct it to edit, stage, and commit there.
24
- - **Multi-PR orchestration** (shared `state.json`): a per-PR clean-coder teammate owns edits, replies, and state writes; the orchestrator never edits inline. The teammate obligations (reply before writing state, state fields, idle handoff) live in the calling skill's multi-PR reference.
23
+ - **Single-PR loops** (no shared `state.json`): the lead spawns `Agent(subagent_type: "clean-coder")` to write the fix. Stop when `Agent` is unavailable. A spawned clean-coder starts in its own working directory, so its prompt names the PR worktree path and directs it to edit, stage, and commit there.
24
+ - **Multi-PR orchestration** (shared `state.json`): a per-PR clean-coder teammate owns edits, replies, and state writes; the orchestrator holds back from inline edits. The teammate obligations reply before writing state, which state fields to set, idle handoff live in the calling skill's multi-PR reference.
25
25
 
26
- Run every git command in the PR worktree `git add`, `git commit`, and `git push` act on the repo of the current working directory, so a cross-repo PR's fix lands in the PR's repo only when the cwd is its worktree.
26
+ Run every git command in the PR worktree. `git add`, `git commit`, and `git push` act on the repo of the current working directory, so a cross-repo PR's fix lands in the PR's repo only when the working directory is its worktree.
27
27
 
28
28
  ## The fix sequence
29
29
 
30
- Follow the shared 13-step sequence in [`_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md) exactly: read each file:line, capture the pre-fix SHA and contents, TDD where the finding has behavior, apply narrowly, `py_compile`, post-fix self-audit, stage by explicit path, one commit, fast-forward push, then reply + resolve atomically per thread. Its Constraints section (narrow scope, no `--no-verify`, preserve helpers) binds every executor.
30
+ Follow the shared 13-step sequence in [`_shared/pr-loop/fix-protocol.md`](../../_shared/pr-loop/fix-protocol.md) exactly: read each file:line, capture the pre-fix SHA and contents, write a failing test first where the finding has behavior, apply the fix narrowly, `py_compile`, run the post-fix self-audit, stage by explicit path, make one commit, fast-forward push, then reply and resolve atomically per thread. Its Constraints section narrow scope, keep the commit hooks, preserve helpers binds every executor.
31
31
 
32
- **Reply transport:** the GitHub MCP `add_reply_to_pull_request_comment` is primary. `python "$HOME/.claude/skills/pr-converge/scripts/post_fix_reply.py" --owner <O> --repo <R> --pr-number <N> --in-reply-to <COMMENT_ID> --body "Fixed in <SHA> — <what changed>"` is the fallback for script-only contexts and multi-PR teammates. Both carry the body shape from [`_shared/pr-loop/audit-reply-template.md`](../../_shared/pr-loop/audit-reply-template.md). For body-only findings with no inline thread, post a top-level review reply citing the new HEAD SHA.
32
+ **Reply transport:** the GitHub MCP `add_reply_to_pull_request_comment` is primary. For a script-only context or a multi-PR teammate, the fallback is `python "$HOME/.claude/skills/pr-converge/scripts/post_fix_reply.py" --owner <O> --repo <R> --pr-number <N> --in-reply-to <COMMENT_ID> --body "Fixed in <SHA> — <what changed>"`. Both carry the body shape from [`_shared/pr-loop/audit-reply-template.md`](../../_shared/pr-loop/audit-reply-template.md). For a body-only finding with no inline thread, post a top-level review reply that cites the new HEAD SHA.
33
33
 
34
- **Thread resolution:** `pull_request_review_write(method="resolve_thread", threadId=<PRRT node id>)` right after each reply. Harvest thread node ids (`PRRT_…`) from `get_review_comments` at fetch time.
34
+ **Thread resolution:** call `pull_request_review_write(method="resolve_thread", threadId=<PRRT node id>)` right after each reply. Harvest the thread node ids (`PRRT_…`) from `get_review_comments` at fetch time.
35
35
 
36
36
  ## The unresolved-thread sweep (hard gate)
37
37
 
38
- No caller advances a phase, records a clean SHA, or marks a PR ready while ANY unresolved review thread exists on the PR.
38
+ A caller advances a phase, records a clean SHA, or marks a PR ready only when the PR carries zero unresolved review threads.
39
39
 
40
40
  ```
41
41
  pull_request_read(method="get_review_comments") → filter threads where is_resolved == false
42
42
  ```
43
43
 
44
- - The filter is purely `is_resolved == false`. Author, anchor commit, and `is_outdated` are all irrelevant to the count.
45
- - `is_outdated` is informational, not a skip flag: GitHub marks a thread outdated when its anchor line changed, but the concern can still apply to current HEAD (the fix may have moved, not landed). Verify each outdated thread against current HEAD like any other.
46
- - Per unresolved thread: verify the concern against current HEAD. The concern holds → fix (this protocol) → reply → resolve. The concern does not hold on current HEAD → reply-with-note explaining why → resolve.
47
- - Code changed during the sweep → push and hand control back to the caller's re-entry phase. Only resolutions, no code → re-run the sweep without resetting caller state.
44
+ - The filter is purely `is_resolved == false`. Author, anchor commit, and `is_outdated` play no part in the count.
45
+ - `is_outdated` is informational, not a skip flag. GitHub marks a thread outdated when its anchor line changes, yet the concern can still apply to current HEAD the fix may have moved rather than landed. Verify each outdated thread against current HEAD like any other.
46
+ - Per unresolved thread: verify the concern against current HEAD. The concern holds → fix (this protocol) → reply → resolve. The concern does not hold on current HEAD → reply with a note that explains why → resolve.
47
+ - Code changed during the sweep → push and hand control back to the caller's re-entry phase. Only resolutions and no code → re-run the sweep and keep the caller's state.
48
48
 
49
49
  ## Post-push obligations
50
50
 
51
51
  Every push through this protocol:
52
52
 
53
- 1. Re-resolve `current_head` (`git rev-parse HEAD` locally; the PR `get` call for API-visible state).
53
+ 1. Re-resolve `current_head` `git rev-parse HEAD` locally, and the PR `get` call for API-visible state.
54
54
  2. Reset the caller-named clean-SHA fields to null (`bugbot_clean_at`, `code_review_clean_at`, `copilot_clean_at` — whichever the caller tracks). A new HEAD invalidates every prior clean.
55
- 3. Re-trigger reviewers per the caller's parameter (for Cursor Bugbot, the `reviewer-gates` Bugbot flow; for Copilot, the caller's request step). Audit-family callers (`bugteam`, `qbug`) skip re-triggering their next loop iteration is the reviewer.
55
+ 3. Re-trigger reviewers per the caller's parameter for Cursor Bugbot the `reviewer-gates` Bugbot flow, for Copilot the caller's request step. Audit-family callers (`bugteam`, `qbug`) skip re-triggering, since their next loop iteration is the reviewer.
56
56
 
57
57
  ## Gotchas
58
58
 
59
- - **Reply-then-resolve, atomic per thread.** Batching all replies before any resolves, or yielding to the orchestrator between the two, leaves threads half-addressed when a run dies mid-loop.
60
- - **An unchanged HEAD after step 11 means no commit landed** exit `stuck — could not address findings` rather than reporting the findings as fixed.
61
- - **A resolved thread with no reply reads as dismissed.** Reviewers (and the humans reading the PR) need the one-paragraph explanation the template carries.
59
+ - **Reply, then resolve, atomic per thread.** Batching every reply ahead of any resolve, or yielding to the orchestrator between the two, leaves threads half-addressed when a run dies mid-loop.
60
+ - **An unchanged HEAD after step 11 means no commit landed.** Exit `stuck — could not address findings` rather than report the findings as fixed.
61
+ - **A resolved thread with no reply reads as dismissed.** Reviewers and the humans reading the PR need the one-paragraph explanation the template carries.
62
62
 
63
63
  ## Folder map
64
64
 
@@ -22,8 +22,8 @@ Find and remove personal data and high-confidence secrets before they land in gi
22
22
 
23
23
  | Category | Blocked examples | Allowed residual |
24
24
  |---|---|---|
25
- | Email | `person@company.io` | `user@example.com`, `user@example.org`, `user@example.net` |
26
- | Home path | `C:/Users/realname/...`, `/Users/realname/...`, `/home/realname/...` | `C:/Users/example/...`, `C:/Users/<you>/...`, `/Users/alice/...` |
25
+ | Email | `user@example.com` | `user@example.com`, `user@example.org`, `user@example.net` |
26
+ | Home path | `C:/Users/example/...`, `/Users/example/...`, `/home/example/...` | `C:/Users/example/...`, `C:/Users/<you>/...`, `/Users/alice/...` |
27
27
  | LAN address | Unlisted `10.x` / `172.16–31.x` / `192.168.x` | Public addresses; your NAS host from `CLAUDE_NAS_HOST` or `~/.claude/local-identity.json`; entries in `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` |
28
28
  | Secret | `ghp_…`, `github_pat_…`, `AKIA…`, PEM private-key headers | Public keys, redacted `***`, env var names without values |
29
29
 
@@ -55,7 +55,7 @@ git diff --name-only origin/main...HEAD
55
55
  Run from the repo root (PowerShell-friendly example):
56
56
 
57
57
  ```
58
- rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '/home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
58
+ rg -n --hidden -g '!node_modules' -g '!.git' -e '@[A-Za-z0-9.-]+\.[A-Za-z]{2,}' -e 'Users[/\\][^/\\]+' -e '[/]home/[^/]+' -e '\b(10\.\d+\.\d+\.\d+|192\.168\.\d+\.\d+|172\.(1[6-9]|2\d|3[0-1])\.\d+\.\d+)\b' -e '\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{20,}\b' -e '\bgithub_pat_[A-Za-z0-9_]{20,}\b' -e '\bAKIA[0-9A-Z]{16}\b' -e '-----BEGIN [A-Z ]*PRIVATE KEY-----'
59
59
  ```
60
60
 
61
61
  Review each hit. Ignore:
@@ -104,6 +104,7 @@ Hooks register via `hooks/hooks.json` into `~/.claude/settings.json`. Once insta
104
104
  ## Open knobs
105
105
 
106
106
  - **NAS / LAN allowlist:** Unlisted private IPs are blocked. The scanner resolves your NAS host from `CLAUDE_NAS_HOST`, then `~/.claude/local-identity.json` (`nas.host`), and allowlists it when it is a private address, so the committed tree holds no real host. `ALL_ALLOWLISTED_PRIVATE_IP_ADDRESSES` in `hooks_constants` holds the static allowlist for any host every machine must share.
107
+ - **Commit-scan exempt repositories:** named owner/repo slugs skip the staged-commit PII scan only. Set `CLAUDE_PII_EXEMPT_REPOS` (comma-separated `owner/repo` values) or list them under `pii_exempt_repositories` in `~/.claude/local-identity.json`. Matching uses the repository's `remote.origin.url` and accepts only the exact host `github.com` (https, ssh scheme, or scp-style). A repository with no readable origin is never exempt (fail-closed to scanning). Write / Edit / MultiEdit and durable post bodies still scan in every repository.
107
108
  - **Public maintainer identity:** when a real email or name is intentional product surface, keep it and note that in the PR body so reviewers do not treat it as a leak.
108
109
 
109
110
  ## What this skill does not do
@@ -61,26 +61,44 @@ Run the pre-check once per run, not per tick or per round. Every later tick read
61
61
 
62
62
  ## Gate 3: Bugbot trigger, acknowledge, and CI-detect
63
63
 
64
- Cursor Bugbot signals through CI check runs, not always through a posted review. The decision tree runs against `<current_head>` with `check_bugbot_ci.py` (installed at `$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py`; `--help` documents every mode):
64
+ Cursor Bugbot signals through CI check runs.
65
+ Mode flags and exit codes for `check_bugbot_ci.py` live in the script docstring
66
+ — run
67
+ `python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --help`
68
+ for every mode. Caller action sequence against `<current_head>`:
65
69
 
66
70
  1. **Silent-pass pre-check (always first).**
67
71
  `python "$HOME/.claude/skills/pr-converge/scripts/check_bugbot_ci.py" --check-clean --owner <O> --repo <R> --sha <current_head>`
68
- - Exit 0 Bugbot CI completed with a `success`/`neutral` conclusion and posted no review: a silent pass. Record `bugbot_clean_at = <current_head>` and stop; do not trigger.
69
- - Exit 1 (not a silent pass) or exit 2 (gh CLI error, silent pass not confirmable) continue.
72
+ - Silent pass confirmed record `bugbot_clean_at = <current_head>` and stop; do not trigger.
73
+ - Silent pass not confirmed continue.
70
74
  2. **Already-queued check.**
71
75
  `... check_bugbot_ci.py --check-active --owner <O> --repo <R> --sha <current_head>`
72
- - Exit 0 — Bugbot is already queued on this commit. Skip posting; wait for completion (callers schedule their own wakeup).
73
- - Exit 1 continue.
74
- 3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is empirically the only re-trigger Cursor Bugbot recognizes; every other phrasing silently no-ops. Wait 8 seconds with an in-turn Monitor delay, never a foreground `sleep` (blocked in headless runs).
76
+ - Already queued skip posting; wait for completion (callers schedule their own wakeup).
77
+ - Not active continue.
78
+ 3. **Trigger.** Post exactly `bugbot run` as an issue comment (`add_issue_comment`) — no `@cursor[bot]` mention, no other text. `bugbot run` is the only re-trigger Cursor Bugbot recognizes; every other phrasing silently no-ops. Wait 8 seconds with an in-turn Monitor delay, never a foreground `sleep` (blocked in headless runs).
75
79
  4. **Acknowledge check.**
76
80
  `... check_bugbot_ci.py --owner <O> --repo <R> --sha <current_head>`
77
- - Exit 0 — a check run is present: record `bugbot_acknowledged_at = <now, ISO 8601>`; the caller polls on its own cadence.
78
- - Exit non-zero Bugbot is down: set `bugbot_down = true` and route past the Bugbot phase; the run continues on its remaining signals.
81
+ - Check run present record `bugbot_acknowledged_at = <now, ISO 8601>`; the caller polls on its own cadence.
82
+ - Check run absent Bugbot is down: set `bugbot_down = true` and route past the Bugbot phase; the run continues on its remaining signals.
79
83
 
80
84
  The silent-pass pre-check runs first so a bot that already finished cleanly is never re-prompted — Bugbot refuses to re-run on an evaluated commit, and without the pre-check the acknowledge step would falsely mark `bugbot_down = true`.
81
85
 
82
86
  **Flag reset rule:** `bugbot_down` resets to false whenever `<current_head>` changes — a new HEAD invalidates the old down-detection. `copilot_down` never resets mid-run; it holds until the run ends.
83
87
 
88
+ ## Convergence-check bypass flags
89
+
90
+ When a reviewer is down, `check_convergence.py` must skip that reviewer's gate so the run still marks ready on its remaining signals. Each condition has one CLI flag and one matching `CLAUDE_REVIEWS_DISABLED` token:
91
+
92
+ | Condition | `check_convergence.py` flag | `CLAUDE_REVIEWS_DISABLED` token |
93
+ |---|---|---|
94
+ | Bugbot down or off | `--bugbot-down` | `bugbot` |
95
+ | Copilot down or opted out | `--copilot-down` | `copilot` |
96
+ | Bugteam CLEAN post blocked | `--bugteam-post-blocked` | `bugteam` |
97
+
98
+ The caller passes the flag on its own `check_convergence.py` run. The flag alone does not reach the independent mark-ready blocker hook, which re-runs `check_convergence.py` with no flags on `gh pr ready`. Each flag also honors its token: `check_convergence.py` reads the same bypass from `CLAUDE_REVIEWS_DISABLED`. So export the matching token in the same shell before `gh pr ready`, and the hook's no-flag re-check inherits the bypass through the env. Combine tokens with commas when more than one reviewer is down (`CLAUDE_REVIEWS_DISABLED="copilot,bugteam"`).
99
+
100
+ The `bugteam` token here means the bugteam CLEAN-review gate is skipped for this convergence check. It does not disable the bugbot or copilot gates: each token is scoped to its own reviewer.
101
+
84
102
  ## Gotchas
85
103
 
86
104
  - **`bugbot run` is load-bearing text.** Alternative phrasings (`@cursor run`, `cursor review`, `bugbot please`) return no error and do nothing.