claude-dev-env 1.82.0 → 1.84.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 (217) hide show
  1. package/CLAUDE.md +16 -13
  2. package/_shared/pr-loop/audit-contract.md +24 -12
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/README.md +1 -0
  5. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  8. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviewer_availability_constants.py +12 -0
  9. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +0 -2
  10. package/_shared/pr-loop/scripts/reviewer_availability.py +182 -0
  11. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  12. package/_shared/pr-loop/scripts/terminology_sweep.py +9 -33
  13. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  14. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  15. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  18. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +159 -0
  19. package/_shared/pr-loop/scripts/tests/test_reviewer_availability_constants.py +36 -0
  20. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +14 -4
  22. package/agents/clean-coder.md +3 -0
  23. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  24. package/bin/install.mjs +16 -8
  25. package/bin/install.test.mjs +17 -6
  26. package/commands/CLAUDE.md +0 -1
  27. package/docs/CODE_RULES.md +1 -1
  28. package/hooks/blocking/CLAUDE.md +2 -4
  29. package/hooks/blocking/code_rules_constants_config.py +164 -1
  30. package/hooks/blocking/code_rules_docstrings.py +428 -15
  31. package/hooks/blocking/code_rules_enforcer.py +33 -0
  32. package/hooks/blocking/code_rules_imports_logging.py +867 -1
  33. package/hooks/blocking/code_rules_magic_values.py +5 -0
  34. package/hooks/blocking/code_rules_naming_collection.py +152 -6
  35. package/hooks/blocking/code_rules_shared.py +34 -0
  36. package/hooks/blocking/code_rules_string_magic.py +68 -0
  37. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  38. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  39. package/hooks/blocking/reviewer_spawn_gate.py +182 -0
  40. package/hooks/blocking/stale_comment_reference_blocker.py +267 -0
  41. package/hooks/blocking/state_description_blocker.py +96 -5
  42. package/hooks/blocking/test_code_rules_config_duplicate_path_anchor.py +132 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +2 -0
  44. package/hooks/blocking/test_code_rules_enforcer_docstring_delegation_summary.py +385 -0
  45. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  46. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  47. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +67 -0
  48. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +40 -0
  49. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +213 -0
  50. package/hooks/blocking/test_code_rules_enforcer_referenced_underscore_loop.py +169 -0
  51. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  52. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  53. package/hooks/blocking/test_code_rules_js_bare_flag_return_directive.py +266 -0
  54. package/hooks/blocking/test_code_rules_js_sibling_return_object_key_drift.py +490 -0
  55. package/hooks/blocking/test_code_rules_logging_adjacent_literals.py +171 -0
  56. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  57. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  58. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  59. package/hooks/blocking/test_pre_tool_use_dispatcher.py +9 -3
  60. package/hooks/blocking/test_reviewer_spawn_gate.py +230 -0
  61. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  62. package/hooks/blocking/test_stale_comment_reference_blocker.py +236 -0
  63. package/hooks/blocking/test_state_description_blocker.py +135 -0
  64. package/hooks/hooks.json +5 -0
  65. package/hooks/hooks_constants/CLAUDE.md +3 -4
  66. package/hooks/hooks_constants/blocking_check_limits.py +46 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -0
  68. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  69. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  70. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  71. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  72. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +41 -0
  73. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +76 -0
  74. package/hooks/hooks_constants/state_description_blocker_constants.py +8 -0
  75. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  76. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  77. package/hooks/workflow/CLAUDE.md +2 -8
  78. package/package.json +1 -1
  79. package/rules/CLAUDE.md +5 -2
  80. package/rules/claude-md-orphan-file.md +5 -0
  81. package/rules/docstring-prose-matches-implementation.md +10 -1
  82. package/rules/env-var-table-code-drift.md +5 -0
  83. package/rules/es-exe-file-search.md +17 -0
  84. package/rules/no-historical-clutter.md +12 -1
  85. package/rules/orphan-css-class.md +5 -0
  86. package/rules/package-inventory-stale-entry.md +10 -0
  87. package/rules/paired-test-coverage.md +5 -0
  88. package/rules/plain-illustrative-docstrings.md +40 -1
  89. package/rules/verify-before-asking.md +7 -0
  90. package/rules/verify-runtime-state.md +40 -0
  91. package/rules/windows-filesystem-safe.md +8 -0
  92. package/rules/workers-done-before-complete.md +33 -0
  93. package/rules/workflow-substitution-slots.md +5 -0
  94. package/skills/CLAUDE.md +7 -2
  95. package/skills/autoconverge/SKILL.md +48 -61
  96. package/skills/autoconverge/reference/closing-report.md +6 -6
  97. package/skills/autoconverge/reference/convergence.md +17 -15
  98. package/skills/autoconverge/reference/gotchas.md +6 -3
  99. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  100. package/skills/autoconverge/workflow/converge.contract.test.mjs +153 -10
  101. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +55 -19
  102. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  103. package/skills/autoconverge/workflow/converge.mjs +167 -106
  104. package/skills/autoconverge/workflow/render_report.py +7 -11
  105. package/skills/bugteam/CLAUDE.md +1 -1
  106. package/skills/bugteam/PROMPTS.md +7 -6
  107. package/skills/bugteam/SKILL.md +25 -72
  108. package/skills/bugteam/reference/CLAUDE.md +1 -3
  109. package/skills/bugteam/reference/README.md +1 -1
  110. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  111. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  112. package/skills/bugteam/reference/team-setup.md +8 -7
  113. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  114. package/skills/bugteam/scripts/README.md +0 -4
  115. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  116. package/skills/code/SKILL.md +2 -0
  117. package/skills/copilot-review/CLAUDE.md +1 -1
  118. package/skills/copilot-review/SKILL.md +25 -23
  119. package/skills/everything-search/SKILL.md +5 -0
  120. package/skills/findbugs/CLAUDE.md +2 -2
  121. package/skills/findbugs/SKILL.md +22 -83
  122. package/skills/fixbugs/SKILL.md +2 -4
  123. package/skills/log-audit/CLAUDE.md +20 -0
  124. package/skills/log-audit/SKILL.md +68 -0
  125. package/skills/log-audit/reference/CLAUDE.md +9 -0
  126. package/skills/log-audit/reference/charter.md +52 -0
  127. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  128. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  129. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  130. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  131. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  132. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  133. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  134. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  135. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  136. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  137. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  138. package/skills/monitor-open-prs/SKILL.md +2 -2
  139. package/skills/post-audit-findings/SKILL.md +84 -0
  140. package/skills/pr-converge/CLAUDE.md +2 -0
  141. package/skills/pr-converge/SKILL.md +72 -59
  142. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  143. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  144. package/skills/pr-converge/reference/examples.md +5 -5
  145. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  146. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  147. package/skills/pr-converge/reference/per-tick.md +24 -45
  148. package/skills/pr-converge/reference/state-schema.md +15 -0
  149. package/skills/pr-converge/scripts/README.md +3 -5
  150. package/skills/pr-fix-protocol/SKILL.md +70 -0
  151. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  152. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  153. package/skills/pr-scope-resolve/SKILL.md +48 -0
  154. package/skills/qbug/CLAUDE.md +4 -4
  155. package/skills/qbug/SKILL.md +46 -144
  156. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  157. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  158. package/skills/reviewer-gates/SKILL.md +96 -0
  159. package/skills/session-log/CLAUDE.md +7 -7
  160. package/skills/session-log/SKILL.md +27 -44
  161. package/skills/test_markdown_link_integrity.py +103 -0
  162. package/commands/doc-gist.md +0 -16
  163. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  164. package/hooks/blocking/conftest.py +0 -30
  165. package/hooks/blocking/md_path_exemptions.py +0 -224
  166. package/hooks/blocking/md_to_html_blocker.py +0 -155
  167. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  168. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  169. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  170. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  171. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  172. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  173. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  174. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  175. package/hooks/workflow/md_to_html_companion.py +0 -358
  176. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  177. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  178. package/skills/bugteam/reference/audit-contract.md +0 -163
  179. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  181. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  182. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  183. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  184. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  185. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  186. package/skills/doc-gist/CLAUDE.md +0 -25
  187. package/skills/doc-gist/SKILL.md +0 -97
  188. package/skills/doc-gist/references/CLAUDE.md +0 -9
  189. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  190. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  191. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  192. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  193. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  194. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  195. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  196. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  197. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  198. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  199. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  200. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  201. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  202. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  203. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  204. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  205. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  206. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  207. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  208. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  209. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  210. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  211. package/skills/doc-gist/references/examples/README.md +0 -5
  212. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  213. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  214. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  215. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  216. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  217. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: log-audit
3
+ description: >-
4
+ Watches this repo's own logs for patterns worth acting on. Use for /log-audit,
5
+ "audit the logs", "what keeps failing", or "what's getting slower". Reads the
6
+ hook block log and the diagnostic extractor pipeline, clusters recurring errors
7
+ and timing regressions, opens a grouped fix PR or a tracked optimization issue
8
+ per real finding, and mines the defects Copilot and Bugbot keep catching into
9
+ skill-edit proposals. Runs as a background agent that resumes after a restart.
10
+ ---
11
+
12
+ # log-audit
13
+
14
+ Read the logs this repo writes about itself, find what keeps going wrong or keeps getting slower, and turn each real pattern into a tracked fix. `reference/charter.md` holds the full contract; this file is how to run a cycle.
15
+
16
+ ## One cycle
17
+
18
+ 1. **Collect.** Run `scripts/collect_log_window.py` to read the recent window of the hook block log into records.
19
+ 2. **Cluster.** Pipe those records into `scripts/cluster_recurrences.py` to group them by a normalized signature and rank the loudest first. When timing samples are on hand, the same module flags operations whose recent runs have grown slower than their earlier runs.
20
+ 3. **Mine reviewers.** Run `scripts/mine_copilot_findings.py` to sort recent reviewer-bot comments into defect classes and print a skill-edit proposal for each class.
21
+ 4. **File findings.** For each real finding, open a grouped fix pull request as a draft, or file a tracked optimization issue for a delay.
22
+ 5. **Report.** Write the per-cycle report: delays removed, pull requests opened, skill improvements suggested.
23
+ 6. **Re-arm.** Save cycle state and schedule the next cycle.
24
+
25
+ ## Scripts
26
+
27
+ | File | What it does |
28
+ |---|---|
29
+ | `scripts/collect_log_window.py` | Tails the JSON-lines hook block log and prints the block records inside a time window as JSON. `--hours` sets the window; `--log-path` overrides the log location. |
30
+ | `scripts/cluster_recurrences.py` | Reads that JSON on stdin, groups records by a normalized message signature, and prints the clusters ranked by recency-weighted count. Its `detect_timing_regressions` flags an operation whose recent runs are slower than its earliest runs. |
31
+ | `scripts/mine_copilot_findings.py` | Reads a repo's reviewer-bot comments through `gh`, sorts them into defect classes, and prints one skill-edit proposal per class, most frequent first. Takes `--repo owner/name`. |
32
+
33
+ Run the first two as a pipeline:
34
+
35
+ ```
36
+ python scripts/collect_log_window.py --hours 24 | python scripts/cluster_recurrences.py
37
+ ```
38
+
39
+ ## What it reads
40
+
41
+ In scope:
42
+
43
+ - The hook block log at `~/.claude/logs/hook-blocks.log`, written by `hook_block_logger`.
44
+ - The `hooks/diagnostic/` extractor pipeline. When Neon is set up, read through that pipeline's SQL — the shape of `queries/blocks_by_category.sql` and its siblings against the `hook_events` table, which carries `duration_ms` for timing work. When Neon is not set up, read the flat `hook-blocks.log` directly. Reuse the diagnostic pipeline's helpers; do not hold a second copy.
45
+
46
+ Out of scope: Samsung-automation logs. Read this repo's log surfaces only.
47
+
48
+ ## Filing findings
49
+
50
+ - A pull request or optimization issue carries its body in a file passed by path, following the `gh-body-file` rule.
51
+ - Any paginated GitHub read follows the `gh-paginate` rule.
52
+ - Group the related fixes for one finding into a single draft pull request rather than one PR per line.
53
+
54
+ ## Reviewer-defect mining
55
+
56
+ `mine_copilot_findings.py` names a skill or rule edit for each defect class the reviewers keep catching — an edit that would block that class at write time rather than at review. These are proposals. A human applies them through review; the agent does not commit them on its own.
57
+
58
+ ## Cycle state and restart survival
59
+
60
+ The agent keeps its state in a JSON file under `~/.claude/runtime/log-audit/` — the last window it read, the signatures it already reported, and the open items it filed. At the start of each cycle, read that file; at the end, write it back. Because the state lives in the durable runtime directory rather than the OS temp directory, a restart reads the same file and resumes the same run.
61
+
62
+ ## Cadence
63
+
64
+ The agent runs on a recurring schedule following the repository's Scheduled Task Cadence: a sub-hour interval, with a 30-minute default. Re-arm the next cycle with `ScheduleWakeup` for a self-paced loop, or register a cron routine through `/schedule` for a fixed clock cadence.
65
+
66
+ ## Per-cycle report
67
+
68
+ Each cycle ends with a short report: the delays removed, the pull requests opened, and the skill improvements suggested.
@@ -0,0 +1,9 @@
1
+ # log-audit/reference
2
+
3
+ Reference material for the `log-audit` skill.
4
+
5
+ ## Key files
6
+
7
+ | File | Purpose |
8
+ |---|---|
9
+ | `charter.md` | The agent's contract: what it watches (this repo's hook-block log and diagnostic extractor pipeline), what it looks for (recurring errors and timing regressions), what it files per finding (grouped draft fix PR or tracked optimization issue), how it mines reviewer defect patterns into skill-definition proposals, and the per-cycle report it emits. Cycle state lives under `~/.claude/runtime/log-audit/`; the agent runs on a sub-hour cadence. |
@@ -0,0 +1,52 @@
1
+ # Log-Audit Agent Charter
2
+
3
+ The contract for the log-audit agent: what it watches, what it looks for, and what it produces each cycle. The skill body (`SKILL.md`) and its scripts carry out this contract; this document is the fixed reference they answer to.
4
+
5
+ ## What the agent is
6
+
7
+ A background agent that reads this repository's own log surfaces, finds patterns worth acting on, and turns each real pattern into a tracked fix. It runs on its own without a person driving it and picks up where it left off after a restart.
8
+
9
+ ## Cycle state and cadence
10
+
11
+ The agent keeps its own state on disk under `~/.claude/runtime/log-audit/`. This directory holds durable cycle state — the last window it read, the signatures it already reported, and the open items it filed — so a restart resumes the same run rather than starting over. The OS temp directory is not a home for this state, because temp files can be cleared between sessions.
12
+
13
+ The agent runs on a recurring schedule. It follows the repository's Scheduled Task Cadence: a sub-hour interval, with a 30-minute default.
14
+
15
+ ## Log sources it reads
16
+
17
+ In scope:
18
+
19
+ - The hook block log at `~/.claude/logs/hook-blocks.log`, written by `hook_block_logger`. Each line is one JSON record naming the hook that blocked, the event, the reason, the tool, and a short input excerpt.
20
+ - The `hooks/diagnostic/` extractor pipeline. When Neon is configured, the agent reads through the pipeline's SQL — the shape of `queries/blocks_by_category.sql` and its siblings against the `hook_events` table, which carries `duration_ms` for timing work. When Neon is not configured, the agent reads the flat `hook-blocks.log` directly. It reuses the diagnostic pipeline's helpers rather than holding a second copy of them.
21
+
22
+ Out of scope:
23
+
24
+ - Samsung-automation logs. The agent reads this repository's log surfaces only.
25
+
26
+ ## What it looks for
27
+
28
+ Two kinds of pattern:
29
+
30
+ - Recurring errors. The same failure showing up again and again. The agent groups log records by a normalized message signature — the message with its digits, paths, and hashes stripped — so records that differ only in those details fall into one cluster. It ranks clusters by count weighted toward recent activity.
31
+ - Timing regressions. The same operation taking longer over time. When a repeated operation's duration climbs across cycles, the agent flags it as an unnecessary delay, separate from any error.
32
+
33
+ ## What it does per finding
34
+
35
+ For each real finding, the agent takes one of two tracked actions:
36
+
37
+ - Opens a grouped fix pull request as a draft, gathering the related fixes into one branch.
38
+ - Files a tracked optimization issue when the finding is a delay to chase rather than a fix to write.
39
+
40
+ Both paths carry their body in a file passed by path, following the `gh-body-file` rule, and any paginated GitHub read follows the `gh-paginate` rule.
41
+
42
+ ## Reviewer-defect mining
43
+
44
+ The agent also reads the defect patterns that Copilot and Bugbot catch again and again across recent pull requests. It clusters those comments into defect classes and proposes concrete edits to the skill definitions that would block each class upstream, at the point of writing, rather than at review. These are proposals: they land through review, not by the agent applying them on its own.
45
+
46
+ ## Per-cycle report
47
+
48
+ Each cycle ends with a short report covering:
49
+
50
+ - Delays removed.
51
+ - Pull requests opened.
52
+ - Skill improvements suggested.
@@ -0,0 +1,27 @@
1
+ # log-audit/scripts
2
+
3
+ The collect, cluster, and mine scripts for the `log-audit` skill, plus their constants package.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |---|---|
9
+ | `collect_log_window.py` | Tails the JSON-lines hook block log and prints the block records inside a time window as JSON. |
10
+ | `test_collect_log_window.py` | Tests for `collect_log_window.py`. |
11
+ | `cluster_recurrences.py` | Groups block records by a normalized signature, ranks them by recency-weighted count, and flags timing regressions. |
12
+ | `test_cluster_recurrences.py` | Tests for `cluster_recurrences.py`. |
13
+ | `mine_copilot_findings.py` | Sorts reviewer-bot comments into defect classes and prints one skill-edit proposal per class. |
14
+ | `test_mine_copilot_findings.py` | Tests for `mine_copilot_findings.py`. |
15
+
16
+ ## Subdirectories
17
+
18
+ | Directory | Role |
19
+ |---|---|
20
+ | `log_audit_constants/` | Named constants imported by the three scripts. |
21
+
22
+ ## Running
23
+
24
+ ```
25
+ python collect_log_window.py --hours 24 | python cluster_recurrences.py
26
+ python mine_copilot_findings.py --repo owner/name
27
+ ```
@@ -0,0 +1,261 @@
1
+ """Group hook-block records by a normalized signature and rank the clusters.
2
+
3
+ Two failures can read differently only because one names line 12 and the other
4
+ line 348. They are the same failure. This script strips the varying parts down
5
+ to one signature, so it counts how often each real failure recurs. It ranks the
6
+ loudest first and weights recent blocks over stale ones. It also reads timing
7
+ samples for one operation and flags that operation when its recent runs have
8
+ grown slower than its earlier runs.
9
+
10
+ Reads the JSON record list collect_log_window prints, on stdin.
11
+
12
+ Usage:
13
+ collect_log_window.py --hours 24 | cluster_recurrences.py
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import re
20
+ import sys
21
+ from collections import defaultdict
22
+ from dataclasses import dataclass
23
+ from datetime import datetime
24
+ from pathlib import Path
25
+
26
+ _script_directory = str(Path(__file__).resolve().parent)
27
+ if _script_directory not in sys.path:
28
+ sys.path.insert(0, _script_directory)
29
+
30
+ from log_audit_constants.cluster_recurrences_constants import ( # noqa: E402
31
+ DIGIT_PATTERN,
32
+ HASH_PATTERN,
33
+ MIN_TIMING_SAMPLES_PER_HALF,
34
+ PATH_PATTERN,
35
+ RECENCY_DECAY_BASE,
36
+ RECENCY_HALF_LIFE_HOURS,
37
+ SAMPLE_HALVES,
38
+ SECONDS_PER_HOUR,
39
+ SIGNATURE_PLACEHOLDER,
40
+ TIMING_REGRESSION_RATIO,
41
+ )
42
+ from log_audit_constants.collect_log_window_constants import ( # noqa: E402
43
+ RECORD_LEVEL_KEY,
44
+ RECORD_MESSAGE_KEY,
45
+ RECORD_SOURCE_KEY,
46
+ RECORD_TIMESTAMP_KEY,
47
+ )
48
+
49
+ from collect_log_window import LogRecord # noqa: E402
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class SignatureCluster:
54
+ """A group of records that share one normalized message signature.
55
+
56
+ Attributes:
57
+ signature: The normalized message the grouped records share.
58
+ count: How many records fell into the cluster.
59
+ latest_timestamp: The most recent record's timestamp in the cluster.
60
+ score: The recency-weighted sum of the cluster's records.
61
+ """
62
+
63
+ signature: str
64
+ count: int
65
+ latest_timestamp: datetime
66
+ score: float
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class TimingSample:
71
+ """One measured run of a repeated operation.
72
+
73
+ Attributes:
74
+ operation: The operation's name.
75
+ timestamp: When the run happened.
76
+ duration_ms: How long the run took, in milliseconds.
77
+ """
78
+
79
+ operation: str
80
+ timestamp: datetime
81
+ duration_ms: float
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class TimingRegression:
86
+ """A repeated operation whose recent runs have grown slower.
87
+
88
+ Attributes:
89
+ operation: The operation's name.
90
+ baseline_ms: The mean duration of the earliest runs.
91
+ recent_ms: The mean duration of the latest runs.
92
+ ratio: The recent mean divided by the baseline mean.
93
+ """
94
+
95
+ operation: str
96
+ baseline_ms: float
97
+ recent_ms: float
98
+ ratio: float
99
+
100
+
101
+ def normalize_signature(message: str) -> str:
102
+ """Reduce a message to a signature shared by its recurring variants.
103
+
104
+ Args:
105
+ message: A raw block-reason message.
106
+
107
+ Returns:
108
+ The message with paths, hashes, and numbers replaced by a placeholder
109
+ and runs of whitespace collapsed to single spaces.
110
+ """
111
+ without_paths = re.sub(PATH_PATTERN, SIGNATURE_PLACEHOLDER, message)
112
+ without_hashes = re.sub(HASH_PATTERN, SIGNATURE_PLACEHOLDER, without_paths)
113
+ without_numbers = re.sub(DIGIT_PATTERN, SIGNATURE_PLACEHOLDER, without_hashes)
114
+ return re.sub(r"\s+", " ", without_numbers).strip()
115
+
116
+
117
+ def recency_weight(record_timestamp: datetime, now: datetime) -> float:
118
+ """Weight a record by how recent it is, halving each half-life of age.
119
+
120
+ Args:
121
+ record_timestamp: When the record was written.
122
+ now: The current time age is measured against.
123
+
124
+ Returns:
125
+ A weight near one for a fresh record, falling toward zero as it ages.
126
+ """
127
+ age_hours = (now - record_timestamp).total_seconds() / SECONDS_PER_HOUR
128
+ non_negative_age_hours = max(age_hours, 0.0)
129
+ return RECENCY_DECAY_BASE ** (non_negative_age_hours / RECENCY_HALF_LIFE_HOURS)
130
+
131
+
132
+ def rank_signature_clusters(
133
+ all_records: list[LogRecord], now: datetime
134
+ ) -> list[SignatureCluster]:
135
+ """Group records by signature and rank them by recency-weighted count.
136
+
137
+ Args:
138
+ all_records: The block records to group.
139
+ now: The current time used to weight each record.
140
+
141
+ Returns:
142
+ One cluster per distinct signature, highest score first.
143
+ """
144
+ records_by_signature: dict[str, list[LogRecord]] = defaultdict(list)
145
+ for each_record in all_records:
146
+ records_by_signature[normalize_signature(each_record.message)].append(
147
+ each_record
148
+ )
149
+ clusters: list[SignatureCluster] = []
150
+ for each_signature, each_signature_records in records_by_signature.items():
151
+ score = sum(
152
+ recency_weight(each_grouped.timestamp, now)
153
+ for each_grouped in each_signature_records
154
+ )
155
+ latest_timestamp = max(
156
+ each_grouped.timestamp for each_grouped in each_signature_records
157
+ )
158
+ clusters.append(
159
+ SignatureCluster(
160
+ signature=each_signature,
161
+ count=len(each_signature_records),
162
+ latest_timestamp=latest_timestamp,
163
+ score=score,
164
+ )
165
+ )
166
+ return sorted(
167
+ clusters,
168
+ key=lambda cluster: (cluster.score, cluster.count),
169
+ reverse=True,
170
+ )
171
+
172
+
173
+ def _mean_duration(all_samples: list[TimingSample]) -> float:
174
+ """Return the mean duration in milliseconds of the given samples."""
175
+ return sum(each_sample.duration_ms for each_sample in all_samples) / len(
176
+ all_samples
177
+ )
178
+
179
+
180
+ def detect_timing_regressions(
181
+ all_samples: list[TimingSample],
182
+ ) -> list[TimingRegression]:
183
+ """Flag operations whose recent runs are slower than their earliest runs.
184
+
185
+ Args:
186
+ all_samples: Timing samples across one or more operations.
187
+
188
+ Returns:
189
+ One regression per operation whose recent-to-baseline duration ratio
190
+ reaches the regression threshold, highest ratio first.
191
+ """
192
+ samples_by_operation: dict[str, list[TimingSample]] = defaultdict(list)
193
+ for each_sample in all_samples:
194
+ samples_by_operation[each_sample.operation].append(each_sample)
195
+ regressions: list[TimingRegression] = []
196
+ for each_operation, each_operation_samples in samples_by_operation.items():
197
+ if len(each_operation_samples) < MIN_TIMING_SAMPLES_PER_HALF * SAMPLE_HALVES:
198
+ continue
199
+ ordered_samples = sorted(
200
+ each_operation_samples, key=lambda sample: sample.timestamp
201
+ )
202
+ baseline_ms = _mean_duration(ordered_samples[:MIN_TIMING_SAMPLES_PER_HALF])
203
+ recent_ms = _mean_duration(ordered_samples[-MIN_TIMING_SAMPLES_PER_HALF:])
204
+ if baseline_ms <= 0:
205
+ continue
206
+ ratio = recent_ms / baseline_ms
207
+ if ratio >= TIMING_REGRESSION_RATIO:
208
+ regressions.append(
209
+ TimingRegression(
210
+ operation=each_operation,
211
+ baseline_ms=baseline_ms,
212
+ recent_ms=recent_ms,
213
+ ratio=ratio,
214
+ )
215
+ )
216
+ return sorted(regressions, key=lambda regression: regression.ratio, reverse=True)
217
+
218
+
219
+ def _record_from_json_dict(all_record_fields: dict[str, str]) -> LogRecord:
220
+ """Rebuild a LogRecord from one collect_log_window JSON dict."""
221
+ return LogRecord(
222
+ timestamp=datetime.fromisoformat(all_record_fields[RECORD_TIMESTAMP_KEY]),
223
+ source=all_record_fields[RECORD_SOURCE_KEY],
224
+ level=all_record_fields[RECORD_LEVEL_KEY],
225
+ message=all_record_fields[RECORD_MESSAGE_KEY],
226
+ )
227
+
228
+
229
+ def main() -> int:
230
+ """Read records on stdin and print ranked signature clusters.
231
+
232
+ Returns:
233
+ The process exit code; zero on success, one when stdin is not a JSON
234
+ list of well-formed records.
235
+ """
236
+ try:
237
+ records_payload = json.load(sys.stdin)
238
+ except json.JSONDecodeError as decode_error:
239
+ print(f"cluster_recurrences: stdin is not valid JSON: {decode_error}", file=sys.stderr)
240
+ return 1
241
+ if not isinstance(records_payload, list):
242
+ print("cluster_recurrences: stdin JSON must be a list of records", file=sys.stderr)
243
+ return 1
244
+ try:
245
+ records = [
246
+ _record_from_json_dict(each_record_fields)
247
+ for each_record_fields in records_payload
248
+ ]
249
+ except (KeyError, TypeError, ValueError) as record_error:
250
+ print(f"cluster_recurrences: malformed record in stdin: {record_error}", file=sys.stderr)
251
+ return 1
252
+ clusters = rank_signature_clusters(records, datetime.now())
253
+ for each_cluster in clusters:
254
+ print(
255
+ f"{each_cluster.count}\t{each_cluster.score:.2f}\t{each_cluster.signature}"
256
+ )
257
+ return 0
258
+
259
+
260
+ if __name__ == "__main__":
261
+ raise SystemExit(main())
@@ -0,0 +1,199 @@
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())
@@ -0,0 +1,12 @@
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`. |
@@ -0,0 +1,23 @@
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
@@ -0,0 +1,24 @@
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"