claude-dev-env 1.59.0 → 1.61.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 (81) hide show
  1. package/CLAUDE.md +4 -0
  2. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
  3. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  4. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +1 -1
  5. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  6. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  7. package/audit-rubrics/prompts/category-e-dead-code.md +17 -4
  8. package/audit-rubrics/prompts/category-f-silent-failures.md +1 -0
  9. package/docs/CODE_RULES.md +2 -2
  10. package/hooks/blocking/code_rules_annotations_length.py +189 -10
  11. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  12. package/hooks/blocking/code_rules_duplicate_body.py +152 -0
  13. package/hooks/blocking/code_rules_enforcer.py +38 -15
  14. package/hooks/blocking/code_rules_orphan_css_class.py +196 -0
  15. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  16. package/hooks/blocking/config/__init__.py +5 -0
  17. package/hooks/blocking/config/verified_commit_constants.py +118 -0
  18. package/hooks/blocking/destructive_command_blocker.py +483 -61
  19. package/hooks/blocking/test_code_rules_enforcer_annotations.py +240 -0
  20. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  21. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  22. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  23. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +82 -0
  24. package/hooks/blocking/test_code_rules_enforcer_orphan_css_class.py +196 -0
  25. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  26. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +213 -0
  28. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  29. package/hooks/blocking/test_verification_verdict_store.py +490 -0
  30. package/hooks/blocking/test_verified_commit_gate.py +495 -0
  31. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  32. package/hooks/blocking/test_verifier_verdict_minter.py +193 -0
  33. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  34. package/hooks/blocking/verification_verdict_store.py +686 -0
  35. package/hooks/blocking/verified_commit_gate.py +535 -0
  36. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  37. package/hooks/blocking/verifier_verdict_minter.py +221 -0
  38. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  39. package/hooks/hooks.json +43 -1
  40. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  41. package/hooks/hooks_constants/code_rules_enforcer_constants.py +6 -0
  42. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  43. package/hooks/hooks_constants/destructive_command_segment_constants.py +15 -0
  44. package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
  45. package/hooks/hooks_constants/orphan_css_class_constants.py +40 -0
  46. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  47. package/hooks/validation/mypy_validator.py +59 -7
  48. package/hooks/validation/test_mypy_validator.py +94 -0
  49. package/package.json +1 -1
  50. package/rules/file-global-constants.md +7 -1
  51. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  52. package/rules/orphan-css-class.md +23 -0
  53. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  54. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  55. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  56. package/skills/autoconverge/SKILL.md +54 -17
  57. package/skills/autoconverge/reference/closing-report.md +59 -17
  58. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  59. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +192 -76
  60. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +395 -206
  62. package/skills/autoconverge/workflow/converge.mjs +520 -57
  63. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  64. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  65. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  66. package/skills/autoconverge/workflow/render_report.py +488 -397
  67. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  68. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  69. package/skills/autoconverge/workflow/test_render_report.py +518 -259
  70. package/skills/pr-converge/reference/per-tick.md +28 -8
  71. package/skills/rebase/SKILL.md +2 -4
  72. package/system-prompts/software-engineer.xml +2 -6
  73. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  74. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  75. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  76. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  77. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  78. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  79. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  80. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  81. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -0,0 +1,321 @@
1
+ """Dead module-level constant check for dedicated constants modules.
2
+
3
+ A constants module (`*_constants.py`, or any module under a ``config/``
4
+ directory) exists to export named values to importer modules elsewhere in the
5
+ project, so a constant defined there is never proven dead by a single-file scan
6
+ alone. This check resolves the enclosing package tree — the scan root — and
7
+ flags an UPPER_SNAKE constant defined in the written module whose name appears
8
+ in no ``.py`` module anywhere under that root: not as an imported name, not as a
9
+ read, not as a re-export. That is the ``MEDIUM_TEXT``-style dead constant the
10
+ CODE_RULES §9.8 dead-code rule targets, caught at Write/Edit time before the
11
+ unused constant lands.
12
+
13
+ The scan is deliberately conservative to keep false positives near zero:
14
+
15
+ - Only dedicated constants modules participate; ordinary production modules,
16
+ whose file-global constants are governed by the use-count rule, are skipped.
17
+ - A module declaring ``__all__`` is skipped: the author has named its export
18
+ surface explicitly, so a name listed there is live by declaration and a name
19
+ absent there is the author's stated intent, neither of which this check second
20
+ guesses.
21
+ - A constant is live when its name appears anywhere under the scan root —
22
+ imported, read, listed in ``__all__``, or referenced in a string annotation —
23
+ in any ``.py`` module, including the constants module itself.
24
+ - Test modules under the scan root still count as references, so a constant used
25
+ only by a test stays live.
26
+ """
27
+
28
+ import ast
29
+ import os
30
+ import sys
31
+ from pathlib import Path
32
+
33
+ _blocking_directory = str(Path(__file__).resolve().parent)
34
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
35
+ if _blocking_directory not in sys.path:
36
+ sys.path.insert(0, _blocking_directory)
37
+ if _hooks_directory not in sys.path:
38
+ sys.path.insert(0, _hooks_directory)
39
+
40
+ from code_rules_shared import ( # noqa: E402
41
+ is_migration_file,
42
+ is_test_file,
43
+ )
44
+
45
+ from hooks_constants.dead_module_constant_constants import ( # noqa: E402
46
+ CONFIG_DIRECTORY_SEGMENT,
47
+ CONSTANTS_MODULE_SUFFIX,
48
+ DEAD_MODULE_CONSTANT_GUIDANCE,
49
+ DUNDER_ALL_NAME,
50
+ DUNDER_INIT_FILENAME,
51
+ MAX_DEAD_MODULE_CONSTANT_ISSUES,
52
+ MAX_SCAN_ROOT_FILE_COUNT,
53
+ MINIMUM_UPPER_SNAKE_LENGTH,
54
+ PYTHON_SOURCE_SUFFIX,
55
+ )
56
+
57
+
58
+ def _is_dedicated_constants_module(file_path: str) -> bool:
59
+ """Return whether a path is a dedicated constants module.
60
+
61
+ A dedicated constants module is one whose filename ends in
62
+ ``_constants.py`` or whose path includes a ``config`` directory segment.
63
+ These modules export named values to importers, so their constants need a
64
+ cross-module scan to judge liveness.
65
+
66
+ Args:
67
+ file_path: The destination path of the write.
68
+
69
+ Returns:
70
+ True for a constants-suffixed module or a module under ``config/``.
71
+ """
72
+ normalized_path = file_path.replace("\\", "/").lower()
73
+ if normalized_path.endswith(CONSTANTS_MODULE_SUFFIX):
74
+ return True
75
+ path_segments = normalized_path.split("/")
76
+ return CONFIG_DIRECTORY_SEGMENT in path_segments[:-1]
77
+
78
+
79
+ def _is_upper_snake_name(name: str) -> bool:
80
+ """Return whether a name is an UPPER_SNAKE_CASE constant identifier."""
81
+ if len(name) < MINIMUM_UPPER_SNAKE_LENGTH:
82
+ return False
83
+ if not name.replace("_", "").isalnum():
84
+ return False
85
+ return name == name.upper() and any(each_char.isalpha() for each_char in name)
86
+
87
+
88
+ def _module_constant_definitions(tree: ast.Module) -> list[tuple[str, int]]:
89
+ """Return (name, line) for each module-scope UPPER_SNAKE constant assignment.
90
+
91
+ Both plain assignments (``NAME = value``) and annotated assignments
92
+ (``NAME: type = value``) at module scope are collected. A name bound more
93
+ than once keeps the line of its first binding.
94
+
95
+ Args:
96
+ tree: The parsed constants module.
97
+
98
+ Returns:
99
+ One (name, line) pair per distinct module-scope constant, in source
100
+ order.
101
+ """
102
+ line_by_name: dict[str, int] = {}
103
+ for each_statement in tree.body:
104
+ targets: list[ast.expr] = []
105
+ if isinstance(each_statement, ast.Assign):
106
+ targets = list(each_statement.targets)
107
+ elif isinstance(each_statement, ast.AnnAssign) and each_statement.value is not None:
108
+ targets = [each_statement.target]
109
+ for each_target in targets:
110
+ if not isinstance(each_target, ast.Name):
111
+ continue
112
+ if not _is_upper_snake_name(each_target.id):
113
+ continue
114
+ if each_target.id not in line_by_name:
115
+ line_by_name[each_target.id] = each_statement.lineno
116
+ return list(line_by_name.items())
117
+
118
+
119
+ def _statement_binds_dunder_all(statement: ast.stmt) -> bool:
120
+ """Return whether a single statement assigns or annotates ``__all__``."""
121
+ if isinstance(statement, ast.Assign):
122
+ return any(
123
+ isinstance(each_target, ast.Name) and each_target.id == DUNDER_ALL_NAME
124
+ for each_target in statement.targets
125
+ )
126
+ return (
127
+ isinstance(statement, ast.AnnAssign)
128
+ and isinstance(statement.target, ast.Name)
129
+ and statement.target.id == DUNDER_ALL_NAME
130
+ )
131
+
132
+
133
+ def _module_declares_dunder_all(tree: ast.Module) -> bool:
134
+ """Return whether the module body assigns or annotates ``__all__``."""
135
+ return any(_statement_binds_dunder_all(each_node) for each_node in tree.body)
136
+
137
+
138
+ def _referenced_names_in_source(source: str, load_only: bool = False) -> set[str]:
139
+ """Return every name a module references — imported, read, or re-exported.
140
+
141
+ Collects imported binding names, ``from`` import member names, name
142
+ references, attribute roots, and string literals (so a name listed in an
143
+ ``__all__`` literal or named in a string annotation counts as a reference).
144
+ A module that fails to parse contributes no names. With ``load_only`` set,
145
+ only ``Load``-context names count, so a constant's own assignment target in
146
+ the module being judged does not count as a reference to itself.
147
+
148
+ Args:
149
+ source: The full text of a ``.py`` module under the scan root.
150
+ load_only: When True, count only ``Load``-context name references,
151
+ excluding ``Store``/``Del`` targets. Used for the written constants
152
+ module so a definition is not mistaken for its own consumer.
153
+
154
+ Returns:
155
+ The set of names the module references.
156
+ """
157
+ try:
158
+ tree = ast.parse(source)
159
+ except SyntaxError:
160
+ return set()
161
+ referenced_names: set[str] = set()
162
+ for each_node in ast.walk(tree):
163
+ if isinstance(each_node, ast.Name):
164
+ if load_only and not isinstance(each_node.ctx, ast.Load):
165
+ continue
166
+ referenced_names.add(each_node.id)
167
+ elif isinstance(each_node, ast.Import | ast.ImportFrom):
168
+ for each_alias in each_node.names:
169
+ referenced_names.add(each_alias.asname or each_alias.name)
170
+ referenced_names.add(each_alias.name)
171
+ elif isinstance(each_node, ast.Constant) and isinstance(each_node.value, str):
172
+ referenced_names.add(each_node.value)
173
+ return referenced_names
174
+
175
+
176
+ def _scan_root_for_constants_module(file_path: str) -> Path:
177
+ """Return the directory tree to scan for references to the module's constants.
178
+
179
+ For a constants module inside a package subdirectory
180
+ (``pkg/foo_constants.py``), the scan root is the package's parent, so an
181
+ importer one directory up (``pkg/../consumer.py``) is in scope. For a
182
+ constants module at the top of a directory, the scan root is that directory.
183
+ A ``config/`` module's scan root is the parent of the ``config`` directory.
184
+
185
+ Args:
186
+ file_path: The destination path of the write.
187
+
188
+ Returns:
189
+ The absolute directory to scan recursively for references.
190
+ """
191
+ written_path = Path(file_path).resolve()
192
+ enclosing_directory = written_path.parent
193
+ if enclosing_directory.name.lower() == CONFIG_DIRECTORY_SEGMENT:
194
+ return enclosing_directory.parent
195
+ if (enclosing_directory / DUNDER_INIT_FILENAME).is_file():
196
+ return enclosing_directory.parent
197
+ return enclosing_directory
198
+
199
+
200
+ def _all_referenced_names_under_root(
201
+ scan_root: Path,
202
+ written_path: Path,
203
+ written_content: str,
204
+ ) -> tuple[set[str], bool]:
205
+ """Return referenced names under the scan root and whether the file cap was hit.
206
+
207
+ The written module's on-disk text is replaced by ``written_content`` so the
208
+ post-edit view is judged, never the stale disk copy. Sibling modules are
209
+ read from disk. Reading stops after the configured file cap so a write under
210
+ an unexpectedly large tree cannot stall the hook; the boolean signals the
211
+ caller to treat that case as "cannot prove dead".
212
+
213
+ Args:
214
+ scan_root: The directory tree to scan.
215
+ written_path: The resolved path of the module being written.
216
+ written_content: The post-edit text of the written module.
217
+
218
+ Returns:
219
+ A (referenced_names, cap_was_hit) pair. The name set is the union across
220
+ every scanned module; cap_was_hit is True when the scan stopped at the
221
+ configured file cap before scanning the whole tree.
222
+ """
223
+ all_referenced_names = _referenced_names_in_source(written_content, load_only=True)
224
+ written_path_key = os.path.normcase(str(written_path))
225
+ scanned_file_count = 1
226
+ for each_path in scan_root.rglob("*" + PYTHON_SOURCE_SUFFIX):
227
+ if not each_path.is_file():
228
+ continue
229
+ if os.path.normcase(str(each_path.resolve())) == written_path_key:
230
+ continue
231
+ scanned_file_count += 1
232
+ if scanned_file_count > MAX_SCAN_ROOT_FILE_COUNT:
233
+ return all_referenced_names, True
234
+ try:
235
+ sibling_source = each_path.read_text(encoding="utf-8")
236
+ except (OSError, UnicodeDecodeError):
237
+ continue
238
+ all_referenced_names |= _referenced_names_in_source(sibling_source)
239
+ return all_referenced_names, False
240
+
241
+
242
+ def _module_is_exempt_from_constant_check(file_path: str) -> bool:
243
+ """Return whether a path is exempt from the dead module-constant check.
244
+
245
+ Test modules and migration modules are exempt, and any module that is not a
246
+ dedicated constants module is out of scope because its file-global constants
247
+ are governed by the use-count rule instead.
248
+
249
+ Args:
250
+ file_path: The destination path of the write.
251
+
252
+ Returns:
253
+ True when the dead module-constant check must not run on this path.
254
+ """
255
+ if is_test_file(file_path):
256
+ return True
257
+ if is_migration_file(file_path):
258
+ return True
259
+ return not _is_dedicated_constants_module(file_path)
260
+
261
+
262
+ def check_dead_module_constants(
263
+ content: str,
264
+ file_path: str,
265
+ full_file_content: str | None = None,
266
+ ) -> list[str]:
267
+ """Flag an UPPER_SNAKE constant in a constants module read by no module.
268
+
269
+ Runs only on a dedicated constants module (``*_constants.py`` or a module
270
+ under ``config/``); every other production module's file-global constants
271
+ are governed by the use-count rule instead. A constant is dead when its name
272
+ appears in no ``.py`` module anywhere under the enclosing package tree — not
273
+ imported, not read, not listed in an ``__all__`` literal, not named in a
274
+ string annotation. A module declaring its own ``__all__`` is skipped so the
275
+ author's explicit export surface is never second-guessed. Whole-file
276
+ analysis runs against ``full_file_content`` when supplied so an Edit fragment
277
+ is judged against the reconstructed post-edit file.
278
+
279
+ Args:
280
+ content: The new content under validation (Edit fragment or whole file).
281
+ file_path: The destination path, used for the constants-module gate and
282
+ the test/registry exemptions.
283
+ full_file_content: The reconstructed post-edit whole-file content for an
284
+ Edit, or None for a Write where ``content`` is already the whole file.
285
+
286
+ Returns:
287
+ One violation message per dead module-level constant, capped at the
288
+ configured maximum.
289
+ """
290
+ if _module_is_exempt_from_constant_check(file_path):
291
+ return []
292
+ effective_content = content if full_file_content is None else full_file_content
293
+ try:
294
+ tree = ast.parse(effective_content)
295
+ except SyntaxError:
296
+ return []
297
+ if _module_declares_dunder_all(tree):
298
+ return []
299
+ constant_definitions = _module_constant_definitions(tree)
300
+ if not constant_definitions:
301
+ return []
302
+ scan_root = _scan_root_for_constants_module(file_path)
303
+ written_path = Path(file_path).resolve()
304
+ all_referenced_names, cap_was_hit = _all_referenced_names_under_root(
305
+ scan_root,
306
+ written_path,
307
+ effective_content,
308
+ )
309
+ if cap_was_hit:
310
+ return []
311
+ issues: list[str] = []
312
+ for each_name, each_line in constant_definitions:
313
+ if each_name in all_referenced_names:
314
+ continue
315
+ issues.append(
316
+ f"Line {each_line}: module-level constant {each_name!r}"
317
+ f" - {DEAD_MODULE_CONSTANT_GUIDANCE}"
318
+ )
319
+ if len(issues) >= MAX_DEAD_MODULE_CONSTANT_ISSUES:
320
+ break
321
+ return issues
@@ -28,6 +28,15 @@ leave the exact violation class unguarded. The enforcer entry points route a
28
28
  hook ``.py`` target to this single check even though the full code-rules verdict
29
29
  stays off hook infrastructure, so a Write or pre-check against a file under the
30
30
  ``blocking/`` directory still blocks a copied sibling helper.
31
+
32
+ ``advise_cross_skill_duplicate_helper`` is the non-blocking companion for a
33
+ different layout: a helper copied between two skills' ``scripts`` directories.
34
+ Two skill folders install on their own, so a shared module would break
35
+ independent install and a same-directory block would be a false positive on a
36
+ sanctioned skill-isolation copy. The advisory prints a ``[CODE_RULES advisory]``
37
+ line to stderr naming the source skill and function so a reviewer confirms the
38
+ copy is intentional, and never enters the deny path. It fires only across skill
39
+ folders; within one skill the blocking check above already covers the copy.
31
40
  """
32
41
 
33
42
  import ast
@@ -47,11 +56,16 @@ from code_rules_shared import ( # noqa: E402
47
56
  )
48
57
 
49
58
  from hooks_constants.duplicate_function_body_constants import ( # noqa: E402
59
+ CROSS_SKILL_ADVISORY_PREFIX,
60
+ CROSS_SKILL_DUPLICATE_GUIDANCE,
50
61
  DUNDER_INIT_FILENAME,
51
62
  DUPLICATE_BODY_GUIDANCE,
63
+ MAX_CROSS_SKILL_ADVISORY_ISSUES,
52
64
  MAX_DUPLICATE_BODY_ISSUES,
53
65
  MINIMUM_DUPLICATE_BODY_STATEMENTS,
54
66
  PYTHON_SOURCE_SUFFIX,
67
+ SKILL_SCRIPTS_DIRECTORY_NAME,
68
+ SKILLS_DIRECTORY_NAME,
55
69
  )
56
70
 
57
71
 
@@ -285,3 +299,141 @@ def check_duplicate_function_body_across_files(
285
299
  all_changed_lines,
286
300
  defer_scope_to_caller,
287
301
  )
302
+
303
+
304
+ def _skill_scripts_root(file_path: str) -> Path | None:
305
+ """Return the ``skills/<name>/scripts`` root the written file sits under.
306
+
307
+ A skill's helper scripts live at ``<...>/skills/<skill-name>/scripts/<file>``.
308
+ This walks the written file's parents for a ``scripts`` directory whose own
309
+ parent's parent is named ``skills``, and returns that ``scripts`` directory.
310
+
311
+ Args:
312
+ file_path: The destination path of the write.
313
+
314
+ Returns:
315
+ The ``skills/<name>/scripts`` directory containing the file, or None when
316
+ the file is not under a skill's ``scripts`` directory.
317
+ """
318
+ written_path = Path(file_path).resolve()
319
+ for each_ancestor in written_path.parents:
320
+ if each_ancestor.name != SKILL_SCRIPTS_DIRECTORY_NAME:
321
+ continue
322
+ skill_directory = each_ancestor.parent
323
+ if skill_directory.parent.name == SKILLS_DIRECTORY_NAME:
324
+ return each_ancestor
325
+ return None
326
+
327
+
328
+ def _other_skill_scripts_directories(scripts_root: Path) -> list[Path]:
329
+ """List the ``scripts`` directories of every sibling skill folder.
330
+
331
+ Args:
332
+ scripts_root: The ``skills/<name>/scripts`` directory of the written file.
333
+
334
+ Returns:
335
+ The ``scripts`` directory of each sibling skill that has one, excluding
336
+ the written file's own skill.
337
+ """
338
+ own_skill_directory = scripts_root.parent
339
+ skills_directory = own_skill_directory.parent
340
+ all_other_scripts_directories: list[Path] = []
341
+ try:
342
+ all_skill_entries = sorted(skills_directory.iterdir())
343
+ except OSError:
344
+ return []
345
+ for each_skill_directory in all_skill_entries:
346
+ if not each_skill_directory.is_dir():
347
+ continue
348
+ if each_skill_directory == own_skill_directory:
349
+ continue
350
+ candidate_scripts = each_skill_directory / SKILL_SCRIPTS_DIRECTORY_NAME
351
+ if candidate_scripts.is_dir():
352
+ all_other_scripts_directories.append(candidate_scripts)
353
+ return all_other_scripts_directories
354
+
355
+
356
+ def _cross_skill_source_signatures(
357
+ all_other_scripts_directories: list[Path],
358
+ ) -> dict[str, list[str]]:
359
+ """Map each function body signature to the ``skill/module::function`` copies.
360
+
361
+ Args:
362
+ all_other_scripts_directories: The ``scripts`` directory of each sibling skill.
363
+
364
+ Returns:
365
+ A signature-to-source-names mapping naming the skill, module, and function
366
+ that carry each comparable top-level body.
367
+ """
368
+ source_names_by_signature: dict[str, list[str]] = {}
369
+ for each_scripts_directory in all_other_scripts_directories:
370
+ skill_name = each_scripts_directory.parent.name
371
+ try:
372
+ all_entries = sorted(each_scripts_directory.iterdir())
373
+ except OSError:
374
+ continue
375
+ for each_entry in all_entries:
376
+ if not _is_comparable_sibling(each_entry, ""):
377
+ continue
378
+ try:
379
+ sibling_source = each_entry.read_text(encoding="utf-8")
380
+ sibling_tree = ast.parse(sibling_source)
381
+ except (OSError, UnicodeDecodeError, SyntaxError):
382
+ continue
383
+ for each_name, each_signature in _top_level_function_signatures(sibling_tree).items():
384
+ location = f"{skill_name}/{each_entry.name}::{each_name}"
385
+ source_names_by_signature.setdefault(each_signature, []).append(location)
386
+ return source_names_by_signature
387
+
388
+
389
+ def advise_cross_skill_duplicate_helper(content: str, file_path: str) -> None:
390
+ """Emit non-blocking stderr advisories for helpers copied across skill folders.
391
+
392
+ A top-level function in the file being written whose normalized body matches a
393
+ top-level function in another skill's ``scripts`` directory is surfaced as a
394
+ ``[CODE_RULES advisory]`` line on stderr — never a block. Two skill folders
395
+ install on their own, so a shared module would break independent install; the
396
+ copy is a defensible skill-isolation tradeoff the writer confirms rather than
397
+ a violation the gate denies. Within one skill the blocking duplicate-body gate
398
+ already covers the copy, so this advisory fires only across skill folders.
399
+
400
+ Test files and ``__init__.py`` are skipped on both the writing side and the
401
+ sibling side, mirroring the blocking gate.
402
+
403
+ Args:
404
+ content: The full post-edit file content being written.
405
+ file_path: The destination path of the write.
406
+ """
407
+ written_name = Path(file_path).name
408
+ if written_name == DUNDER_INIT_FILENAME:
409
+ return
410
+ if is_test_file(file_path):
411
+ return
412
+ scripts_root = _skill_scripts_root(file_path)
413
+ if scripts_root is None:
414
+ return
415
+ try:
416
+ written_tree = ast.parse(content)
417
+ except SyntaxError:
418
+ return
419
+ written_signatures = _top_level_function_signatures(written_tree)
420
+ if not written_signatures:
421
+ return
422
+ all_other_scripts_directories = _other_skill_scripts_directories(scripts_root)
423
+ if not all_other_scripts_directories:
424
+ return
425
+ source_names_by_signature = _cross_skill_source_signatures(all_other_scripts_directories)
426
+ advisory_count = 0
427
+ for each_name, each_signature in written_signatures.items():
428
+ matching_locations = source_names_by_signature.get(each_signature)
429
+ if not matching_locations:
430
+ continue
431
+ print(
432
+ f"{CROSS_SKILL_ADVISORY_PREFIX} {file_path}: function {each_name!r} "
433
+ f"duplicates {matching_locations[0]} in another skill — "
434
+ f"{CROSS_SKILL_DUPLICATE_GUIDANCE}",
435
+ file=sys.stderr,
436
+ )
437
+ advisory_count += 1
438
+ if advisory_count >= MAX_CROSS_SKILL_ADVISORY_ISSUES:
439
+ break
@@ -33,6 +33,7 @@ from code_rules_annotations_length import ( # noqa: E402
33
33
  check_known_pytest_fixture_annotations,
34
34
  check_parameter_annotations,
35
35
  check_return_annotations,
36
+ check_unused_known_pytest_fixture_parameters,
36
37
  )
37
38
  from code_rules_banned_identifiers import ( # noqa: E402
38
39
  check_banned_identifiers,
@@ -54,11 +55,15 @@ from code_rules_constants_config import ( # noqa: E402
54
55
  from code_rules_dead_dataclass_field import ( # noqa: E402
55
56
  check_dead_dataclass_fields,
56
57
  )
58
+ from code_rules_dead_module_constant import ( # noqa: E402
59
+ check_dead_module_constants,
60
+ )
57
61
  from code_rules_docstrings import ( # noqa: E402
58
62
  check_docstring_args_match_signature,
59
63
  check_docstring_format,
60
64
  )
61
65
  from code_rules_duplicate_body import ( # noqa: E402
66
+ advise_cross_skill_duplicate_helper,
62
67
  check_duplicate_function_body_across_files,
63
68
  )
64
69
  from code_rules_imports_logging import ( # noqa: E402
@@ -85,6 +90,9 @@ from code_rules_optional_params import ( # noqa: E402
85
90
  check_duplicated_format_patterns,
86
91
  check_unused_optional_parameters,
87
92
  )
93
+ from code_rules_orphan_css_class import ( # noqa: E402
94
+ check_orphan_css_classes,
95
+ )
88
96
  from code_rules_paths_syspath import ( # noqa: E402
89
97
  check_hardcoded_user_paths,
90
98
  check_sys_path_insert_deduplication_guard,
@@ -120,6 +128,7 @@ from code_rules_typeddict_stub import ( # noqa: E402
120
128
  check_stub_implementations,
121
129
  check_thin_wrapper_files,
122
130
  check_typed_dict_encode_decode,
131
+ check_zero_payload_function_alias,
123
132
  )
124
133
  from code_rules_unused_imports import ( # noqa: E402
125
134
  check_unused_module_level_imports,
@@ -228,6 +237,7 @@ def validate_content(
228
237
  all_issues.extend(check_test_branching_in_production(effective_content, file_path))
229
238
  all_issues.extend(check_bare_except(effective_content, file_path))
230
239
  all_issues.extend(check_thin_wrapper_files(effective_content, file_path))
240
+ all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
231
241
  all_issues.extend(check_boundary_types(effective_content, file_path))
232
242
  all_issues.extend(check_docstring_format(effective_content, file_path))
233
243
  all_issues.extend(check_docstring_args_match_signature(effective_content, file_path))
@@ -269,9 +279,15 @@ def validate_content(
269
279
  all_issues.extend(
270
280
  check_dead_dataclass_fields(content, file_path, full_file_content)
271
281
  )
282
+ all_issues.extend(
283
+ check_dead_module_constants(content, file_path, full_file_content)
284
+ )
272
285
  all_issues.extend(check_library_print(content, file_path))
273
286
  all_issues.extend(check_parameter_annotations(content, file_path))
274
287
  all_issues.extend(check_known_pytest_fixture_annotations(content, file_path))
288
+ all_issues.extend(
289
+ check_unused_known_pytest_fixture_parameters(content, file_path)
290
+ )
275
291
  all_issues.extend(check_return_annotations(content, file_path))
276
292
  all_issues.extend(
277
293
  check_function_length(
@@ -285,8 +301,10 @@ def validate_content(
285
301
  all_issues.extend(check_inline_literal_collections(content, file_path))
286
302
  all_issues.extend(check_inline_tuple_string_magic(content, file_path))
287
303
  all_issues.extend(check_string_literal_magic(content, file_path))
304
+ all_issues.extend(check_orphan_css_classes(effective_content, file_path))
288
305
  check_incomplete_mocks(content, file_path)
289
306
  check_duplicated_format_patterns(content, file_path)
307
+ advise_cross_skill_duplicate_helper(effective_content, file_path)
290
308
 
291
309
  elif extension in ALL_JAVASCRIPT_EXTENSIONS:
292
310
  if not is_test_file(file_path):
@@ -383,18 +401,20 @@ def _is_hook_infrastructure_python_target(file_path: str) -> bool:
383
401
  return get_file_extension(file_path) in ALL_PYTHON_EXTENSIONS
384
402
 
385
403
 
386
- def _hook_infrastructure_duplicate_body_issues(
404
+ def _hook_infrastructure_blocking_issues(
387
405
  content: str,
388
406
  file_path: str,
389
407
  full_file_content: str | None = None,
390
408
  prior_full_file_content: str = "",
391
409
  ) -> list[str]:
392
- """Run only the cross-file duplicate-body check for a hook Python target.
410
+ """Run the checks that still guard a hook Python target.
393
411
 
394
412
  The whole code-rules verdict stays off hook-infrastructure files, so this
395
- runs the single check that must still guard them, span-scoped to the lines
396
- an edit touched exactly as ``validate_content`` scopes it for production
397
- code.
413
+ runs the two checks that must still guard them: the cross-file duplicate-body
414
+ check, span-scoped to the lines an edit touched exactly as ``validate_content``
415
+ scopes it for production code; and the zero-payload alias check, whose
416
+ docstring names hook modules as its motivating case, run over the whole
417
+ post-edit file.
398
418
 
399
419
  Args:
400
420
  content: The fragment or whole-file body under validation.
@@ -405,7 +425,8 @@ def _hook_infrastructure_duplicate_body_issues(
405
425
  recover the changed lines on an Edit.
406
426
 
407
427
  Returns:
408
- The in-scope duplicate-body violations for the target.
428
+ The in-scope duplicate-body violations and the zero-payload alias
429
+ violations for the target.
409
430
  """
410
431
  effective_content = content if full_file_content is None else full_file_content
411
432
  all_changed_lines = (
@@ -413,11 +434,13 @@ def _hook_infrastructure_duplicate_body_issues(
413
434
  if full_file_content is not None
414
435
  else None
415
436
  )
416
- return check_duplicate_function_body_across_files(
437
+ all_issues = check_duplicate_function_body_across_files(
417
438
  effective_content,
418
439
  file_path,
419
440
  all_changed_lines,
420
441
  )
442
+ all_issues.extend(check_zero_payload_function_alias(effective_content, file_path))
443
+ return all_issues
421
444
 
422
445
 
423
446
  def _without_line_prefix(violation_text: str) -> str:
@@ -540,7 +563,7 @@ def _run_precheck(
540
563
  old_content = _read_existing_file_content(target_path) or ""
541
564
  all_issues = validate_content(candidate_content, target_path, old_content)
542
565
  else:
543
- all_issues = _hook_infrastructure_duplicate_body_issues(
566
+ all_issues = _hook_infrastructure_blocking_issues(
544
567
  candidate_content, target_path
545
568
  )
546
569
  for each_issue in all_issues:
@@ -755,18 +778,18 @@ def _report_blocking_violations(
755
778
  )
756
779
 
757
780
 
758
- def _report_hook_duplicate_body(
781
+ def _report_hook_blocking_issues(
759
782
  content: str,
760
783
  file_path: str,
761
784
  full_file_content_after_edit: str | None,
762
785
  prior_full_file_content: str,
763
786
  deny_stream: TextIO,
764
787
  ) -> None:
765
- """Write a deny payload when a hook target copies a sibling function body.
788
+ """Write a deny payload when a hook target trips a check that still guards it.
766
789
 
767
- The full code-rules verdict stays off hook-infrastructure files; this runs
768
- the single duplicate-body check that must still guard them and emits the deny
769
- payload when it fires.
790
+ The full code-rules verdict stays off hook-infrastructure files; this runs the
791
+ two checks that must still guard them the cross-file duplicate-body check and
792
+ the zero-payload alias check — and emits the deny payload when either fires.
770
793
 
771
794
  Args:
772
795
  content: The fragment or whole-file body under validation.
@@ -776,7 +799,7 @@ def _report_hook_duplicate_body(
776
799
  prior_full_file_content: The on-disk content before the edit.
777
800
  deny_stream: The stream the JSON deny payload is written to.
778
801
  """
779
- all_blocking_issues = _hook_infrastructure_duplicate_body_issues(
802
+ all_blocking_issues = _hook_infrastructure_blocking_issues(
780
803
  content,
781
804
  file_path,
782
805
  full_file_content_after_edit,
@@ -835,7 +858,7 @@ def main(all_arguments: list[str]) -> None:
835
858
  sys.exit(0)
836
859
 
837
860
  if not runs_full_verdict:
838
- _report_hook_duplicate_body(
861
+ _report_hook_blocking_issues(
839
862
  content,
840
863
  file_path,
841
864
  full_file_content_after_edit,