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
@@ -718,12 +718,13 @@ const OLD_FOLDED_HOOKS_SETTINGS = {
718
718
  };
719
719
 
720
720
 
721
- test('FOLDED_HOOK_RELATIVE_PATHS contains all 16 hooks removed from hooks.json', () => {
722
- assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size, 16);
721
+ test('FOLDED_HOOK_RELATIVE_PATHS contains all 16 hooks removed from hooks.json plus the retired md_to_html_blocker', () => {
722
+ assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size, 17);
723
723
  assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/write_existing_file_blocker.py'));
724
724
  assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/plain_language_blocker.py'));
725
725
  assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/code_rules_enforcer.py'));
726
726
  assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/pytest_testpaths_orphan_blocker.py'));
727
+ assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/md_to_html_blocker.py'));
727
728
  });
728
729
 
729
730
 
@@ -746,16 +747,25 @@ test('FOLDED_HOOK_RELATIVE_PATHS lists every hook the PreToolUse dispatcher host
746
747
  'blocking/open_questions_in_plans_blocker.py',
747
748
  'blocking/plain_language_blocker.py',
748
749
  ];
750
+ const retiredHooks = [
751
+ 'blocking/md_to_html_blocker.py',
752
+ ];
749
753
  for (const hostedPath of dispatcherHostedHooks) {
750
754
  assert.ok(
751
755
  FOLDED_HOOK_RELATIVE_PATHS.has(hostedPath),
752
756
  `dispatcher-hosted hook ${hostedPath} must be in FOLDED_HOOK_RELATIVE_PATHS so a reinstall prunes its stale standalone entry and it does not double-run`
753
757
  );
754
758
  }
759
+ for (const retiredPath of retiredHooks) {
760
+ assert.ok(
761
+ FOLDED_HOOK_RELATIVE_PATHS.has(retiredPath),
762
+ `retired hook ${retiredPath} must be in FOLDED_HOOK_RELATIVE_PATHS so a reinstall prunes its stale standalone entry pointing at a script no longer on disk`
763
+ );
764
+ }
755
765
  assert.equal(
756
766
  FOLDED_HOOK_RELATIVE_PATHS.size,
757
- dispatcherHostedHooks.length,
758
- 'FOLDED_HOOK_RELATIVE_PATHS must hold exactly the dispatcher-hosted hooks, no more, no fewer'
767
+ dispatcherHostedHooks.length + retiredHooks.length,
768
+ 'FOLDED_HOOK_RELATIVE_PATHS must hold exactly the dispatcher-hosted hooks plus the retired hooks, no more, no fewer'
759
769
  );
760
770
  });
761
771
 
@@ -887,11 +897,12 @@ const OLD_POST_FOLDED_HOOKS_SETTINGS = {
887
897
  };
888
898
 
889
899
 
890
- test('POST_FOLDED_HOOK_RELATIVE_PATHS contains the three after-write hooks folded into the PostToolUse dispatcher', () => {
891
- assert.equal(POST_FOLDED_HOOK_RELATIVE_PATHS.size, 3);
900
+ test('POST_FOLDED_HOOK_RELATIVE_PATHS contains the after-write hooks and the retired md_to_html_companion', () => {
901
+ assert.equal(POST_FOLDED_HOOK_RELATIVE_PATHS.size, 4);
892
902
  assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('validation/mypy_validator.py'));
893
903
  assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/auto_formatter.py'));
894
904
  assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/doc_gist_auto_publish.py'));
905
+ assert.ok(POST_FOLDED_HOOK_RELATIVE_PATHS.has('workflow/md_to_html_companion.py'));
895
906
  });
896
907
 
897
908
 
@@ -7,7 +7,6 @@ Slash-command definitions installed into `~/.claude/commands/` by `bin/install.m
7
7
  | File | Command | What it does |
8
8
  |---|---|---|
9
9
  | `commit.md` | `/commit` | Commits and pushes changes to GitHub |
10
- | `doc-gist.md` | `/doc-gist` | Uploads an HTML file as a secret gist and opens the htmlpreview URL |
11
10
  | `docupdate.md` | `/docupdate` | Updates documentation to match current code state |
12
11
  | `hook-log-extract.md` | `/hook-log-extract` | Extracts and formats hook log entries for a session |
13
12
  | `hook-log-init.md` | `/hook-log-init` | Initializes the Neon Postgres schema that backs the hook-log extractor (one-time per machine) |
@@ -94,4 +94,4 @@ If you already have the data, don't fetch it again.
94
94
 
95
95
  ## 11. ENFORCEMENT SURFACES
96
96
 
97
- ⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery, docstring-prose-matches-implementation; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit.
97
+ ⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery, docstring-prose-matches-implementation; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
@@ -77,7 +77,6 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
77
77
  | `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`) |
78
78
  | `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on |
79
79
  | `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
80
- | `md_to_html_blocker.py` | PreToolUse (Write/Edit) | Writing `.md` files when an `.html` companion is required |
81
80
  | `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
82
81
  | `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
83
82
  | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
@@ -86,9 +85,11 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
86
85
  | `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
87
86
  | `pytest_testpaths_orphan_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | New `test_*.py` files created under a directory absent from a package's explicit pytest `testpaths` allowlist |
88
87
  | `question_to_user_enforcer.py` | Stop | User-directed questions not routed through `AskUserQuestion` |
88
+ | `reviewer_spawn_gate.py` | PreToolUse (Bash) | A sentinel-marked autoconverge reviewer-spawn command (Copilot review request, Bugbot rerun comment) run while `reviewer_availability.py` reports that reviewer down or out of quota |
89
89
  | `send_user_file_open_locally_blocker.py` | PreToolUse (SendUserFile) | A desk-side file attach (`SendUserFile` with `status` not `proactive`); points to opening the file locally via `Show-Asset.ps1` |
90
90
  | `sensitive_file_protector.py` | PreToolUse (Write/Edit) | Writes to sensitive credential or config files |
91
91
  | `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
92
+ | `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
92
93
  | `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
93
94
  | `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
94
95
  | `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
@@ -105,8 +106,6 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
105
106
  | File | Role |
106
107
  |---|---|
107
108
  | `_gh_body_arg_utils.py` | Parsing helpers for `gh_body_arg_blocker.py` |
108
- | `_md_to_html_blocker_test_support.py` | Test fixtures shared across `md_to_html_blocker` tests |
109
- | `md_path_exemptions.py` | Path exemption logic for `md_to_html_blocker.py` |
110
109
  | `pr_description_body_audit.py` | Body audit logic for `pr_description_enforcer.py` |
111
110
  | `pr_description_command_parser.py` | `gh` command parsing for `pr_description_enforcer.py` |
112
111
  | `pr_description_pr_number.py` | PR number extraction logic |
@@ -117,4 +116,3 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
117
116
 
118
117
  - Tests live beside each hook as `test_<hookname>.py` or `test_<hookname>_<suffix>.py`. Run with `python -m pytest <test_file>`.
119
118
  - Tunable constants live in `hooks_constants/<hook_name>_constants.py`; the verified-commit family uses `blocking/config/verified_commit_constants.py`.
120
- - `conftest.py` gives shared pytest fixtures for the blocking test suite.
@@ -1,4 +1,9 @@
1
- """Constants-outside-config checks and the file-global constant use-count check."""
1
+ """Constants-outside-config checks and the file-global constant use-count check.
2
+
3
+ Also carries check_config_duplicate_path_anchor, which flags a config module
4
+ that rebuilds a directory a sibling module in the same package already
5
+ anchors from its own location.
6
+ """
2
7
 
3
8
  import ast
4
9
  import re
@@ -18,12 +23,16 @@ from code_rules_path_utils import ( # noqa: E402
18
23
  )
19
24
  from code_rules_shared import ( # noqa: E402
20
25
  _build_parent_map,
26
+ docstring_line_numbers,
21
27
  get_file_extension,
22
28
  is_migration_file,
23
29
  is_test_file,
24
30
  is_workflow_registry_file,
25
31
  )
26
32
 
33
+ from hooks_constants.blocking_check_limits import ( # noqa: E402
34
+ MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES,
35
+ )
27
36
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
28
37
  ALL_PYTHON_EXTENSIONS,
29
38
  FILE_GLOBAL_UPPER_SNAKE_PATTERN,
@@ -102,6 +111,7 @@ def _scan_function_body_constants(content: str) -> list[str]:
102
111
  """
103
112
  advisory_issues: list[str] = []
104
113
  lines = content.split("\n")
114
+ all_docstring_line_numbers = docstring_line_numbers(content)
105
115
  function_indent_stack: list[int] = []
106
116
  constant_pattern = re.compile(r"^([A-Z][A-Z0-9_]{2,})(?:\s*:\s*[^=]+)?\s*=\s*[^=]")
107
117
 
@@ -111,6 +121,9 @@ def _scan_function_body_constants(content: str) -> list[str]:
111
121
  if not stripped:
112
122
  continue
113
123
 
124
+ if each_line_number in all_docstring_line_numbers:
125
+ continue
126
+
114
127
  indent = len(each_line) - len(each_line.lstrip())
115
128
 
116
129
  while function_indent_stack and indent <= function_indent_stack[-1] and not stripped.startswith(("#", "@", ")")):
@@ -253,3 +266,153 @@ def check_file_global_constants_use_count(content: str, file_path: str) -> list[
253
266
  )
254
267
 
255
268
  return issues
269
+
270
+
271
+ def _references_dunder_file(node: ast.AST) -> bool:
272
+ """Return True when the expression tree reads ``__file__``."""
273
+ return any(
274
+ isinstance(each_node, ast.Name) and each_node.id == "__file__"
275
+ for each_node in ast.walk(node)
276
+ )
277
+
278
+
279
+ def _file_anchor_up_count(node: ast.AST) -> Optional[int]:
280
+ """Return how many directory levels a ``__file__`` anchor climbs, else None.
281
+
282
+ A ``parents[N]`` subscript on a ``__file__``-rooted expression climbs
283
+ ``N + 1`` levels; a chain of ``.parent`` attributes climbs one level per
284
+ link. Any other expression returns None.
285
+ """
286
+ if isinstance(node, ast.Subscript):
287
+ subscripted = node.value
288
+ if (
289
+ isinstance(subscripted, ast.Attribute)
290
+ and subscripted.attr == "parents"
291
+ and isinstance(node.slice, ast.Constant)
292
+ and isinstance(node.slice.value, int)
293
+ and _references_dunder_file(subscripted.value)
294
+ ):
295
+ return node.slice.value + 1
296
+ parent_step_count = 0
297
+ current_node = node
298
+ while isinstance(current_node, ast.Attribute) and current_node.attr == "parent":
299
+ parent_step_count += 1
300
+ current_node = current_node.value
301
+ if parent_step_count and _references_dunder_file(current_node):
302
+ return parent_step_count
303
+ return None
304
+
305
+
306
+ def _anchored_join_signature(node: ast.AST) -> Optional[tuple[int, str]]:
307
+ """Return (up_count, first literal segment) for an anchored ``/`` join, else None.
308
+
309
+ Flattens a left-leaning ``/`` chain, requires the left-most operand to be
310
+ a ``__file__`` anchor, and requires the first joined segment to be a
311
+ string literal.
312
+ """
313
+ if not isinstance(node, ast.BinOp) or not isinstance(node.op, ast.Div):
314
+ return None
315
+ all_segments: list[str] = []
316
+ current_node: ast.expr = node
317
+ while isinstance(current_node, ast.BinOp) and isinstance(current_node.op, ast.Div):
318
+ right_operand = current_node.right
319
+ if isinstance(right_operand, ast.Constant) and isinstance(right_operand.value, str):
320
+ all_segments.insert(0, right_operand.value)
321
+ else:
322
+ all_segments.insert(0, "")
323
+ current_node = current_node.left
324
+ anchor_up_count = _file_anchor_up_count(current_node)
325
+ if anchor_up_count is None or not all_segments or not all_segments[0]:
326
+ return None
327
+ return anchor_up_count, all_segments[0]
328
+
329
+
330
+ def _module_anchor_signatures(module_tree: ast.Module) -> dict[tuple[int, str], int]:
331
+ """Return line numbers keyed by anchored-join signature for top-level assignments."""
332
+ signatures_by_key: dict[tuple[int, str], int] = {}
333
+ for each_statement in module_tree.body:
334
+ if isinstance(each_statement, ast.Assign):
335
+ assigned_expression = each_statement.value
336
+ elif isinstance(each_statement, ast.AnnAssign) and each_statement.value is not None:
337
+ assigned_expression = each_statement.value
338
+ else:
339
+ continue
340
+ for each_node in ast.walk(assigned_expression):
341
+ signature = _anchored_join_signature(each_node)
342
+ if signature is not None:
343
+ signatures_by_key.setdefault(signature, each_statement.lineno)
344
+ return signatures_by_key
345
+
346
+
347
+ def _config_anchor_check_applies(file_path: str) -> bool:
348
+ """Return whether the duplicate-path-anchor check should inspect this file."""
349
+ return (
350
+ is_config_file(file_path)
351
+ and not is_test_file(file_path)
352
+ and get_file_extension(file_path) in ALL_PYTHON_EXTENSIONS
353
+ )
354
+
355
+
356
+ def _sibling_anchor_owner_names(config_directory: Path, written_name: str) -> dict[tuple[int, str], str]:
357
+ """Return the sibling config module name that first builds each anchor signature."""
358
+ owner_name_by_signature: dict[tuple[int, str], str] = {}
359
+ for each_sibling_path in sorted(config_directory.glob("*.py")):
360
+ if each_sibling_path.name == written_name or is_test_file(str(each_sibling_path)):
361
+ continue
362
+ try:
363
+ sibling_tree = ast.parse(each_sibling_path.read_text(encoding="utf-8"))
364
+ except (OSError, SyntaxError, UnicodeDecodeError):
365
+ continue
366
+ for each_signature in _module_anchor_signatures(sibling_tree):
367
+ owner_name_by_signature.setdefault(each_signature, each_sibling_path.name)
368
+ return owner_name_by_signature
369
+
370
+
371
+ def check_config_duplicate_path_anchor(content: str, file_path: str) -> list[str]:
372
+ """Flag a config module re-anchoring a path a sibling config module already builds.
373
+
374
+ Two config modules in the same ``config/`` directory that each anchor
375
+ ``Path(__file__)`` the same number of levels up and join the same first
376
+ literal segment are two sources of truth for one directory: a rename in
377
+ one module silently leaves the other pointing at the old folder. The
378
+ check fires on a config-module write whose module-level assignment joins
379
+ a ``__file__`` anchor (``parents[N]`` or a ``.parent`` chain) with a
380
+ literal segment that a sibling module in the same directory also joins at
381
+ the same depth. Compose one shared base constant and build the other path
382
+ from it. Test files, non-Python files, and modules outside a config
383
+ directory are exempt; an unreadable or unparseable sibling is skipped.
384
+
385
+ Args:
386
+ content: The Python source under validation.
387
+ file_path: The destination path, used to locate sibling config modules.
388
+
389
+ Returns:
390
+ One issue line per duplicated anchor, capped at the configured maximum.
391
+ """
392
+ if not _config_anchor_check_applies(file_path):
393
+ return []
394
+ try:
395
+ module_tree = ast.parse(content)
396
+ except SyntaxError:
397
+ return []
398
+ written_signatures = _module_anchor_signatures(module_tree)
399
+ if not written_signatures:
400
+ return []
401
+ config_directory = Path(file_path).parent
402
+ if not config_directory.is_dir():
403
+ return []
404
+ owner_name_by_signature = _sibling_anchor_owner_names(config_directory, Path(file_path).name)
405
+ issues: list[str] = []
406
+ for each_signature, each_line_number in sorted(written_signatures.items(), key=lambda pair: pair[1]):
407
+ owner_name = owner_name_by_signature.get(each_signature)
408
+ if owner_name is None:
409
+ continue
410
+ anchor_up_count, first_segment = each_signature
411
+ issues.append(
412
+ f"Line {each_line_number}: joins {first_segment} onto the same base, "
413
+ f"{anchor_up_count} levels above this file, that {owner_name} "
414
+ "already builds - define the base once and compose both paths from it"
415
+ )
416
+ if len(issues) >= MAX_CONFIG_DUPLICATE_PATH_ANCHOR_ISSUES:
417
+ break
418
+ return issues