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
package/CLAUDE.md CHANGED
@@ -115,30 +115,33 @@ Before any coding task, call the `initial_instructions` tool to load the Serena
115
115
 
116
116
  ### Tool hierarchy for code navigation
117
117
  1. **Serena** — symbol-level navigation (declarations, references, implementations, rename)
118
- 2. **Everything** (`everything_search`) — file-system search by name/path/extension
118
+ 2. **es.exe** — file-system search by name/path/extension/size/date (Everything CLI)
119
119
  3. **Grep/Glob** — content and pattern matching
120
120
 
121
- ## Everything Search (MCP Tool)
121
+ ## Everything Search (es.exe CLI)
122
122
 
123
- This machine has **Everything (voidtools)** running with an HTTP server on port 54321.
124
- The `everything_search` MCP tool is available in every session.
123
+ Search files by name, path, extension, size, or date with the Everything command-line tool `es.exe`. It reads the same live index the desktop app keeps, so results return instantly.
125
124
 
126
- ### Use Everything for file-system searches
127
- Use `everything_search` for finding files by name, path, extension, size, or date. For content searches, use Grep Everything's `content:` search is a fallback when Grep returns nothing.
125
+ ### Invocation and scope tokens
126
+ Run `es.exe` with a scoped query a project path, an `ext:`/`dm:`/`size:` filter, or a name pattern. The `es_exe_path_rewriter` hook resolves a `{project-name}` placeholder or a bare registry key from `~/.claude/project-paths.json` into its quoted absolute path before the command runs (it allows and rewrites, never blocks).
127
+
128
+ ### Hard limits
129
+ - Scope every search. A bare whole-drive scan or a network-share sweep is out of bounds.
128
130
 
129
131
  ### Fallback order
130
- 1. **Everything** (`everything_search`) — file-system search by name/path/extension/size/date, and content search
131
- 2. **Grep** — complex regex content searches if Everything's `content:` returns nothing
132
- 3. **Glob** — precise relative-path pattern matching within the current project
132
+ 1. **es.exe** — file-system search by name/path/extension/size/date
133
+ 2. **Debug** — try to find out why es.exe isn't working, and then prompt user for next-steps if you can't self-heal.
134
+ 3. **Grep** — file-content search (Grep owns content)
135
+ 4. **Glob** — relative-path pattern matching within the current project
133
136
 
134
137
  ### Search syntax quick reference
135
- - `ext:py` — find by extension (multiple: `ext:ts;js`)
138
+ - `ext:py` — find by extension (multiple: `ext:ts | ext:js`)
136
139
  - `path:src\components` — match against full path
137
- - `count:10` — limit number of results to 10
138
140
  - `*.config.*` — wildcards
139
141
  - `size:>10mb` — size filter
140
142
  - `dm:today` / `dm:thisweek` — date modified filter
141
- - `content:keyword` — search inside file contents
142
- - `parent:node_modules package.json` — match parent folder
143
+ - `-n 50` — limit results; `-sort dm` — sort by date modified
143
144
  - `foo bar` — AND, `foo | bar` — OR, `!foo` — NOT
144
145
  - `"exact phrase"` — literal match
146
+
147
+ Full operator reference: `skills/everything-search/SKILL.md`.
@@ -8,7 +8,8 @@ Shared output schema and audit-loop contract used by `/bugteam`, `/qbug`, `/find
8
8
  - Adversarial second pass
9
9
  - Haiku secondary auditor
10
10
  - Post-fix self-audit
11
- - Persistence (loop-N-audit.json, loop-N-diagnostics.json)
11
+ - De-dup and merge
12
+ - Persistence (loop-<L>-audit.json, loop-<L>-diagnostics.json)
12
13
 
13
14
  ## Finding schema
14
15
 
@@ -18,7 +19,7 @@ Each finding an audit produces MUST be one of exactly two shapes.
18
19
 
19
20
  ```json
20
21
  {
21
- "id": "loop<N>-<K>",
22
+ "id": "loop<L>-<K>",
22
23
  "file": "path/relative/to/repo/root.py",
23
24
  "line": 123,
24
25
  "category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
@@ -29,7 +30,7 @@ Each finding an audit produces MUST be one of exactly two shapes.
29
30
  }
30
31
  ```
31
32
 
32
- `id` is `loop<N>-<K>` where `N` is the loop counter (1-based) and `K` is the 1-based index within the loop. For `/findbugs` which runs once, use `find<K>`.
33
+ `id` is `loop<L>-<K>` where `L` is the loop counter (1-based) and `K` is the 1-based index within the loop. For `/findbugs` which runs once, use `find<K>`.
33
34
 
34
35
  ### Shape B — structured proof-of-absence
35
36
 
@@ -98,16 +99,16 @@ For single-subagent skills (`/qbug`, `/findbugs`) the LEAD spawns two `Agent()`
98
99
 
99
100
  Both audit the same diff. The secondary returns findings to the LEAD only — never posted to the PR.
100
101
 
101
- Merge rules:
102
+ Merge rules — applied whenever the LEAD combines findings from multiple sources (primary auditor, Haiku secondary auditor, adversarial pass):
102
103
 
103
- - **De-dup key**: `(file, line, category)`.
104
- - **Severity conflict**: max wins (P0 > P1 > P2).
105
- - **Unique-to-Haiku findings**: added to the primary set with Haiku's severity and source annotation.
104
+ - **De-dup key**: `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple are the same finding and collapse into one entry.
105
+ - **Severity conflict**: max wins (`P0 > P1 > P2`). When sources disagree on severity for the same de-dup key, the merged entry keeps the highest severity.
106
+ - **Unique-to-secondary findings**: added to the merged set with the secondary's severity and source annotation.
106
107
  - **Unique-to-primary findings**: kept as-is.
107
- - **Zero Haiku findings**: primary set trusted; proceed.
108
- - **Malformed or non-parseable Haiku output**: lead trusts the primary set, logs the event in `loop-<N>-diagnostics.json` under `haiku_findings` as `[{"parse_error": "<message>"}]`.
108
+ - **Zero secondary findings**: the primary set is trusted and the audit moves on.
109
+ - **Malformed or non-parseable secondary output**: lead trusts the primary set and logs the event in `loop-<L>-diagnostics.json` under `haiku_findings` as `[{"parse_error": "<message>"}]`.
109
110
 
110
- For multi-subagent skills (`/bugteam`) the parallel-auditors pattern in [`audit-and-teammates.md`](audit-and-teammates.md) already provides cross-model coverage via the three variant teammates.
111
+ For `/bugteam`, the single audit agent provides per-category coverage by walking all A–P rubrics in one invocation.
111
112
 
112
113
  ## Post-fix self-audit
113
114
 
@@ -128,11 +129,22 @@ Sequence:
128
129
 
129
130
  `converged` exit condition: `primary_audit_clean AND post_fix_audit_clean` for the committing loop.
130
131
 
132
+ ## De-dup and merge
133
+
134
+ Findings from primary, adversarial, Haiku secondary, and post-fix passes are merged into a single deduped finding list before persistence.
135
+
136
+ - **De-dup key:** `(file, line, category)`. Two findings sharing the same `(file, line, category)` tuple collapse into a single deduped entry.
137
+ - **Severity conflict resolution:** `max wins`. When merged findings disagree on severity, the deduped entry carries the highest severity (`P0 > P1 > P2`).
138
+ - **Excerpt and failure_mode:** the deduped entry inherits these fields from the highest-severity contributing finding. Ties keep the first observed contributor.
139
+ - **`evidence_files`:** the deduped entry carries the union of every contributor's `evidence_files`, deduplicated and sorted.
140
+
141
+ The merged list lands in `loop-<L>-diagnostics.json` under both `merged` (one entry per contributing finding) and `deduped` (one entry per unique `(file, line, category)` tuple).
142
+
131
143
  ## Persistence
132
144
 
133
145
  Every audit loop writes two JSON files under the skill's scoped temp directory (resolved via `tempfile.gettempdir()`):
134
146
 
135
- ### `loop-<N>-audit.json`
147
+ ### `loop-<L>-audit.json`
136
148
 
137
149
  ```json
138
150
  {
@@ -142,7 +154,7 @@ Every audit loop writes two JSON files under the skill's scoped temp directory (
142
154
  }
143
155
  ```
144
156
 
145
- ### `loop-<N>-diagnostics.json`
157
+ ### `loop-<L>-diagnostics.json`
146
158
 
147
159
  ```json
148
160
  {
@@ -15,6 +15,7 @@ Python scripts invoked at runtime by the PR-loop skills. Each script is a standa
15
15
  | `terminology_sweep.py` | Flags a prose term that near-misses an identifier introduced on added code lines of a unified diff (shared leading word, divergent tail) |
16
16
  | `reviews_disabled.py` | Shared helper for the `CLAUDE_REVIEWS_DISABLED` opt-out gate; parses the env-var token to find which reviewer types are suppressed |
17
17
  | `copilot_quota.py` | Copilot premium-request quota pre-check: resolves a configured GitHub account, reads its remaining `premium_interactions` quota via `gh api copilot_internal/user`, and exits 0 (run Copilot) or non-zero (skip: out of quota, API down, or no account configured) |
18
+ | `reviewer_availability.py` | Unified reviewer-availability entry point for Copilot and Bugbot: reuses `copilot_quota.py` and `reviews_disabled.py` and exits 0 when the named `--reviewer` may be spawned, non-zero when it is opted out or (for Copilot) out of quota |
18
19
  | `fix_hookspath.py` | Repairs a malformed `core.hooksPath` global git config entry |
19
20
  | `_claude_permissions_common.py` | Internal helpers shared by the grant/revoke scripts: atomic settings.json writes, list mutation, path helpers |
20
21
 
@@ -7,6 +7,7 @@ Runnable helpers used by **bugteam**, **qbug**, **pr-converge**, and related ski
7
7
  | File | Purpose |
8
8
  | --- | --- |
9
9
  | `preflight.py` | Local checks before a PR-loop run (pytest discovery, optional pre-commit, hooksPath sanity). |
10
+ | `reviewer_availability.py` | Unified availability check for Copilot and Bugbot; reuses `copilot_quota.py` and `reviews_disabled.py` and exits 0 when the named `--reviewer` may be spawned. |
10
11
  | `code_rules_gate.py` | CODE_RULES gate over PR-scoped diffs (`--base`, staged-only, path filters). |
11
12
  | `fix_hookspath.py` | Repair `core.hooksPath` when it does not point at the packaged git-hooks tree. |
12
13
  | `grant_project_claude_permissions.py` / `revoke_project_claude_permissions.py` | Claude Code permission JSON helpers used during publish-style flows. |
@@ -299,9 +299,11 @@ def write_atomically_with_mode(
299
299
 
300
300
  Raises:
301
301
  OSError: When os.open or os.fdopen fails. The raw file descriptor
302
- is closed before re-raising so the descriptor does not leak.
302
+ is closed and the on-disk temporary file is unlinked before
303
+ re-raising so neither the descriptor nor the file leaks.
303
304
  MemoryError: When os.fdopen runs out of buffer memory; the file
304
- descriptor is closed before re-raising.
305
+ descriptor is closed and the temporary file is unlinked
306
+ before re-raising.
305
307
  """
306
308
  file_descriptor = os.open(
307
309
  str(temporary_path),
@@ -312,6 +314,10 @@ def write_atomically_with_mode(
312
314
  writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
313
315
  except (OSError, MemoryError):
314
316
  os.close(file_descriptor)
317
+ try:
318
+ os.unlink(str(temporary_path))
319
+ except OSError:
320
+ pass
315
321
  raise
316
322
  with writer:
317
323
  writer.write(serialized_content)
@@ -321,7 +327,8 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
321
327
  """Write settings to a JSON file atomically with permission preservation.
322
328
 
323
329
  Creates a temporary sibling file, writes content, then atomically
324
- replaces the target. Cleans up the temporary file in a finally block.
330
+ replaces the target. Cleans up the temporary file in a finally block,
331
+ printing a warning to stderr when that cleanup unlink fails.
325
332
 
326
333
  Args:
327
334
  settings_path: Path to the target settings JSON file.
@@ -352,8 +359,12 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
352
359
  if temporary_path.exists():
353
360
  try:
354
361
  temporary_path.unlink()
355
- except OSError:
356
- pass
362
+ except OSError as unlink_error:
363
+ print(
364
+ f"Warning: could not remove temp file {temporary_path}: "
365
+ f"{type(unlink_error).__name__}: {unlink_error}",
366
+ file=sys.stderr,
367
+ )
357
368
 
358
369
 
359
370
  def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
@@ -1673,7 +1673,6 @@ def main(all_arguments: list[str]) -> int:
1673
1673
  if arguments.staged:
1674
1674
  all_terminology_findings = staged_terminology_findings(repository_root)
1675
1675
  _report_terminology_findings(all_terminology_findings)
1676
- terminology_exit_code = 1 if all_terminology_findings else 0
1677
1676
  staged_test_exit_code = run_staged_test_files(repository_root)
1678
1677
  staged_file_paths = paths_from_git_staged(repository_root)
1679
1678
  staged_file_paths = filter_paths_under_prefixes(
@@ -1682,7 +1681,7 @@ def main(all_arguments: list[str]) -> int:
1682
1681
  arguments.only_under,
1683
1682
  )
1684
1683
  if not staged_file_paths:
1685
- return terminology_exit_code or staged_test_exit_code
1684
+ return staged_test_exit_code
1686
1685
  staged_added_lines = added_lines_by_file_staged(
1687
1686
  repository_root, staged_file_paths
1688
1687
  )
@@ -1693,7 +1692,7 @@ def main(all_arguments: list[str]) -> int:
1693
1692
  all_added_lines_by_path=staged_added_lines,
1694
1693
  read_staged_content_flag=True,
1695
1694
  )
1696
- return gate_exit_code or terminology_exit_code or staged_test_exit_code
1695
+ return gate_exit_code or staged_test_exit_code
1697
1696
  file_paths = paths_from_git_diff(repository_root, arguments.base)
1698
1697
  file_paths = filter_paths_under_prefixes(
1699
1698
  file_paths,
@@ -17,6 +17,7 @@ Named constants for every script in `_shared/pr-loop/scripts/`. Each module owns
17
17
  | `reviews_disabled_constants.py` | `reviews_disabled.py` — `CLAUDE_REVIEWS_DISABLED` token taxonomy |
18
18
  | `copilot_quota_constants.py` | `copilot_quota.py` — `COPILOT_QUOTA_ACCOUNT` env-var name, `copilot_internal/user` API path, the `premium_interactions` gating field names, the four skip/run exit codes, and the default `.env` path |
19
19
  | `terminology_sweep_constants.py` | `terminology_sweep.py` — identifier and prose-token regexes, diff-parsing prefixes, code-file extensions, and the finding-message template |
20
+ | `reviewer_availability_constants.py` | `reviewer_availability.py` — the available/down exit codes for the unified reviewer-availability pre-check |
20
21
  | `__init__.py` | Empty package marker |
21
22
 
22
23
  ## Convention
@@ -0,0 +1,12 @@
1
+ """Configuration constants for the unified reviewer-availability pre-check.
2
+
3
+ The down code is 3 so a deliberate reviewer-down report stays distinguishable
4
+ from a generic interpreter crash (exit 1) and an argparse usage error
5
+ (exit 2). A caller that gates on the down code treats any other non-zero exit
6
+ as a broken check, not a down reviewer, and fails open.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ EXIT_CODE_REVIEWER_AVAILABLE: int = 0
12
+ EXIT_CODE_REVIEWER_DOWN: int = 3
@@ -24,8 +24,6 @@ HYPHENATED_PROSE_TOKEN_PATTERN: re.Pattern[str] = re.compile(
24
24
  r"\b[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z][A-Za-z0-9]*)+\b"
25
25
  )
26
26
 
27
- PROSE_WORD_PATTERN: re.Pattern[str] = re.compile(r"[A-Za-z][A-Za-z0-9]*")
28
-
29
27
  STRING_LITERAL_CONTENT_PATTERN: re.Pattern[str] = re.compile(
30
28
  r"\"([^\"]*)\"|'([^']*)'|`([^`]*)`"
31
29
  )
@@ -0,0 +1,182 @@
1
+ """Unified reviewer-availability pre-check for GitHub Copilot and Cursor Bugbot.
2
+
3
+ Run this before either reviewer step spawns. It answers one question: is the
4
+ named reviewer available to run right now. It reuses two existing checks
5
+ instead of re-implementing their rules. The ``CLAUDE_REVIEWS_DISABLED``
6
+ opt-out gate lives in ``reviews_disabled.py``. The Copilot premium-interaction
7
+ quota pre-check lives in ``copilot_quota.py``.
8
+
9
+ Copilot counts as down in two cases: it is opted out via
10
+ ``CLAUDE_REVIEWS_DISABLED``, or its quota pre-check returns anything other
11
+ than quota available (out of quota, the quota API or account down, or no
12
+ account configured).
13
+
14
+ Bugbot carries no quota or availability API. It counts as down only when it
15
+ is opted out via ``CLAUDE_REVIEWS_DISABLED``. A genuine runtime outage shows
16
+ up later as a poll timeout, not here.
17
+
18
+ The exit code tells the caller what to do. Exit 0 means the reviewer is
19
+ available and may be spawned. The documented down code (3) means skip it. Any
20
+ other non-zero exit is a broken check — an interpreter crash or a usage
21
+ error — not a down report, so a gating caller fails open on it. Every path
22
+ prints one line: available lines go to stdout, down lines go to stderr.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import sys
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+
32
+ from copilot_quota import (
33
+ COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
34
+ EXIT_CODE_QUOTA_AVAILABLE,
35
+ evaluate_copilot_quota,
36
+ )
37
+ from pr_loop_shared_constants.reviewer_availability_constants import (
38
+ EXIT_CODE_REVIEWER_AVAILABLE,
39
+ EXIT_CODE_REVIEWER_DOWN,
40
+ )
41
+ from pr_loop_shared_constants.reviews_disabled_constants import (
42
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
43
+ CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
44
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
45
+ )
46
+ from reviews_disabled import is_bugbot_disabled_via_env, is_copilot_disabled_via_env
47
+
48
+
49
+ @dataclass(frozen=True)
50
+ class ReviewerAvailability:
51
+ """One reviewer-availability outcome: the exit code and the single log line.
52
+
53
+ The exit code tells the caller what to do, where 0 means the reviewer may
54
+ be spawned and the documented down code (3) means skip it. The CLI prints
55
+ the message on one line, to stdout when available and to stderr when down.
56
+ """
57
+
58
+ exit_code: int
59
+ message: str
60
+
61
+
62
+ def _evaluate_copilot_availability(env_file_path: Path) -> ReviewerAvailability:
63
+ """Decide whether GitHub Copilot is available to spawn.
64
+
65
+ Args:
66
+ env_file_path: The ``.env`` file the quota pre-check consults for the
67
+ configured account when neither the flag nor the environment
68
+ variable names one.
69
+
70
+ Returns:
71
+ A ReviewerAvailability that is down when Copilot is opted out via
72
+ ``CLAUDE_REVIEWS_DISABLED`` or the quota pre-check reports anything
73
+ other than quota available, and available otherwise.
74
+ """
75
+ if is_copilot_disabled_via_env():
76
+ return ReviewerAvailability(
77
+ EXIT_CODE_REVIEWER_DOWN,
78
+ f"reviewer-availability: copilot is disabled via "
79
+ f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping.",
80
+ )
81
+ quota_decision = evaluate_copilot_quota(cli_account=None, env_file_path=env_file_path)
82
+ if quota_decision.exit_code != EXIT_CODE_QUOTA_AVAILABLE:
83
+ return ReviewerAvailability(EXIT_CODE_REVIEWER_DOWN, quota_decision.message)
84
+ return ReviewerAvailability(EXIT_CODE_REVIEWER_AVAILABLE, quota_decision.message)
85
+
86
+
87
+ def _evaluate_bugbot_availability() -> ReviewerAvailability:
88
+ """Decide whether Cursor Bugbot is available to spawn.
89
+
90
+ Bugbot carries no quota or availability API, so this checks only the
91
+ deterministic ``CLAUDE_REVIEWS_DISABLED`` opt-out.
92
+
93
+ Returns:
94
+ A ReviewerAvailability that is down when Bugbot is opted out via
95
+ ``CLAUDE_REVIEWS_DISABLED``, and available otherwise.
96
+ """
97
+ if is_bugbot_disabled_via_env():
98
+ return ReviewerAvailability(
99
+ EXIT_CODE_REVIEWER_DOWN,
100
+ f"reviewer-availability: bugbot is disabled via "
101
+ f"{CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME} — skipping.",
102
+ )
103
+ return ReviewerAvailability(
104
+ EXIT_CODE_REVIEWER_AVAILABLE,
105
+ "reviewer-availability: bugbot is available.",
106
+ )
107
+
108
+
109
+ def evaluate_reviewer_availability(
110
+ reviewer_token: str, env_file_path: Path
111
+ ) -> ReviewerAvailability:
112
+ """Decide whether the named reviewer is available to spawn.
113
+
114
+ Args:
115
+ reviewer_token: Either the Copilot or the Bugbot reviewer token.
116
+ env_file_path: The ``.env`` file the Copilot quota pre-check consults
117
+ for the configured account.
118
+
119
+ Returns:
120
+ The ReviewerAvailability for the named reviewer.
121
+
122
+ Raises:
123
+ ValueError: When reviewer_token names neither the Copilot nor the
124
+ Bugbot reviewer token.
125
+ """
126
+ if reviewer_token == CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN:
127
+ return _evaluate_copilot_availability(env_file_path)
128
+ if reviewer_token == CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN:
129
+ return _evaluate_bugbot_availability()
130
+ raise ValueError(f"unknown reviewer token: {reviewer_token}")
131
+
132
+
133
+ def _parse_arguments(all_argv: list[str]) -> argparse.Namespace:
134
+ """Parse command-line arguments for the reviewer-availability check.
135
+
136
+ Args:
137
+ all_argv: Argument list excluding the program name, typically
138
+ ``sys.argv[1:]``.
139
+
140
+ Returns:
141
+ Namespace exposing a ``reviewer`` attribute constrained to the
142
+ Copilot and Bugbot reviewer tokens.
143
+ """
144
+ parser = argparse.ArgumentParser(description=__doc__)
145
+ parser.add_argument(
146
+ "--reviewer",
147
+ required=True,
148
+ choices=[
149
+ CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
150
+ CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
151
+ ],
152
+ help="Reviewer to check availability for",
153
+ )
154
+ return parser.parse_args(all_argv)
155
+
156
+
157
+ def main(all_arguments: list[str]) -> int:
158
+ """Run the reviewer-availability check end-to-end and print its decision.
159
+
160
+ Args:
161
+ all_arguments: Argument list excluding the program name.
162
+
163
+ Returns:
164
+ 0 when the named reviewer is available to spawn, the documented
165
+ reviewer-down exit code when it is down.
166
+ """
167
+ arguments = _parse_arguments(all_arguments)
168
+ availability = evaluate_reviewer_availability(
169
+ reviewer_token=arguments.reviewer,
170
+ env_file_path=COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH,
171
+ )
172
+ message_stream = (
173
+ sys.stdout
174
+ if availability.exit_code == EXIT_CODE_REVIEWER_AVAILABLE
175
+ else sys.stderr
176
+ )
177
+ print(availability.message, file=message_stream)
178
+ return availability.exit_code
179
+
180
+
181
+ if __name__ == "__main__":
182
+ raise SystemExit(main(sys.argv[1:]))
@@ -103,6 +103,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
103
103
  choices=[
104
104
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
105
105
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
106
+ CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
106
107
  ],
107
108
  help="Reviewer token to test against CLAUDE_REVIEWS_DISABLED",
108
109
  )
@@ -122,6 +123,7 @@ def main(all_arguments: list[str]) -> int:
122
123
  disabled_checker_by_reviewer = {
123
124
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: is_bugbot_disabled_via_env,
124
125
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: is_bugteam_disabled_via_env,
126
+ CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN: is_copilot_disabled_via_env,
125
127
  }
126
128
  return 0 if disabled_checker_by_reviewer[arguments.reviewer]() else 1
127
129
 
@@ -7,11 +7,18 @@ finds the other.
7
7
 
8
8
  This sweep reads a unified diff and collects every multi-word identifier added
9
9
  on code lines. Then it scans each added prose line — a Markdown line, or a
10
- comment, docstring, or string inside a code file. It flags a hyphen or space
11
- variant that shares an identifier's leading word but diverges in the tail. Each
10
+ comment, docstring, or string inside a code file. It flags a hyphenated term
11
+ that shares an identifier's leading word but diverges in the tail. Each
12
12
  near-miss prints as one ``file:line`` finding, and the run exits non-zero when
13
13
  any finding remains, so a commit gate can block on it.
14
14
 
15
+ Only hyphenated terms are candidates. A hyphen marks a deliberate compound —
16
+ the author bound the words into one term — so a divergent tail there is a real
17
+ naming drift. A bare spaced word run is ordinary prose: windowing every
18
+ sentence that starts with an identifier's first word ("each attempt", "file
19
+ exists") floods a vocabulary-dense change with false findings and gives the
20
+ gate nothing actionable.
21
+
15
22
  A shared leading token alone is too weak a signal: ordinary English compounds
16
23
  such as ``read-only`` and ``data-driven`` collide with unrelated identifiers
17
24
  (``read_config``, ``data_source``) and would block a commit falsely. To keep the
@@ -56,7 +63,6 @@ from pr_loop_shared_constants.terminology_sweep_constants import ( # noqa: E402
56
63
  JSDOC_CONTINUATION_MARKER,
57
64
  MARKDOWN_FILE_EXTENSION,
58
65
  MINIMUM_IDENTIFIER_TOKEN_COUNT,
59
- PROSE_WORD_PATTERN,
60
66
  PYTHON_COMMENT_MARKER,
61
67
  SNAKE_CASE_IDENTIFIER_PATTERN,
62
68
  STRING_LITERAL_CONTENT_PATTERN,
@@ -234,35 +240,6 @@ def _hyphenated_candidates(fragment: str) -> list[tuple[str, IdentifierTuple]]:
234
240
  return all_candidates
235
241
 
236
242
 
237
- def _spaced_candidates(
238
- fragment: str,
239
- identifiers_by_first_token: dict[str, list[IdentifierTuple]],
240
- ) -> list[tuple[str, IdentifierTuple]]:
241
- """Return spaced word windows whose first word matches an identifier prefix.
242
-
243
- Only a window anchored at a known identifier prefix and sized to one of that
244
- prefix's identifiers is returned, which bounds the windows to the shapes the
245
- near-miss check can act on.
246
-
247
- Args:
248
- fragment: The prose fragment to scan.
249
- identifiers_by_first_token: Identifier tuples grouped by leading token.
250
-
251
- Returns:
252
- ``(display, token_tuple)`` pairs for each candidate window.
253
- """
254
- all_words = [
255
- each_word.lower() for each_word in PROSE_WORD_PATTERN.findall(fragment)
256
- ]
257
- all_candidates: list[tuple[str, IdentifierTuple]] = []
258
- for each_index, each_word in enumerate(all_words):
259
- for each_identifier in identifiers_by_first_token.get(each_word, []):
260
- window = tuple(all_words[each_index : each_index + len(each_identifier)])
261
- if len(window) == len(each_identifier):
262
- all_candidates.append((" ".join(window), window))
263
- return all_candidates
264
-
265
-
266
243
  def _tokens_are_plural_variants(first_word: str, second_word: str) -> bool:
267
244
  """Return whether two words are singular/plural forms of one word.
268
245
 
@@ -348,7 +325,6 @@ def _findings_for_line(
348
325
  reported_tuples: set[IdentifierTuple] = set()
349
326
  for each_fragment in _prose_fragments(file_path, line_text):
350
327
  all_candidates = _hyphenated_candidates(each_fragment)
351
- all_candidates += _spaced_candidates(each_fragment, identifiers_by_first_token)
352
328
  for each_display, each_tuple in all_candidates:
353
329
  matched_identifier = _near_miss_identifier(
354
330
  each_tuple, all_identifier_tuples, identifiers_by_first_token
@@ -24,6 +24,8 @@ pytest suite for the scripts and constants in `_shared/pr-loop/scripts/`. Each t
24
24
  | `test_reviews_disabled.py` | `reviews_disabled.py` opt-out gate parsing |
25
25
  | `test_copilot_quota.py` | `copilot_quota.py` end-to-end: account resolution, premium-quota classification, exit codes, and skip logging |
26
26
  | `test_copilot_quota_constants.py` | `pr_loop_shared_constants/copilot_quota_constants.py` |
27
+ | `test_reviewer_availability.py` | `reviewer_availability.py` end-to-end: Copilot and Bugbot availability, opt-out via `CLAUDE_REVIEWS_DISABLED`, and every Copilot quota outcome |
28
+ | `test_reviewer_availability_constants.py` | `pr_loop_shared_constants/reviewer_availability_constants.py` |
27
29
  | `test_revoke_project_claude_permissions.py` | `revoke_project_claude_permissions.py` end-to-end |
28
30
  | `test_agent_config_carveout.py` | Agent-config deny-rule carve-out logic |
29
31
  | `conftest.py` | Shared pytest fixtures |