claude-dev-env 1.93.1 → 1.94.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 (215) hide show
  1. package/_shared/pr-loop/CLAUDE.md +1 -0
  2. package/_shared/pr-loop/audit-contract.md +1 -1
  3. package/_shared/pr-loop/gh-payloads.md +3 -3
  4. package/_shared/pr-loop/post-audit-thread-contract.md +51 -0
  5. package/_shared/pr-loop/scripts/README.md +2 -2
  6. package/_shared/pr-loop/scripts/fix_hookspath.py +47 -12
  7. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -69
  8. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -374
  9. package/_shared/pr-loop/state-schema.md +3 -22
  10. package/bin/CLAUDE.md +1 -1
  11. package/bin/install.mjs +2 -7
  12. package/bin/install.test.mjs +10 -4
  13. package/hooks/blocking/CLAUDE.md +2 -0
  14. package/hooks/blocking/pii_commit_command.py +398 -0
  15. package/hooks/blocking/pii_payload_scan.py +133 -0
  16. package/hooks/blocking/pii_prevention_blocker.py +151 -449
  17. package/hooks/blocking/test_pii_prevention_blocker.py +299 -0
  18. package/hooks/blocking/test_verdict_directory_write_blocker.py +42 -7
  19. package/hooks/hooks_constants/CLAUDE.md +2 -2
  20. package/hooks/hooks_constants/code_rules_path_utils_constants.py +0 -2
  21. package/hooks/hooks_constants/local_identity.py +65 -15
  22. package/hooks/hooks_constants/pii_prevention_constants.py +16 -0
  23. package/hooks/hooks_constants/pr_description_enforcer_constants.py +1 -1
  24. package/hooks/hooks_constants/test_local_identity.py +68 -0
  25. package/package.json +1 -1
  26. package/scripts/CLAUDE.md +10 -1
  27. package/scripts/Get-SessionAccount.ps1 +210 -0
  28. package/scripts/tests/CLAUDE.md +15 -1
  29. package/scripts/tests/Get-SessionAccount.Tests.ps1 +350 -0
  30. package/skills/CLAUDE.md +3 -25
  31. package/skills/anthropic-plan/SKILL.md +1 -13
  32. package/skills/autoconverge/CLAUDE.md +5 -2
  33. package/skills/autoconverge/SKILL.md +112 -387
  34. package/skills/autoconverge/reference/CLAUDE.md +4 -1
  35. package/skills/autoconverge/reference/closing-report.md +9 -2
  36. package/skills/autoconverge/reference/convergence.md +26 -22
  37. package/skills/autoconverge/reference/headless-safety.md +44 -0
  38. package/skills/autoconverge/reference/multi-pr.md +88 -0
  39. package/skills/autoconverge/reference/self-closing-loop.md +84 -0
  40. package/skills/autoconverge/reference/stop-conditions.md +21 -13
  41. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +87 -27
  42. package/skills/autoconverge/workflow/converge.contract.test.mjs +40 -10
  43. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +179 -47
  44. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  45. package/skills/autoconverge/workflow/converge.mjs +284 -94
  46. package/skills/bugteam/CLAUDE.md +1 -1
  47. package/skills/bugteam/CONSTRAINTS.md +3 -3
  48. package/skills/bugteam/PROMPTS.md +8 -19
  49. package/skills/bugteam/SKILL.md +13 -14
  50. package/skills/bugteam/reference/CLAUDE.md +1 -1
  51. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  52. package/skills/bugteam/reference/copilot-gap-analysis.md +2 -0
  53. package/skills/bugteam/reference/github-pr-reviews.md +56 -38
  54. package/skills/bugteam/reference/team-setup.md +11 -11
  55. package/skills/bugteam/scripts/CLAUDE.md +6 -6
  56. package/skills/bugteam/scripts/README.md +20 -8
  57. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +13 -1463
  58. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +23 -358
  59. package/skills/bugteam/scripts/bugteam_preflight.py +13 -315
  60. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +2 -3
  61. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +34 -1053
  62. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +30 -347
  63. package/skills/bugteam/scripts/test_bugteam_preflight.py +30 -305
  64. package/skills/copilot-finding-triage/SKILL.md +80 -28
  65. package/skills/copilot-finding-triage/reference/tier-rubric.md +70 -11
  66. package/skills/copilot-finding-triage/templates/notification.md +8 -3
  67. package/skills/everything-search/SKILL.md +1 -2
  68. package/skills/grokify/SKILL.md +58 -0
  69. package/skills/grokify/templates/handoff-template.md +92 -0
  70. package/skills/orchestrator/SKILL.md +12 -8
  71. package/skills/orchestrator-refresh/SKILL.md +3 -3
  72. package/skills/pr-converge/SKILL.md +132 -233
  73. package/skills/pr-converge/reference/convergence-gates.md +102 -66
  74. package/skills/pr-converge/reference/examples.md +37 -25
  75. package/skills/pr-converge/reference/fix-protocol.md +7 -7
  76. package/skills/pr-converge/reference/multi-pr-orchestration.md +10 -4
  77. package/skills/pr-converge/reference/per-tick.md +64 -41
  78. package/skills/pr-converge/reference/state-schema.md +27 -2
  79. package/skills/pr-converge/scripts/CLAUDE.md +8 -0
  80. package/skills/pr-converge/scripts/README.md +4 -3
  81. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +23 -0
  82. package/skills/pr-converge/scripts/check_convergence.py +189 -645
  83. package/skills/pr-converge/scripts/check_convergence_gates.py +235 -0
  84. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +219 -0
  85. package/skills/pr-converge/scripts/conftest.py +8 -0
  86. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -0
  87. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -0
  88. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +26 -0
  89. package/skills/pr-converge/scripts/test_check_convergence.py +73 -228
  90. package/skills/pr-converge/scripts/test_check_convergence_contract.py +321 -0
  91. package/skills/pr-converge/scripts/test_check_convergence_gates.py +68 -0
  92. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +75 -0
  93. package/skills/privacy-hygiene/SKILL.md +4 -3
  94. package/skills/reviewer-gates/SKILL.md +26 -8
  95. package/skills/session-log/SKILL.md +11 -12
  96. package/skills/session-tidy/SKILL.md +40 -30
  97. package/skills/skill-builder/CLAUDE.md +13 -10
  98. package/skills/skill-builder/SKILL.md +31 -31
  99. package/skills/skill-builder/references/CLAUDE.md +10 -6
  100. package/skills/skill-builder/references/delegation-map.md +13 -4
  101. package/skills/skill-builder/references/description-field.md +113 -0
  102. package/skills/skill-builder/references/self-audit-checklist.md +21 -4
  103. package/skills/skill-builder/references/skill-modularity.md +113 -0
  104. package/skills/skill-builder/references/skill-types.md +4 -0
  105. package/skills/skill-builder/templates/CLAUDE.md +4 -4
  106. package/skills/skill-builder/templates/gap-analysis.md +25 -1
  107. package/skills/skill-builder/workflows/CLAUDE.md +6 -4
  108. package/skills/skill-builder/workflows/improve-skill.md +17 -16
  109. package/skills/skill-builder/workflows/new-skill.md +34 -10
  110. package/skills/skill-builder/workflows/polish-skill.md +59 -43
  111. package/skills/usage-pause/SKILL.md +10 -14
  112. package/skills/bdd-protocol/CLAUDE.md +0 -26
  113. package/skills/bdd-protocol/SKILL.md +0 -30
  114. package/skills/bdd-protocol/references/CLAUDE.md +0 -10
  115. package/skills/bdd-protocol/references/anti-patterns.md +0 -26
  116. package/skills/bdd-protocol/references/example-mapping.md +0 -23
  117. package/skills/bg-agent/CLAUDE.md +0 -17
  118. package/skills/bg-agent/SKILL.md +0 -69
  119. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +0 -55
  120. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_fix_hookspath_constants.py +0 -26
  121. package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_preflight_constants.py +0 -35
  122. package/skills/caveman/CLAUDE.md +0 -15
  123. package/skills/caveman/SKILL.md +0 -38
  124. package/skills/code/CLAUDE.md +0 -17
  125. package/skills/code/SKILL.md +0 -178
  126. package/skills/deep-research/CLAUDE.md +0 -17
  127. package/skills/deep-research/SKILL.md +0 -80
  128. package/skills/findbugs/CLAUDE.md +0 -20
  129. package/skills/findbugs/SKILL.md +0 -241
  130. package/skills/fixbugs/CLAUDE.md +0 -19
  131. package/skills/fixbugs/SKILL.md +0 -142
  132. package/skills/gh-paginate/CLAUDE.md +0 -18
  133. package/skills/gh-paginate/SKILL.md +0 -84
  134. package/skills/gotcha/CLAUDE.md +0 -33
  135. package/skills/gotcha/SKILL.md +0 -73
  136. package/skills/implement/CLAUDE.md +0 -27
  137. package/skills/implement/SKILL.md +0 -66
  138. package/skills/implement/scripts/CLAUDE.md +0 -22
  139. package/skills/implement/scripts/append_note.py +0 -133
  140. package/skills/implement/scripts/implement_scripts_constants/CLAUDE.md +0 -22
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +0 -12
  143. package/skills/implement/scripts/test_append_note.py +0 -191
  144. package/skills/log-audit/CLAUDE.md +0 -20
  145. package/skills/log-audit/SKILL.md +0 -66
  146. package/skills/log-audit/reference/CLAUDE.md +0 -9
  147. package/skills/log-audit/reference/charter.md +0 -52
  148. package/skills/log-audit/scripts/CLAUDE.md +0 -27
  149. package/skills/log-audit/scripts/cluster_recurrences.py +0 -261
  150. package/skills/log-audit/scripts/collect_log_window.py +0 -199
  151. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +0 -12
  152. package/skills/log-audit/scripts/log_audit_constants/__init__.py +0 -0
  153. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +0 -23
  154. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +0 -24
  155. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +0 -49
  156. package/skills/log-audit/scripts/mine_copilot_findings.py +0 -302
  157. package/skills/log-audit/scripts/test_cluster_recurrences.py +0 -160
  158. package/skills/log-audit/scripts/test_collect_log_window.py +0 -111
  159. package/skills/log-audit/scripts/test_mine_copilot_findings.py +0 -126
  160. package/skills/logifix/CLAUDE.md +0 -36
  161. package/skills/logifix/SKILL.md +0 -69
  162. package/skills/logifix/scripts/CLAUDE.md +0 -16
  163. package/skills/logifix/scripts/logifix.ps1 +0 -205
  164. package/skills/monitor-open-prs/CLAUDE.md +0 -34
  165. package/skills/monitor-open-prs/SKILL.md +0 -88
  166. package/skills/monitor-open-prs/scripts/CLAUDE.md +0 -17
  167. package/skills/monitor-open-prs/scripts/discover_open_prs.py +0 -69
  168. package/skills/monitor-open-prs/scripts/test_discover_open_prs.py +0 -149
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -27
  170. package/skills/post-audit-findings/SKILL.md +0 -80
  171. package/skills/pr-consistency-audit/CLAUDE.md +0 -34
  172. package/skills/pr-consistency-audit/SKILL.md +0 -116
  173. package/skills/pr-consistency-audit/reference/CLAUDE.md +0 -16
  174. package/skills/pr-consistency-audit/reference/detection-rules.md +0 -96
  175. package/skills/pr-consistency-audit/reference/illustrations.md +0 -78
  176. package/skills/pr-fix-protocol/SKILL.md +0 -65
  177. package/skills/pr-review-responder/CLAUDE.md +0 -35
  178. package/skills/pr-review-responder/EXAMPLES.md +0 -590
  179. package/skills/pr-review-responder/PRINCIPLES.md +0 -533
  180. package/skills/pr-review-responder/README.md +0 -168
  181. package/skills/pr-review-responder/SKILL.md +0 -203
  182. package/skills/pr-review-responder/TESTING.md +0 -350
  183. package/skills/pr-scope-resolve/SKILL.md +0 -45
  184. package/skills/pre-compact/CLAUDE.md +0 -24
  185. package/skills/pre-compact/SKILL.md +0 -134
  186. package/skills/qbug/CLAUDE.md +0 -40
  187. package/skills/qbug/SKILL.md +0 -387
  188. package/skills/qbug/test_qbug_skill_audit_schema.py +0 -152
  189. package/skills/qbug/test_qbug_skill_post_fix_audit.py +0 -103
  190. package/skills/refine/CLAUDE.md +0 -44
  191. package/skills/refine/SKILL.md +0 -262
  192. package/skills/refine/templates/CLAUDE.md +0 -17
  193. package/skills/refine/templates/implementation-notes-template.html +0 -56
  194. package/skills/refine/templates/plan-template.md +0 -60
  195. package/skills/research-mode/CLAUDE.md +0 -35
  196. package/skills/research-mode/SKILL.md +0 -53
  197. package/skills/structure-prompt/CLAUDE.md +0 -42
  198. package/skills/structure-prompt/SKILL.md +0 -44
  199. package/skills/structure-prompt/reference/CLAUDE.md +0 -28
  200. package/skills/structure-prompt/reference/adversarial-tuning.md +0 -62
  201. package/skills/structure-prompt/reference/block-classification.md +0 -27
  202. package/skills/structure-prompt/reference/canonical-case.md +0 -48
  203. package/skills/structure-prompt/reference/citation-depth.md +0 -70
  204. package/skills/structure-prompt/reference/cleanup.md +0 -33
  205. package/skills/structure-prompt/reference/constraints.md +0 -33
  206. package/skills/structure-prompt/reference/directives.md +0 -37
  207. package/skills/structure-prompt/reference/examples.md +0 -72
  208. package/skills/structure-prompt/reference/instantiation.md +0 -51
  209. package/skills/structure-prompt/reference/output-contract.md +0 -72
  210. package/skills/structure-prompt/reference/per-category.md +0 -23
  211. package/skills/structure-prompt/reference/persona.md +0 -38
  212. package/skills/structure-prompt/reference/research.md +0 -33
  213. package/skills/structure-prompt/reference/structure.md +0 -28
  214. package/skills/verified-build/CLAUDE.md +0 -33
  215. package/skills/verified-build/SKILL.md +0 -33
@@ -1,302 +0,0 @@
1
- """Turn the defects reviewers keep catching into upstream skill-edit proposals.
2
-
3
- Copilot and Bugbot flag the same kinds of defect over and over — an untyped
4
- return here, a broad except there. Each catch is a defect that reached review
5
- because nothing blocked it at write time. This script reads recent reviewer
6
- comments, sorts them into defect classes, and for each class names a concrete
7
- skill or rule edit that would block that class upstream. The proposals are for a
8
- human to apply through review, not for the agent to commit on its own.
9
-
10
- Usage:
11
- mine_copilot_findings.py --repo owner/name
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import argparse
17
- import json
18
- import subprocess
19
- import sys
20
- from collections import defaultdict
21
- from dataclasses import dataclass
22
- from pathlib import Path
23
-
24
- _script_directory = str(Path(__file__).resolve().parent)
25
- if _script_directory not in sys.path:
26
- sys.path.insert(0, _script_directory)
27
-
28
- from log_audit_constants.mine_copilot_findings_constants import ( # noqa: E402
29
- ALL_REVIEWER_BOT_LOGINS,
30
- KEYWORDS_BY_DEFECT_CLASS,
31
- MAX_EXAMPLES_PER_CLUSTER,
32
- PROPOSAL_BY_DEFECT_CLASS,
33
- PULL_COMMENTS_ENDPOINT_TEMPLATE,
34
- RECENT_PULL_COUNT,
35
- RECENT_PULLS_ENDPOINT_TEMPLATE,
36
- )
37
-
38
-
39
- class GitHubCommandError(RuntimeError):
40
- """Raised when a gh api call fails or returns output that is not JSON."""
41
-
42
-
43
- @dataclass(frozen=True)
44
- class ReviewerComment:
45
- """One review comment left by a reviewer bot.
46
-
47
- Attributes:
48
- pull_number: The pull request the comment was left on.
49
- author: The reviewer bot's login.
50
- body: The comment text.
51
- """
52
-
53
- pull_number: int
54
- author: str
55
- body: str
56
-
57
-
58
- @dataclass(frozen=True)
59
- class DefectCluster:
60
- """The reviewer comments that fall into one defect class.
61
-
62
- Attributes:
63
- defect_class: The class name the comments were sorted into.
64
- count: How many comments fell into the class.
65
- example_bodies: A capped sample of the comment bodies in the class.
66
- """
67
-
68
- defect_class: str
69
- count: int
70
- example_bodies: tuple[str, ...]
71
-
72
-
73
- @dataclass(frozen=True)
74
- class SkillProposal:
75
- """A proposed skill-definition edit that blocks one defect class upstream.
76
-
77
- Attributes:
78
- defect_class: The defect class the edit would block.
79
- proposal: The concrete edit to make.
80
- """
81
-
82
- defect_class: str
83
- proposal: str
84
-
85
-
86
- def classify_defect(comment_body: str) -> str | None:
87
- """Sort a review comment into a defect class by its keywords.
88
-
89
- Args:
90
- comment_body: The review comment text.
91
-
92
- Returns:
93
- The first defect class whose keywords appear in the comment, or None
94
- when no class matches.
95
- """
96
- lowered_body = comment_body.lower()
97
- for each_defect_class, each_keywords in KEYWORDS_BY_DEFECT_CLASS.items():
98
- if any(each_keyword in lowered_body for each_keyword in each_keywords):
99
- return each_defect_class
100
- return None
101
-
102
-
103
- def cluster_defects(all_comments: list[ReviewerComment]) -> list[DefectCluster]:
104
- """Group review comments by defect class and rank by how often each recurs.
105
-
106
- Args:
107
- all_comments: The reviewer comments to sort.
108
-
109
- Returns:
110
- One cluster per defect class present, most frequent first, each with a
111
- capped sample of its comment bodies.
112
- """
113
- bodies_by_class: dict[str, list[str]] = defaultdict(list)
114
- for each_comment in all_comments:
115
- defect_class = classify_defect(each_comment.body)
116
- if defect_class is None:
117
- continue
118
- bodies_by_class[defect_class].append(each_comment.body)
119
- clusters: list[DefectCluster] = []
120
- for each_defect_class, each_bodies in bodies_by_class.items():
121
- clusters.append(
122
- DefectCluster(
123
- defect_class=each_defect_class,
124
- count=len(each_bodies),
125
- example_bodies=tuple(each_bodies[:MAX_EXAMPLES_PER_CLUSTER]),
126
- )
127
- )
128
- return sorted(clusters, key=lambda cluster: cluster.count, reverse=True)
129
-
130
-
131
- def proposal_for_defect_class(defect_class: str) -> SkillProposal:
132
- """Return the skill-edit proposal that blocks a defect class upstream.
133
-
134
- Args:
135
- defect_class: The defect class to propose an edit for.
136
-
137
- Returns:
138
- The proposal mapped to the class.
139
- """
140
- return SkillProposal(
141
- defect_class=defect_class, proposal=PROPOSAL_BY_DEFECT_CLASS[defect_class]
142
- )
143
-
144
-
145
- def _pull_number_from_url(pull_request_url: str) -> int | None:
146
- """Read the trailing pull number from a GitHub pull-request URL."""
147
- last_segment = pull_request_url.rsplit("/", 1)[-1]
148
- try:
149
- return int(last_segment)
150
- except ValueError:
151
- return None
152
-
153
-
154
- def _reviewer_comment_from_payload(payload: object) -> ReviewerComment | None:
155
- """Build a ReviewerComment from one gh comment payload, or None to skip it."""
156
- if not isinstance(payload, dict):
157
- return None
158
- user_field = payload.get("user")
159
- login = user_field.get("login") if isinstance(user_field, dict) else None
160
- body = payload.get("body")
161
- pull_request_url = payload.get("pull_request_url")
162
- if not isinstance(login, str) or login not in ALL_REVIEWER_BOT_LOGINS:
163
- return None
164
- if not isinstance(body, str) or not isinstance(pull_request_url, str):
165
- return None
166
- pull_number = _pull_number_from_url(pull_request_url)
167
- if pull_number is None:
168
- return None
169
- return ReviewerComment(pull_number=pull_number, author=login, body=body)
170
-
171
-
172
- def _flatten_pages(slurped_payload: object) -> list[object]:
173
- """Flatten the per-page arrays a gh --slurp response holds into one list.
174
-
175
- Args:
176
- slurped_payload: The parsed output of a ``gh api --paginate --slurp``
177
- call: a list whose entries are the individual page responses.
178
-
179
- Returns:
180
- Every entry across the pages in page order. A page that is itself a list
181
- contributes its entries; any other page contributes itself.
182
- """
183
- flattened_entries: list[object] = []
184
- if isinstance(slurped_payload, list):
185
- for each_page in slurped_payload:
186
- if isinstance(each_page, list):
187
- flattened_entries.extend(each_page)
188
- else:
189
- flattened_entries.append(each_page)
190
- return flattened_entries
191
-
192
-
193
- def _run_gh_json(endpoint: str, *, should_paginate: bool = False) -> object:
194
- """Run one gh api call for an endpoint and return its parsed JSON payload.
195
-
196
- Args:
197
- endpoint: The gh api endpoint path to request.
198
- should_paginate: When True, follow pagination with ``--paginate
199
- --slurp`` and flatten the per-page arrays into one list, so a list
200
- endpoint with more than one page returns every entry rather than
201
- only the first page.
202
-
203
- Returns:
204
- The parsed JSON payload: the flattened entry list when should_paginate
205
- is True, otherwise the single response parsed as-is.
206
-
207
- Raises:
208
- GitHubCommandError: When gh is not installed, the gh api call fails, or
209
- its output is not JSON.
210
- """
211
- gh_command = ["gh", "api", endpoint]
212
- if should_paginate:
213
- gh_command += ["--paginate", "--slurp"]
214
- try:
215
- completed_process = subprocess.run(
216
- gh_command,
217
- capture_output=True,
218
- text=True,
219
- check=True,
220
- )
221
- except FileNotFoundError as missing_gh_error:
222
- raise GitHubCommandError(
223
- "gh command not found; install the GitHub CLI to mine reviewer comments"
224
- ) from missing_gh_error
225
- except subprocess.CalledProcessError as gh_failure_error:
226
- raise GitHubCommandError(
227
- f"gh api {endpoint} failed: {gh_failure_error.stderr.strip()}"
228
- ) from gh_failure_error
229
- try:
230
- parsed_payload = json.loads(completed_process.stdout)
231
- except json.JSONDecodeError as decode_error:
232
- raise GitHubCommandError(
233
- f"gh api {endpoint} returned output that is not JSON"
234
- ) from decode_error
235
- if should_paginate:
236
- return _flatten_pages(parsed_payload)
237
- return parsed_payload
238
-
239
-
240
- def _fetch_recent_pull_numbers(repo: str, pull_count: int) -> list[int]:
241
- """Fetch the most recently updated pull numbers, capped at pull_count."""
242
- pulls_payload = _run_gh_json(
243
- RECENT_PULLS_ENDPOINT_TEMPLATE.format(repo=repo, pull_count=pull_count)
244
- )
245
- recent_pull_numbers: list[int] = []
246
- if isinstance(pulls_payload, list):
247
- for each_pull in pulls_payload:
248
- if isinstance(each_pull, dict):
249
- pull_number = each_pull.get("number")
250
- if isinstance(pull_number, int):
251
- recent_pull_numbers.append(pull_number)
252
- return recent_pull_numbers
253
-
254
-
255
- def _fetch_pull_comments(repo: str, pull_number: int) -> list[object]:
256
- """Fetch one pull request's review comments through gh."""
257
- comments_payload = _run_gh_json(
258
- PULL_COMMENTS_ENDPOINT_TEMPLATE.format(repo=repo, pull_number=pull_number),
259
- should_paginate=True,
260
- )
261
- if isinstance(comments_payload, list):
262
- return list(comments_payload)
263
- return []
264
-
265
-
266
- def _fetch_reviewer_comments(repo: str) -> list[ReviewerComment]:
267
- """Fetch reviewer-bot comments from the repo's most recent pull requests."""
268
- recent_pull_numbers = _fetch_recent_pull_numbers(repo, RECENT_PULL_COUNT)
269
- reviewer_comments: list[ReviewerComment] = []
270
- for each_pull_number in recent_pull_numbers:
271
- for each_payload in _fetch_pull_comments(repo, each_pull_number):
272
- parsed_comment = _reviewer_comment_from_payload(each_payload)
273
- if parsed_comment is not None:
274
- reviewer_comments.append(parsed_comment)
275
- return reviewer_comments
276
-
277
-
278
- def main() -> int:
279
- """Print one skill-edit proposal per recurring reviewer defect class.
280
-
281
- Returns:
282
- The process exit code; zero on success.
283
- """
284
- parser = argparse.ArgumentParser(
285
- description="Mine reviewer defect patterns into skill-edit proposals."
286
- )
287
- parser.add_argument("--repo", required=True)
288
- parsed_arguments = parser.parse_args()
289
- try:
290
- comments = _fetch_reviewer_comments(parsed_arguments.repo)
291
- except GitHubCommandError as gh_error:
292
- print(str(gh_error), file=sys.stderr)
293
- return 1
294
- clusters = cluster_defects(comments)
295
- for each_cluster in clusters:
296
- proposal = proposal_for_defect_class(each_cluster.defect_class)
297
- print(f"{each_cluster.count}\t{each_cluster.defect_class}\t{proposal.proposal}")
298
- return 0
299
-
300
-
301
- if __name__ == "__main__":
302
- raise SystemExit(main())
@@ -1,160 +0,0 @@
1
- """Tests for cluster_recurrences — signature grouping and timing regressions."""
2
-
3
- import io
4
- import sys
5
- from datetime import datetime, timedelta
6
- from pathlib import Path
7
-
8
- import pytest
9
-
10
- _script_directory = str(Path(__file__).resolve().parent)
11
- if _script_directory not in sys.path:
12
- sys.path.insert(0, _script_directory)
13
-
14
- from cluster_recurrences import ( # noqa: E402
15
- TimingSample,
16
- detect_timing_regressions,
17
- main,
18
- normalize_signature,
19
- recency_weight,
20
- rank_signature_clusters,
21
- )
22
- from collect_log_window import LogRecord # noqa: E402
23
-
24
-
25
- def _record(timestamp: datetime, message: str) -> LogRecord:
26
- return LogRecord(
27
- timestamp=timestamp, source="a_hook", level="block", message=message
28
- )
29
-
30
-
31
- class TestNormalizeSignature:
32
- def test_two_messages_differing_only_in_numbers_share_a_signature(self) -> None:
33
- first = normalize_signature("blocked at line 12 with 3 issues")
34
- second = normalize_signature("blocked at line 348 with 9 issues")
35
- assert first == second
36
-
37
- def test_messages_differing_only_in_a_path_share_a_signature(self) -> None:
38
- first = normalize_signature(r"cannot read C:\a\b\one.py right now")
39
- second = normalize_signature(r"cannot read C:\x\y\two.py right now")
40
- assert first == second
41
-
42
- def test_messages_differing_only_in_a_hash_share_a_signature(self) -> None:
43
- first = normalize_signature("commit a1b2c3d failed to apply")
44
- second = normalize_signature("commit deadbeef failed to apply")
45
- assert first == second
46
-
47
- def test_distinct_messages_keep_distinct_signatures(self) -> None:
48
- assert normalize_signature("heavy word detected") != normalize_signature(
49
- "missing type hint"
50
- )
51
-
52
-
53
- class TestRecencyWeight:
54
- def test_a_fresh_record_weighs_about_one(self) -> None:
55
- now = datetime(2026, 7, 3, 12, 0, 0)
56
- weight = recency_weight(now, now)
57
- assert abs(weight - 1.0) < 0.001
58
-
59
- def test_a_record_one_half_life_old_weighs_about_the_decay_base(self) -> None:
60
- now = datetime(2026, 7, 3, 12, 0, 0)
61
- one_half_life_ago = now - timedelta(hours=24)
62
- weight = recency_weight(one_half_life_ago, now)
63
- assert abs(weight - 0.5) < 0.001
64
-
65
- def test_a_future_record_is_clamped_to_at_most_one(self) -> None:
66
- now = datetime(2026, 7, 3, 12, 0, 0)
67
- one_hour_ahead = now + timedelta(hours=1)
68
- weight = recency_weight(one_hour_ahead, now)
69
- assert abs(weight - 1.0) < 0.001
70
-
71
-
72
- class TestRankSignatureClusters:
73
- def test_the_most_frequent_recent_signature_ranks_first(self) -> None:
74
- now = datetime(2026, 7, 3, 12, 0, 0)
75
- records = [
76
- _record(now - timedelta(hours=1), "blocked at line 1"),
77
- _record(now - timedelta(hours=2), "blocked at line 2"),
78
- _record(now - timedelta(hours=1), "blocked at line 3"),
79
- _record(now - timedelta(hours=200), "a rare stale failure"),
80
- ]
81
- clusters = rank_signature_clusters(records, now)
82
- assert clusters[0].count == 3
83
- assert clusters[0].signature == normalize_signature("blocked at line 1")
84
-
85
- def test_each_distinct_signature_becomes_one_cluster(self) -> None:
86
- now = datetime(2026, 7, 3, 12, 0, 0)
87
- records = [
88
- _record(now, "heavy word here"),
89
- _record(now, "missing type hint"),
90
- ]
91
- clusters = rank_signature_clusters(records, now)
92
- assert len(clusters) == 2
93
-
94
-
95
- class TestDetectTimingRegressions:
96
- def test_flags_an_operation_whose_recent_runs_are_slower(self) -> None:
97
- base = datetime(2026, 7, 3, 0, 0, 0)
98
- samples = [
99
- TimingSample("extract", base + timedelta(minutes=1), 100.0),
100
- TimingSample("extract", base + timedelta(minutes=2), 110.0),
101
- TimingSample("extract", base + timedelta(minutes=3), 105.0),
102
- TimingSample("extract", base + timedelta(minutes=40), 300.0),
103
- TimingSample("extract", base + timedelta(minutes=41), 320.0),
104
- TimingSample("extract", base + timedelta(minutes=42), 310.0),
105
- ]
106
- regressions = detect_timing_regressions(samples)
107
- assert len(regressions) == 1
108
- assert regressions[0].operation == "extract"
109
- assert regressions[0].ratio > 1.5
110
-
111
- def test_ignores_a_steady_operation(self) -> None:
112
- base = datetime(2026, 7, 3, 0, 0, 0)
113
- samples = [
114
- TimingSample("steady", base + timedelta(minutes=each_minute_offset), 100.0)
115
- for each_minute_offset in range(6)
116
- ]
117
- assert detect_timing_regressions(samples) == []
118
-
119
- def test_ignores_an_operation_with_too_few_samples(self) -> None:
120
- base = datetime(2026, 7, 3, 0, 0, 0)
121
- samples = [
122
- TimingSample("sparse", base + timedelta(minutes=1), 100.0),
123
- TimingSample("sparse", base + timedelta(minutes=2), 900.0),
124
- ]
125
- assert detect_timing_regressions(samples) == []
126
-
127
-
128
- class TestMain:
129
- def test_rejects_non_json_stdin(
130
- self,
131
- monkeypatch: pytest.MonkeyPatch,
132
- capsys: pytest.CaptureFixture[str],
133
- ) -> None:
134
- monkeypatch.setattr(sys, "stdin", io.StringIO("not json"))
135
- exit_code = main()
136
- captured = capsys.readouterr()
137
- assert exit_code == 1
138
- assert "not valid JSON" in captured.err
139
-
140
- def test_rejects_a_non_list_payload(
141
- self,
142
- monkeypatch: pytest.MonkeyPatch,
143
- capsys: pytest.CaptureFixture[str],
144
- ) -> None:
145
- monkeypatch.setattr(sys, "stdin", io.StringIO('{"not": "a list"}'))
146
- exit_code = main()
147
- captured = capsys.readouterr()
148
- assert exit_code == 1
149
- assert "must be a list" in captured.err
150
-
151
- def test_rejects_a_malformed_record(
152
- self,
153
- monkeypatch: pytest.MonkeyPatch,
154
- capsys: pytest.CaptureFixture[str],
155
- ) -> None:
156
- monkeypatch.setattr(sys, "stdin", io.StringIO('[{"missing": "fields"}]'))
157
- exit_code = main()
158
- captured = capsys.readouterr()
159
- assert exit_code == 1
160
- assert "malformed record" in captured.err
@@ -1,111 +0,0 @@
1
- """Tests for collect_log_window — parsing and windowing hook-block records."""
2
-
3
- import json
4
- import sys
5
- from datetime import datetime, timedelta
6
- from pathlib import Path
7
-
8
- _script_directory = str(Path(__file__).resolve().parent)
9
- if _script_directory not in sys.path:
10
- sys.path.insert(0, _script_directory)
11
-
12
- from collect_log_window import ( # noqa: E402
13
- LogRecord,
14
- collect_records,
15
- parse_log_line,
16
- read_log_window,
17
- )
18
-
19
-
20
- def _block_line(timestamp_text: str, hook_name: str, reason_text: str) -> str:
21
- return json.dumps(
22
- {
23
- "timestamp": timestamp_text,
24
- "hook": hook_name,
25
- "event": "PreToolUse",
26
- "reason": reason_text,
27
- }
28
- )
29
-
30
-
31
- class TestParseLogLine:
32
- def test_parses_a_well_formed_block_record(self) -> None:
33
- line = _block_line(
34
- "2026-07-03T09:00:00", "plain_language_blocker", "Heavy word"
35
- )
36
- record = parse_log_line(line)
37
- assert record == LogRecord(
38
- timestamp=datetime(2026, 7, 3, 9, 0, 0),
39
- source="plain_language_blocker",
40
- level="block",
41
- message="Heavy word",
42
- )
43
-
44
- def test_returns_none_for_a_blank_line(self) -> None:
45
- assert parse_log_line(" ") is None
46
-
47
- def test_returns_none_for_non_json(self) -> None:
48
- assert parse_log_line("not json at all") is None
49
-
50
- def test_returns_none_when_reason_is_missing(self) -> None:
51
- line = json.dumps({"timestamp": "2026-07-03T09:00:00", "hook": "x"})
52
- assert parse_log_line(line) is None
53
-
54
- def test_returns_none_for_an_unparseable_timestamp(self) -> None:
55
- line = _block_line("not-a-timestamp", "x", "y")
56
- assert parse_log_line(line) is None
57
-
58
-
59
- class TestCollectRecords:
60
- def test_keeps_only_records_at_or_after_the_window_start(self) -> None:
61
- window_start = datetime(2026, 7, 3, 8, 0, 0)
62
- log_text = "\n".join(
63
- [
64
- _block_line("2026-07-03T07:59:59", "old_hook", "before window"),
65
- _block_line("2026-07-03T08:00:00", "edge_hook", "on the boundary"),
66
- _block_line("2026-07-03T09:30:00", "new_hook", "inside window"),
67
- ]
68
- )
69
- records = collect_records(log_text, window_start)
70
- assert [each_record.source for each_record in records] == [
71
- "edge_hook",
72
- "new_hook",
73
- ]
74
-
75
- def test_skips_malformed_lines(self) -> None:
76
- window_start = datetime(2026, 7, 3, 0, 0, 0)
77
- log_text = "\n".join(
78
- [
79
- "garbage",
80
- _block_line("2026-07-03T09:00:00", "good_hook", "kept"),
81
- "",
82
- ]
83
- )
84
- records = collect_records(log_text, window_start)
85
- assert len(records) == 1
86
- assert records[0].source == "good_hook"
87
-
88
-
89
- class TestReadLogWindow:
90
- def test_returns_empty_list_when_the_log_is_absent(self, tmp_path: Path) -> None:
91
- missing_path = tmp_path / "hook-blocks.log"
92
- assert read_log_window(missing_path, 24, datetime(2026, 7, 3, 12, 0, 0)) == []
93
-
94
- def test_reads_records_inside_the_window_from_a_real_file(
95
- self, tmp_path: Path
96
- ) -> None:
97
- now = datetime(2026, 7, 3, 12, 0, 0)
98
- recent_time = (now - timedelta(hours=1)).isoformat()
99
- stale_time = (now - timedelta(hours=48)).isoformat()
100
- log_path = tmp_path / "hook-blocks.log"
101
- log_path.write_text(
102
- "\n".join(
103
- [
104
- _block_line(stale_time, "stale_hook", "too old"),
105
- _block_line(recent_time, "recent_hook", "in window"),
106
- ]
107
- ),
108
- encoding="utf-8",
109
- )
110
- records = read_log_window(log_path, 24, now)
111
- assert [each_record.source for each_record in records] == ["recent_hook"]
@@ -1,126 +0,0 @@
1
- """Tests for mine_copilot_findings — defect classification and clustering."""
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- _script_directory = str(Path(__file__).resolve().parent)
7
- if _script_directory not in sys.path:
8
- sys.path.insert(0, _script_directory)
9
-
10
- from mine_copilot_findings import ( # noqa: E402
11
- ReviewerComment,
12
- _flatten_pages,
13
- _pull_number_from_url,
14
- _reviewer_comment_from_payload,
15
- classify_defect,
16
- cluster_defects,
17
- proposal_for_defect_class,
18
- )
19
- from log_audit_constants.mine_copilot_findings_constants import ( # noqa: E402
20
- PROPOSAL_BY_DEFECT_CLASS,
21
- )
22
-
23
-
24
- class TestClassifyDefect:
25
- def test_recognizes_a_missing_type_hint_comment(self) -> None:
26
- assert (
27
- classify_defect("This function is missing a type hint on the return")
28
- == "missing-type-hint"
29
- )
30
-
31
- def test_recognizes_a_broad_except_comment(self) -> None:
32
- assert classify_defect("There is a broad except swallowing errors") == (
33
- "broad-except"
34
- )
35
-
36
- def test_recognizes_a_magic_value_comment(self) -> None:
37
- assert classify_defect("42 is a magic number, extract it") == "magic-value"
38
-
39
- def test_returns_none_for_praise(self) -> None:
40
- assert classify_defect("Looks great, nicely done!") is None
41
-
42
-
43
- class TestClusterDefects:
44
- def test_groups_comments_by_class_and_ranks_by_count(self) -> None:
45
- comments = [
46
- ReviewerComment(1, "cursor[bot]", "missing type hint here"),
47
- ReviewerComment(2, "cursor[bot]", "another missing annotation"),
48
- ReviewerComment(3, "cursor[bot]", "this is a broad except"),
49
- ReviewerComment(4, "cursor[bot]", "nice work"),
50
- ]
51
- clusters = cluster_defects(comments)
52
- assert clusters[0].defect_class == "missing-type-hint"
53
- assert clusters[0].count == 2
54
- assert len(clusters) == 2
55
-
56
- def test_caps_the_number_of_example_bodies(self) -> None:
57
- comments = [
58
- ReviewerComment(
59
- each_index, "cursor[bot]", f"missing type hint number {each_index}"
60
- )
61
- for each_index in range(10)
62
- ]
63
- clusters = cluster_defects(comments)
64
- assert clusters[0].count == 10
65
- assert len(clusters[0].example_bodies) == 3
66
-
67
-
68
- class TestProposalForDefectClass:
69
- def test_maps_a_class_to_its_skill_edit_proposal(self) -> None:
70
- proposal = proposal_for_defect_class("broad-except")
71
- assert proposal.defect_class == "broad-except"
72
- assert proposal.proposal == PROPOSAL_BY_DEFECT_CLASS["broad-except"]
73
-
74
-
75
- class TestPullNumberFromUrl:
76
- def test_reads_the_trailing_pull_number(self) -> None:
77
- url = "https://api.github.com/repos/owner/name/pulls/867"
78
- assert _pull_number_from_url(url) == 867
79
-
80
- def test_returns_none_when_the_last_segment_is_not_a_number(self) -> None:
81
- assert _pull_number_from_url("https://api.github.com/repos/owner/name") is None
82
-
83
-
84
- class TestReviewerCommentFromPayload:
85
- def test_builds_a_comment_from_a_reviewer_bot_payload(self) -> None:
86
- payload = {
87
- "user": {"login": "cursor[bot]"},
88
- "body": "missing type hint",
89
- "pull_request_url": "https://api.github.com/repos/owner/name/pulls/12",
90
- }
91
- parsed_comment = _reviewer_comment_from_payload(payload)
92
- assert parsed_comment == ReviewerComment(
93
- pull_number=12, author="cursor[bot]", body="missing type hint"
94
- )
95
-
96
- def test_skips_a_comment_from_a_non_reviewer_login(self) -> None:
97
- payload = {
98
- "user": {"login": "some-human"},
99
- "body": "looks good",
100
- "pull_request_url": "https://api.github.com/repos/owner/name/pulls/12",
101
- }
102
- assert _reviewer_comment_from_payload(payload) is None
103
-
104
- def test_skips_a_payload_that_is_not_a_dict(self) -> None:
105
- assert _reviewer_comment_from_payload("not a payload") is None
106
-
107
- def test_skips_a_payload_missing_the_pull_request_url(self) -> None:
108
- payload = {"user": {"login": "cursor[bot]"}, "body": "missing type hint"}
109
- assert _reviewer_comment_from_payload(payload) is None
110
-
111
- def test_skips_a_payload_whose_url_has_no_numeric_pull_number(self) -> None:
112
- payload = {
113
- "user": {"login": "cursor[bot]"},
114
- "body": "missing type hint",
115
- "pull_request_url": "https://api.github.com/repos/owner/name/pulls",
116
- }
117
- assert _reviewer_comment_from_payload(payload) is None
118
-
119
-
120
- class TestFlattenPages:
121
- def test_flattens_multiple_pages_into_one_list(self) -> None:
122
- slurped_payload = [[{"id": 1}, {"id": 2}], [{"id": 3}]]
123
- assert _flatten_pages(slurped_payload) == [{"id": 1}, {"id": 2}, {"id": 3}]
124
-
125
- def test_returns_empty_list_for_a_non_list_payload(self) -> None:
126
- assert _flatten_pages({"message": "not a list"}) == []