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
@@ -1,199 +0,0 @@
1
- """Read the recent window of the hook block log into structured records.
2
-
3
- Tails the JSON-lines hook block log every blocking hook appends to, and hands
4
- back the block events inside a time window as plain records. Picture the audit
5
- agent waking up: it asks this script for the last few hours of blocks, and gets
6
- one record per block carrying when it happened, which hook fired, and why. Those
7
- records feed cluster_recurrences, which groups them by a normalized signature.
8
-
9
- Usage:
10
- collect_log_window.py --hours 24
11
- collect_log_window.py --hours 6 --log-path /path/to/hook-blocks.log
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import argparse
17
- import json
18
- import sys
19
- from collections.abc import Iterable
20
- from dataclasses import dataclass
21
- from datetime import datetime, timedelta
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.collect_log_window_constants import ( # noqa: E402
29
- BLOCK_LEVEL_LABEL,
30
- DEFAULT_WINDOW_HOURS,
31
- HOOK_BLOCKS_LOG_RELATIVE_PATH,
32
- LOG_HOOK_KEY,
33
- LOG_REASON_KEY,
34
- LOG_TIMESTAMP_KEY,
35
- RECORD_LEVEL_KEY,
36
- RECORD_MESSAGE_KEY,
37
- RECORD_SOURCE_KEY,
38
- RECORD_TIMESTAMP_KEY,
39
- )
40
-
41
-
42
- @dataclass(frozen=True)
43
- class LogRecord:
44
- """One block event read from the hook block log.
45
-
46
- Attributes:
47
- timestamp: When the block happened, parsed from the record's ISO string.
48
- source: The hook that blocked, taken from the record's hook field.
49
- level: The severity label; every hook-block record reads as a block.
50
- message: The human-readable block reason.
51
- """
52
-
53
- timestamp: datetime
54
- source: str
55
- level: str
56
- message: str
57
-
58
-
59
- def parse_log_line(raw_line: str) -> LogRecord | None:
60
- """Parse one hook-block-log line into a LogRecord.
61
-
62
- Args:
63
- raw_line: A single line of the JSON-lines hook block log.
64
-
65
- Returns:
66
- The parsed record, or None when the line is blank, is not a JSON object,
67
- or omits the timestamp, hook, or reason field, or carries a timestamp
68
- that does not parse as an ISO datetime.
69
- """
70
- stripped_line = raw_line.strip()
71
- if not stripped_line:
72
- return None
73
- try:
74
- record_fields = json.loads(stripped_line)
75
- except json.JSONDecodeError:
76
- return None
77
- if not isinstance(record_fields, dict):
78
- return None
79
- timestamp_text = record_fields.get(LOG_TIMESTAMP_KEY)
80
- source_text = record_fields.get(LOG_HOOK_KEY)
81
- message_text = record_fields.get(LOG_REASON_KEY)
82
- if not isinstance(timestamp_text, str):
83
- return None
84
- if not isinstance(source_text, str) or not isinstance(message_text, str):
85
- return None
86
- try:
87
- parsed_timestamp = datetime.fromisoformat(timestamp_text)
88
- except ValueError:
89
- return None
90
- return LogRecord(
91
- timestamp=parsed_timestamp,
92
- source=source_text,
93
- level=BLOCK_LEVEL_LABEL,
94
- message=message_text,
95
- )
96
-
97
-
98
- def _records_within_window(
99
- all_lines: Iterable[str], window_start: datetime
100
- ) -> list[LogRecord]:
101
- """Parse each line and keep the records at or after the window start.
102
-
103
- Args:
104
- all_lines: The hook-block-log lines to parse, in file order.
105
- window_start: The earliest timestamp a kept record may carry.
106
-
107
- Returns:
108
- The in-window records in the order their lines appear.
109
- """
110
- kept_records: list[LogRecord] = []
111
- for each_line in all_lines:
112
- parsed_record = parse_log_line(each_line)
113
- if parsed_record is None:
114
- continue
115
- if parsed_record.timestamp >= window_start:
116
- kept_records.append(parsed_record)
117
- return kept_records
118
-
119
-
120
- def collect_records(log_text: str, window_start: datetime) -> list[LogRecord]:
121
- """Parse log text and keep records at or after the window start.
122
-
123
- Args:
124
- log_text: The full text of the hook block log.
125
- window_start: The earliest timestamp a kept record may carry.
126
-
127
- Returns:
128
- The in-window records in the order they appear in the log.
129
- """
130
- return _records_within_window(log_text.splitlines(), window_start)
131
-
132
-
133
- def read_log_window(
134
- log_path: Path, window_hours: int, now: datetime
135
- ) -> list[LogRecord]:
136
- """Read the hook block log and return records within the window.
137
-
138
- Args:
139
- log_path: Path to the JSON-lines hook block log.
140
- window_hours: How many hours back from now to keep.
141
- now: The current time the window is measured back from.
142
-
143
- Returns:
144
- The in-window records, or an empty list when the log file is absent.
145
- """
146
- window_start = now - timedelta(hours=window_hours)
147
- try:
148
- with log_path.open(encoding="utf-8") as log_file:
149
- return _records_within_window(log_file, window_start)
150
- except FileNotFoundError:
151
- return []
152
-
153
-
154
- def _default_log_path() -> Path:
155
- """Return the home-relative path of the hook block log."""
156
- return Path.home() / HOOK_BLOCKS_LOG_RELATIVE_PATH
157
-
158
-
159
- def _record_as_json_dict(record: LogRecord) -> dict[str, str]:
160
- """Render a record as a JSON-ready dict with an ISO timestamp string.
161
-
162
- Args:
163
- record: The record to render.
164
-
165
- Returns:
166
- A dict of string values keyed by the record's field names.
167
- """
168
- return {
169
- RECORD_TIMESTAMP_KEY: record.timestamp.isoformat(),
170
- RECORD_SOURCE_KEY: record.source,
171
- RECORD_LEVEL_KEY: record.level,
172
- RECORD_MESSAGE_KEY: record.message,
173
- }
174
-
175
-
176
- def main() -> int:
177
- """Print the recent hook-block records as JSON to stdout.
178
-
179
- Returns:
180
- The process exit code; zero on success.
181
- """
182
- parser = argparse.ArgumentParser(description="Tail the hook block log window.")
183
- parser.add_argument("--hours", type=int, default=DEFAULT_WINDOW_HOURS)
184
- parser.add_argument("--log-path", type=str, default=None)
185
- parsed_arguments = parser.parse_args()
186
- log_path = (
187
- Path(parsed_arguments.log_path).expanduser().resolve()
188
- if parsed_arguments.log_path is not None
189
- else _default_log_path()
190
- )
191
- records = read_log_window(log_path, parsed_arguments.hours, datetime.now())
192
- json.dump(
193
- [_record_as_json_dict(each_record) for each_record in records], sys.stdout
194
- )
195
- return 0
196
-
197
-
198
- if __name__ == "__main__":
199
- raise SystemExit(main())
@@ -1,12 +0,0 @@
1
- # log-audit/scripts/log_audit_constants
2
-
3
- Named constants imported by the `log-audit` scripts. Each module holds the tunables for one script, keeping magic values out of the scripts.
4
-
5
- ## Files
6
-
7
- | File | What it holds |
8
- |---|---|
9
- | `__init__.py` | Package marker. |
10
- | `collect_log_window_constants.py` | The hook-block-log path, default window, block level label, and the input and output record key names for `collect_log_window.py`. |
11
- | `cluster_recurrences_constants.py` | The signature-stripping regexes, recency decay, and timing-regression thresholds for `cluster_recurrences.py`. |
12
- | `mine_copilot_findings_constants.py` | The reviewer-bot logins, the recent-pulls and per-pull comments endpoint templates, the recent-pull count, and the defect-class keyword and proposal maps for `mine_copilot_findings.py`. |
@@ -1,23 +0,0 @@
1
- """Constants for the cluster_recurrences script.
2
-
3
- PATH_PATTERN, HASH_PATTERN, DIGIT_PATTERN: regexes stripped from a message, in this
4
- order, so records that differ only in a path, hash, or number share one signature.
5
- SIGNATURE_PLACEHOLDER: the token each stripped span collapses to.
6
- SECONDS_PER_HOUR: seconds in one hour, dividing a record's age into hours.
7
- RECENCY_DECAY_BASE: the weight a record carries once it is one half-life old.
8
- RECENCY_HALF_LIFE_HOURS: age in hours at which a record's weight reaches the decay base.
9
- TIMING_REGRESSION_RATIO: recent-over-baseline duration ratio that flags a regression.
10
- MIN_TIMING_SAMPLES_PER_HALF: samples required in the baseline half and the recent half.
11
- SAMPLE_HALVES: the number of halves a timing series splits into before it can be judged.
12
- """
13
-
14
- PATH_PATTERN = r"(?:[A-Za-z]:)?(?:[\\/][\w.\-]+)+"
15
- HASH_PATTERN = r"\b[0-9a-f]{7,40}\b"
16
- DIGIT_PATTERN = r"\d+"
17
- SIGNATURE_PLACEHOLDER = "*"
18
- SECONDS_PER_HOUR = 3600
19
- RECENCY_DECAY_BASE = 0.5
20
- RECENCY_HALF_LIFE_HOURS = 24.0
21
- TIMING_REGRESSION_RATIO = 1.5
22
- MIN_TIMING_SAMPLES_PER_HALF = 3
23
- SAMPLE_HALVES = 2
@@ -1,24 +0,0 @@
1
- """Constants for the collect_log_window script.
2
-
3
- HOOK_BLOCKS_LOG_RELATIVE_PATH: home-relative path of the JSON-lines hook block log.
4
- DEFAULT_WINDOW_HOURS: how many hours back a collection reads when no window is given.
5
- BLOCK_LEVEL_LABEL: the level stamped on every record read from the hook block log.
6
- LOG_TIMESTAMP_KEY: hook-block-log JSON key holding the ISO-8601 block time.
7
- LOG_HOOK_KEY: hook-block-log JSON key holding the blocking hook's name.
8
- LOG_REASON_KEY: hook-block-log JSON key holding the human-readable block reason.
9
- RECORD_TIMESTAMP_KEY: output-record key holding the ISO-8601 block time.
10
- RECORD_SOURCE_KEY: output-record key holding the blocking hook's name.
11
- RECORD_LEVEL_KEY: output-record key holding the severity label.
12
- RECORD_MESSAGE_KEY: output-record key holding the block reason.
13
- """
14
-
15
- HOOK_BLOCKS_LOG_RELATIVE_PATH = ".claude/logs/hook-blocks.log"
16
- DEFAULT_WINDOW_HOURS = 24
17
- BLOCK_LEVEL_LABEL = "block"
18
- LOG_TIMESTAMP_KEY = "timestamp"
19
- LOG_HOOK_KEY = "hook"
20
- LOG_REASON_KEY = "reason"
21
- RECORD_TIMESTAMP_KEY = "timestamp"
22
- RECORD_SOURCE_KEY = "source"
23
- RECORD_LEVEL_KEY = "level"
24
- RECORD_MESSAGE_KEY = "message"
@@ -1,49 +0,0 @@
1
- """Constants for the mine_copilot_findings script.
2
-
3
- ALL_REVIEWER_BOT_LOGINS: the GitHub logins whose review comments count as defects.
4
- RECENT_PULLS_ENDPOINT_TEMPLATE: the gh api path for a repo's most recent pulls.
5
- PULL_COMMENTS_ENDPOINT_TEMPLATE: the gh api path for one pull request's comments.
6
- RECENT_PULL_COUNT: how many recent pull requests a mining run reads.
7
- MAX_EXAMPLES_PER_CLUSTER: example comment bodies kept per defect class.
8
- KEYWORDS_BY_DEFECT_CLASS: lowercased substrings that sort a comment into a defect class.
9
- PROPOSAL_BY_DEFECT_CLASS: the concrete skill-definition edit proposed for each class.
10
- """
11
-
12
- ALL_REVIEWER_BOT_LOGINS = ("copilot-pull-request-reviewer[bot]", "cursor[bot]")
13
- RECENT_PULLS_ENDPOINT_TEMPLATE = (
14
- "repos/{repo}/pulls?state=all&sort=updated&direction=desc&per_page={pull_count}"
15
- )
16
- PULL_COMMENTS_ENDPOINT_TEMPLATE = (
17
- "repos/{repo}/pulls/{pull_number}/comments?per_page=100"
18
- )
19
- RECENT_PULL_COUNT = 20
20
- MAX_EXAMPLES_PER_CLUSTER = 3
21
- KEYWORDS_BY_DEFECT_CLASS = {
22
- "missing-type-hint": ("type hint", "missing annotation", "untyped", "no return type"),
23
- "broad-except": ("broad except", "bare except", "except exception", "catch-all except"),
24
- "magic-value": ("magic number", "magic value", "hardcoded", "hard-coded"),
25
- "unclear-name": ("abbreviation", "unclear name", "rename this", "single-letter"),
26
- "missing-docstring": ("missing docstring", "no docstring", "undocumented"),
27
- }
28
- PROPOSAL_BY_DEFECT_CLASS = {
29
- "missing-type-hint": (
30
- "Tighten the code skill's type-hint rule to flag the untyped shape this "
31
- "class keeps hitting."
32
- ),
33
- "broad-except": (
34
- "Extend the no-broad-except enforcer pattern to the except shape this "
35
- "class keeps hitting."
36
- ),
37
- "magic-value": (
38
- "Extend the magic-value enforcer to the literal shape this class keeps "
39
- "hitting."
40
- ),
41
- "unclear-name": (
42
- "Add the abbreviation this class keeps flagging to the banned-identifier "
43
- "list."
44
- ),
45
- "missing-docstring": (
46
- "Tighten the public-docstring rule to the surface this class keeps "
47
- "hitting."
48
- ),
49
- }
@@ -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())