claude-dev-env 2.13.0 → 2.14.1

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 (263) hide show
  1. package/AGENTS.md +135 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  13. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  15. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  16. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  17. package/_shared/process-tree/AGENTS.md +41 -0
  18. package/_shared/process-tree/CLAUDE.md +1 -41
  19. package/agents/AGENTS.md +26 -0
  20. package/agents/CLAUDE.md +1 -31
  21. package/agents/clean-coder.md +6 -6
  22. package/agents/test_agent_frontmatter.py +11 -11
  23. package/audit-rubrics/AGENTS.md +43 -0
  24. package/audit-rubrics/CLAUDE.md +1 -43
  25. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  26. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  27. package/audit-rubrics/prompts/AGENTS.md +37 -0
  28. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  29. package/bin/AGENTS.md +112 -0
  30. package/bin/CLAUDE.md +1 -112
  31. package/bin/ever-shipped-skills.mjs +1 -0
  32. package/bin/install-constants.mjs +6 -7
  33. package/bin/install.mjs +24 -5
  34. package/bin/install.test.mjs +34 -0
  35. package/commands/AGENTS.md +13 -0
  36. package/commands/CLAUDE.md +1 -23
  37. package/docs/AGENTS.md +32 -0
  38. package/docs/CLAUDE.md +1 -32
  39. package/docs/CODE_RULES.md +1 -1
  40. package/docs/imagegen.md +22 -0
  41. package/docs/references/AGENTS.md +17 -0
  42. package/docs/references/CLAUDE.md +1 -17
  43. package/hooks/AGENTS.md +31 -0
  44. package/hooks/CLAUDE.md +1 -31
  45. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  46. package/hooks/advisory/AGENTS.md +16 -0
  47. package/hooks/advisory/CLAUDE.md +1 -16
  48. package/hooks/blocking/AGENTS.md +123 -0
  49. package/hooks/blocking/CLAUDE.md +1 -123
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  52. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  53. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  54. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  55. package/hooks/blocking/config/AGENTS.md +10 -0
  56. package/hooks/blocking/config/CLAUDE.md +1 -10
  57. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  58. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  59. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  60. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  61. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  62. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  63. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  64. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  65. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  66. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  67. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  69. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  70. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  71. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  72. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  73. package/hooks/diagnostic/AGENTS.md +75 -0
  74. package/hooks/diagnostic/CLAUDE.md +1 -43
  75. package/hooks/diagnostic/hook_log_extractor.py +2 -2
  76. package/hooks/diagnostic/hook_log_init.py +2 -2
  77. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  78. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  79. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  80. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  81. package/hooks/diagnostic/test_hook_log_extractor.py +2 -2
  82. package/hooks/git-hooks/AGENTS.md +31 -0
  83. package/hooks/git-hooks/CLAUDE.md +1 -31
  84. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  85. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  86. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -6
  87. package/hooks/git-hooks/pre_push.py +47 -34
  88. package/hooks/git-hooks/test_pre_push.py +111 -7
  89. package/hooks/hooks_constants/AGENTS.md +104 -0
  90. package/hooks/hooks_constants/CLAUDE.md +1 -104
  91. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  92. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  93. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  94. package/hooks/lifecycle/AGENTS.md +18 -0
  95. package/hooks/lifecycle/CLAUDE.md +1 -18
  96. package/hooks/observability/AGENTS.md +20 -0
  97. package/hooks/observability/CLAUDE.md +1 -20
  98. package/hooks/session/AGENTS.md +33 -0
  99. package/hooks/session/CLAUDE.md +1 -33
  100. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  101. package/hooks/session/task_list_loop_starter.py +3 -3
  102. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  103. package/hooks/session/test_task_list_loop_starter.py +10 -2
  104. package/hooks/session/test_working_style_prompt.py +9 -4
  105. package/hooks/validation/AGENTS.md +20 -0
  106. package/hooks/validation/CLAUDE.md +1 -20
  107. package/hooks/validators/AGENTS.md +51 -0
  108. package/hooks/validators/CLAUDE.md +1 -51
  109. package/hooks/validators/health_check.py +52 -19
  110. package/hooks/validators/python_style_checks.py +1 -1
  111. package/hooks/validators/test_health_check.py +112 -27
  112. package/hooks/workflow/AGENTS.md +16 -0
  113. package/hooks/workflow/CLAUDE.md +1 -16
  114. package/installable-surfaces.manifest.json +1 -0
  115. package/output-styles/AGENTS.md +15 -0
  116. package/output-styles/CLAUDE.md +1 -15
  117. package/package.json +2 -1
  118. package/rules/AGENTS.md +61 -0
  119. package/rules/CLAUDE.md +1 -61
  120. package/rules/code-standards.md +5 -9
  121. package/rules/doc-prose-cuts.md +1 -1
  122. package/scripts/AGENTS.md +62 -0
  123. package/scripts/CLAUDE.md +1 -62
  124. package/scripts/codex_compat_materializer.py +4 -0
  125. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  126. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  127. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  128. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  129. package/scripts/test_active_capability_references.py +2 -2
  130. package/scripts/test_validate_instruction_pairs.py +90 -0
  131. package/scripts/test_verify_installable_package.py +5 -1
  132. package/scripts/tests/AGENTS.md +33 -0
  133. package/scripts/tests/CLAUDE.md +1 -33
  134. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  135. package/scripts/validate_instruction_pairs.py +183 -0
  136. package/skills/AGENTS.md +72 -0
  137. package/skills/CLAUDE.md +1 -72
  138. package/skills/_shared/AGENTS.md +44 -0
  139. package/skills/_shared/CLAUDE.md +1 -44
  140. package/skills/_shared/advisor/AGENTS.md +9 -0
  141. package/skills/_shared/advisor/CLAUDE.md +1 -9
  142. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  143. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  144. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  145. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  146. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  147. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  148. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  149. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  150. package/skills/anthropic-plan/AGENTS.md +34 -0
  151. package/skills/anthropic-plan/CLAUDE.md +1 -34
  152. package/skills/anthropic-plan/SKILL.md +1 -1
  153. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  154. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  155. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  156. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  157. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  158. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  159. package/skills/anthropic-plan/test_skill_contract.py +8 -6
  160. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  161. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  162. package/skills/auditing-claude-config/AGENTS.md +21 -0
  163. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  164. package/skills/autoconverge/AGENTS.md +36 -0
  165. package/skills/autoconverge/CLAUDE.md +1 -36
  166. package/skills/autoconverge/reference/AGENTS.md +16 -0
  167. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  168. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  169. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  170. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  171. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  172. package/skills/bugteam/AGENTS.md +30 -0
  173. package/skills/bugteam/CLAUDE.md +1 -30
  174. package/skills/bugteam/reference/AGENTS.md +20 -0
  175. package/skills/bugteam/reference/CLAUDE.md +1 -20
  176. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  177. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  178. package/skills/bugteam/scripts/AGENTS.md +30 -0
  179. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  180. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  181. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  182. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  183. package/skills/codex-review/AGENTS.md +46 -0
  184. package/skills/codex-review/CLAUDE.md +1 -46
  185. package/skills/codex-review/reference/AGENTS.md +15 -0
  186. package/skills/codex-review/reference/CLAUDE.md +1 -15
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  189. package/skills/codex-review/test_skill_scaffold.py +3 -3
  190. package/skills/copilot-review/AGENTS.md +18 -0
  191. package/skills/copilot-review/CLAUDE.md +1 -18
  192. package/skills/everything-search/AGENTS.md +17 -0
  193. package/skills/everything-search/CLAUDE.md +1 -17
  194. package/skills/fresh-branch/AGENTS.md +14 -0
  195. package/skills/fresh-branch/CLAUDE.md +1 -14
  196. package/skills/grok-spawn/AGENTS.md +28 -0
  197. package/skills/grok-spawn/CLAUDE.md +1 -28
  198. package/skills/pr-converge/AGENTS.md +32 -0
  199. package/skills/pr-converge/CLAUDE.md +1 -32
  200. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  201. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  202. package/skills/pr-converge/reference/AGENTS.md +28 -0
  203. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  204. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  205. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  206. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  207. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  208. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  209. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  210. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  211. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  212. package/skills/prototype/SKILL.md +1 -2
  213. package/skills/prototype/reference/honest-limitations.md +1 -1
  214. package/skills/prototype/reference/promotion-tasks.md +1 -1
  215. package/skills/prototype/workflows/promotion.md +1 -1
  216. package/skills/rebase/AGENTS.md +32 -0
  217. package/skills/rebase/CLAUDE.md +1 -32
  218. package/skills/recall/AGENTS.md +30 -0
  219. package/skills/recall/CLAUDE.md +1 -30
  220. package/skills/remember/AGENTS.md +31 -0
  221. package/skills/remember/CLAUDE.md +1 -31
  222. package/skills/reviews/SKILL.md +1 -1
  223. package/skills/session-log/AGENTS.md +32 -0
  224. package/skills/session-log/CLAUDE.md +1 -32
  225. package/skills/session-tidy/AGENTS.md +36 -0
  226. package/skills/session-tidy/CLAUDE.md +1 -36
  227. package/skills/skill-builder/AGENTS.md +49 -0
  228. package/skills/skill-builder/CLAUDE.md +1 -49
  229. package/skills/skill-builder/references/AGENTS.md +25 -0
  230. package/skills/skill-builder/references/CLAUDE.md +1 -25
  231. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  232. package/skills/skill-builder/templates/AGENTS.md +14 -0
  233. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  234. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  235. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  236. package/skills/task-build/AGENTS.md +29 -0
  237. package/skills/task-build/CLAUDE.md +1 -29
  238. package/skills/update/AGENTS.md +38 -0
  239. package/skills/update/CLAUDE.md +1 -38
  240. package/system-prompts/AGENTS.md +17 -0
  241. package/system-prompts/CLAUDE.md +1 -17
  242. package/system-prompts/software-engineer.xml +3 -3
  243. package/agents/caveman.md +0 -73
  244. package/agents/clasp-deployment-orchestrator.md +0 -608
  245. package/agents/code-advisor.md +0 -23
  246. package/agents/deep-research.md +0 -152
  247. package/agents/docs-agent.md +0 -85
  248. package/commands/commit.md +0 -28
  249. package/commands/docupdate.md +0 -322
  250. package/commands/hook-log-extract.md +0 -70
  251. package/commands/hook-log-init.md +0 -76
  252. package/commands/implement.md +0 -102
  253. package/commands/plan.md +0 -14
  254. package/commands/pr-comments.md +0 -47
  255. package/commands/review-plan.md +0 -5
  256. package/commands/right-size.md +0 -15
  257. package/commands/sum.md +0 -30
  258. package/skills/imagegen/SKILL.md +0 -21
  259. package/skills/imagegen/scripts/config/__init__.py +0 -1
  260. package/skills/imagegen/scripts/config/constants.py +0 -28
  261. package/skills/imagegen/scripts/imagegen.py +0 -45
  262. package/skills/imagegen/scripts/imagegen_core.py +0 -528
  263. package/skills/imagegen/scripts/test_imagegen.py +0 -204
@@ -29,13 +29,20 @@ The scan is deliberately conservative to keep false positives near zero:
29
29
  exported constant live.
30
30
  - When the package-tree scan leaves a constant unreferenced, the scan widens to
31
31
  the repository root (the nearest ``.git`` ancestor). The widened pass counts a
32
- sibling-tree reference only when a module imports the name through a
33
- ``from <module> import`` whose final dotted segment equals the written
34
- module's filename stem, so a genuine cross-tree consumer of this constants
35
- module keeps the constant live while a same-named constant exported by an
36
- unrelated module never masks a dead one. The widened pass reads a repository
37
- file only to test whether its text names the written module's filename stem;
38
- a file that never mentions the stem cannot carry such an import, so it is
32
+ sibling-tree reference in two shapes. The first is an import of the name
33
+ through a ``from <module> import`` whose final dotted segment equals the
34
+ written module's filename stem. The second is an attribute read on a binding
35
+ of the written module itself: a consumer that writes
36
+ ``import pkg.config.constants``, the same with an alias,
37
+ ``from pkg.config import constants``, or the same with an alias, and then
38
+ reads each constant as an attribute on that binding (``alias.NAME``,
39
+ ``pkg.config.constants.NAME``), keeps every name it reads that way live. Both
40
+ shapes are bound to the written module's stem, so a genuine cross-tree
41
+ consumer of this constants module keeps the constant live while a same-named
42
+ constant exported by an unrelated module never masks a dead one. The widened
43
+ pass reads a repository file only to test whether its text names the written
44
+ module's filename stem; neither shape can appear in a file that never spells
45
+ the stem, so such a file is
39
46
  skipped without spending scan-cap budget, keeping the widened pass bounded to
40
47
  the handful of candidate importer files even in a large repository. A module
41
48
  outside any repository is
@@ -65,6 +72,7 @@ if _blocking_directory not in sys.path:
65
72
  if _hooks_directory not in sys.path:
66
73
  sys.path.insert(0, _hooks_directory)
67
74
 
75
+ from code_rules_probe_chains import _dotted_attribute_chain # noqa: E402
68
76
  from code_rules_shared import ( # noqa: E402
69
77
  is_migration_file,
70
78
  is_test_file,
@@ -268,13 +276,116 @@ def _module_final_segment(module_path: str | None) -> str:
268
276
  return module_path.rsplit(".", 1)[-1]
269
277
 
270
278
 
271
- def _qualified_import_member_names(source: str, module_stem: str) -> set[str]:
272
- """Return names imported from a module whose filename stem is ``module_stem``.
279
+ def _stem_import_member_names(import_node: ast.ImportFrom, module_stem: str) -> set[str]:
280
+ """Return the member names a stem-matched ``from ... import`` brings in.
273
281
 
274
- A cross-package consumer of an exported constant imports it through an
275
- explicit ``from <module> import NAME`` whose module path ends in the defining
276
- module's filename stem. Collecting only those member names binds a
277
- widened-scan reference to the module that actually defines the constant, so a
282
+ ::
283
+
284
+ from pkg.config.constants import NAME -> "NAME"
285
+ from pkg.config.other import NAME -> nothing (stem mismatch)
286
+
287
+ Args:
288
+ import_node: A ``from ... import`` statement node.
289
+ module_stem: The filename stem of the constants module being judged.
290
+
291
+ Returns:
292
+ The imported member names when the module path's final dotted segment
293
+ equals ``module_stem``, empty otherwise.
294
+ """
295
+ if _module_final_segment(import_node.module) != module_stem:
296
+ return set()
297
+ return {each_alias.name for each_alias in import_node.names}
298
+
299
+
300
+ def _import_module_bindings(all_import_aliases: list[ast.alias], module_stem: str) -> set[str]:
301
+ """Return the names an import's aliases bind to the constants module object.
302
+
303
+ ::
304
+
305
+ import pkg.config.constants -> "pkg.config.constants"
306
+ import pkg.config.constants as sg -> "sg"
307
+ from pkg.config import constants -> "constants"
308
+ from . import constants as sg -> "sg"
309
+ import pkg.config.other -> nothing (stem mismatch)
310
+
311
+ A plain ``import`` names the module by its dotted path, so the final
312
+ segment is matched against the stem. A ``from ... import`` alias name is a
313
+ single identifier, which the same final-segment match covers unchanged.
314
+
315
+ Args:
316
+ all_import_aliases: The ``names`` list of an ``Import`` or
317
+ ``ImportFrom`` node.
318
+ module_stem: The filename stem of the constants module being judged.
319
+
320
+ Returns:
321
+ The dotted paths and aliases through which the module object is
322
+ reachable.
323
+ """
324
+ binding_names: set[str] = set()
325
+ for each_alias in all_import_aliases:
326
+ if _module_final_segment(each_alias.name) != module_stem:
327
+ continue
328
+ binding_names.add(each_alias.asname or each_alias.name)
329
+ return binding_names
330
+
331
+
332
+ def _collect_widened_scan_facts(
333
+ tree: ast.Module, module_stem: str
334
+ ) -> tuple[set[str], set[str], list[ast.Attribute]]:
335
+ """Collect a candidate module's import and attribute facts in one walk.
336
+
337
+ Args:
338
+ tree: The parsed candidate module.
339
+ module_stem: The filename stem of the constants module being judged.
340
+
341
+ Returns:
342
+ The member names imported through a stem-matched ``from ... import``,
343
+ the names bound to the constants module object itself, and every
344
+ attribute node in the module.
345
+ """
346
+ member_names: set[str] = set()
347
+ binding_names: set[str] = set()
348
+ all_attribute_nodes: list[ast.Attribute] = []
349
+ for each_node in ast.walk(tree):
350
+ if isinstance(each_node, ast.ImportFrom):
351
+ member_names |= _stem_import_member_names(each_node, module_stem)
352
+ if isinstance(each_node, (ast.Import, ast.ImportFrom)):
353
+ binding_names |= _import_module_bindings(each_node.names, module_stem)
354
+ if isinstance(each_node, ast.Attribute):
355
+ all_attribute_nodes.append(each_node)
356
+ return member_names, binding_names, all_attribute_nodes
357
+
358
+
359
+ def _attribute_read_names(
360
+ all_attribute_nodes: list[ast.Attribute], all_binding_names: set[str]
361
+ ) -> set[str]:
362
+ """Return the constant names read as attributes on a module-object binding.
363
+
364
+ The expression an attribute is read on is spelled back to its dotted source
365
+ form and matched against the bindings, so an alias read (``sg.NAME``) and a
366
+ full dotted path (``pkg.config.constants.NAME``) both resolve, while a read
367
+ on a binding of some other module counts for nothing.
368
+
369
+ Args:
370
+ all_attribute_nodes: Every attribute node in the candidate module.
371
+ all_binding_names: The names bound to the constants module object.
372
+
373
+ Returns:
374
+ The attribute names read on a binding of the constants module.
375
+ """
376
+ if not all_binding_names:
377
+ return set()
378
+ return {
379
+ each_attribute.attr
380
+ for each_attribute in all_attribute_nodes
381
+ if _dotted_attribute_chain(each_attribute.value) in all_binding_names
382
+ }
383
+
384
+
385
+ def _widened_scan_reference_names(source: str, module_stem: str) -> set[str]:
386
+ """Return the names a repository-wide candidate contributes, parsing it once.
387
+
388
+ Both counted shapes are bound to the written module's filename stem, so a
278
389
  same-named constant exported by an unrelated module never masks a dead one.
279
390
 
280
391
  Args:
@@ -282,22 +393,18 @@ def _qualified_import_member_names(source: str, module_stem: str) -> set[str]:
282
393
  module_stem: The filename stem of the constants module being judged.
283
394
 
284
395
  Returns:
285
- The member names imported from a module whose final dotted segment equals
286
- ``module_stem``. A module that fails to parse contributes no names.
396
+ The members imported through a stem-matched ``from <module> import``,
397
+ plus the constant names read as attributes on an imported binding of the
398
+ constants module. A module that fails to parse contributes no names.
287
399
  """
288
400
  try:
289
401
  tree = ast.parse(source)
290
402
  except SyntaxError:
291
403
  return set()
292
- member_names: set[str] = set()
293
- for each_node in ast.walk(tree):
294
- if not isinstance(each_node, ast.ImportFrom):
295
- continue
296
- if _module_final_segment(each_node.module) != module_stem:
297
- continue
298
- for each_alias in each_node.names:
299
- member_names.add(each_alias.name)
300
- return member_names
404
+ member_names, binding_names, all_attribute_nodes = _collect_widened_scan_facts(
405
+ tree, module_stem
406
+ )
407
+ return member_names | _attribute_read_names(all_attribute_nodes, binding_names)
301
408
 
302
409
 
303
410
  def _scan_root_for_constants_module(file_path: str) -> Path:
@@ -350,11 +457,13 @@ def _read_candidate_source(file_path: Path, required_substring: str | None) -> s
350
457
  required_substring = None -> read and keep every file
351
458
  required_substring = the module stem -> keep only files naming it
352
459
  from pkg.foo_constants import BAR -> names the stem -> candidate
460
+ from pkg import foo_constants as c -> names the stem -> candidate
353
461
  def unrelated() -> int: ... -> no stem mention -> skipped
354
462
 
355
- The widened repository pass looks only for a ``from <module> import`` whose
356
- final dotted segment equals the constants module's filename stem, and such an
357
- import always spells that stem in the file's text. A file whose text never
463
+ The widened repository pass looks only for an import bound to the constants
464
+ module's filename stem a ``from <module> import`` whose final dotted segment
465
+ equals the stem, or an import of the module object itself and every such
466
+ import spells that stem in the file's text. A file whose text never
358
467
  mentions the stem cannot carry the import, so returning None for it lets the
359
468
  caller skip the file without spending scan-cap budget, which keeps the widened
360
469
  pass bounded to the candidate importer files even under a large repository.
@@ -422,7 +531,9 @@ def _collect_names_under_root(
422
531
  in before the walk begins.
423
532
  extract_names: Maps one module's source text to the set of names it
424
533
  contributes — the generous reference collector for the package-tree
425
- pass, the stem-bound import collector for the widened pass.
534
+ pass, and for the widened pass the stem-bound collector that counts
535
+ both a stem-matched from-import and an attribute read on a binding
536
+ of the written module.
426
537
  already_scanned_count: The parsed-file count accumulated by a prior
427
538
  pass, so the parse cap bounds the combined work of the
428
539
  package-tree and widened passes.
@@ -552,12 +663,13 @@ def check_dead_module_constants(
552
663
  are governed by the use-count rule instead. A constant is dead when its name
553
664
  appears in no ``.py`` module under the enclosing package tree — not imported,
554
665
  not read, not listed in another module's ``__all__`` literal, not named in a
555
- string annotation and, in the repository-wide scan the check widens to when
556
- the package-tree scan leaves the constant unreferenced, no module imports the
557
- name from a ``from <module> import`` whose final dotted segment equals this
558
- module's filename stem. Binding the widened scan to the stem keeps a genuine
559
- cross-tree consumer counting while a same-named constant exported by an
560
- unrelated module never masks a dead one. A module declaring ``__all__``
666
+ string annotation. When that leaves a constant unreferenced, the scan widens
667
+ to the repository, where two shapes count: a ``from <module> import`` whose
668
+ final dotted segment equals this module's filename stem, and an attribute
669
+ read on a binding of this module (``import pkg.config.constants as alias``
670
+ then the alias followed by the constant name). Binding both to the stem
671
+ keeps a same-named constant in an unrelated module from masking a dead one.
672
+ A module declaring ``__all__``
561
673
  narrows the check to the constants its ``__all__`` list names: each must be
562
674
  imported or read by another module, and the module's own ``__all__`` entry
563
675
  never counts as that consumer, so an exported constant no module consumes is
@@ -616,14 +728,14 @@ def check_dead_module_constants(
616
728
  if has_unreferenced_constant:
617
729
  repository_root = _repository_root_for(written_path)
618
730
  if repository_root is not None and repository_root != scan_root:
619
- collect_qualified_imports = partial(
620
- _qualified_import_member_names, module_stem=written_path.stem
731
+ collect_widened_scan_names = partial(
732
+ _widened_scan_reference_names, module_stem=written_path.stem
621
733
  )
622
734
  widened_names, _widened_count, widened_cap_was_hit = _collect_names_under_root(
623
735
  repository_root,
624
736
  written_path,
625
737
  set(),
626
- collect_qualified_imports,
738
+ collect_widened_scan_names,
627
739
  already_scanned_count=scanned_file_count,
628
740
  excluded_subtree=scan_root,
629
741
  required_substring=written_path.stem,
@@ -1,4 +1,8 @@
1
- """Attribute-chain and os.environ alias-resolution primitives for the test-isolation check."""
1
+ """Attribute-chain and os.environ alias-resolution primitives.
2
+
3
+ Serves the test-isolation check, and lends ``_dotted_attribute_chain`` to the
4
+ dead-module-constant check's widened attribute-read scan.
5
+ """
2
6
 
3
7
  import ast
4
8
  import sys
@@ -143,7 +147,7 @@ def _attribute_chain_resolves_to_os_environ(
143
147
  return canonical_chain == OS_ENVIRON_DOTTED_NAME
144
148
 
145
149
 
146
- def _dotted_attribute_chain(attribute_node: ast.Attribute) -> str | None:
150
+ def _dotted_attribute_chain(attribute_node: ast.expr) -> str | None:
147
151
  chain_parts: list[str] = []
148
152
  walker: ast.expr = attribute_node
149
153
  while isinstance(walker, ast.Attribute):
@@ -0,0 +1,10 @@
1
+ # hooks/blocking/config
2
+
3
+ A Python package that holds shared constants for blocking hooks. Opinionated prose-style gates import from here.
4
+
5
+ ## Key files
6
+
7
+ | File | Contents |
8
+ |---|---|
9
+ | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
10
+ | `prose_style_enforcement_constants.py` | `CLAUDE_PROSE_STYLE_ENFORCEMENT` opt-in (default off) for opinionated prose gates |
@@ -1,10 +1 @@
1
- # hooks/blocking/config
2
-
3
- A Python package that holds shared constants for blocking hooks. Opinionated prose-style gates import from here.
4
-
5
- ## Key files
6
-
7
- | File | Contents |
8
- |---|---|
9
- | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
10
- | `prose_style_enforcement_constants.py` | `CLAUDE_PROSE_STYLE_ENFORCEMENT` opt-in (default off) for opinionated prose gates |
1
+ @AGENTS.md
@@ -36,6 +36,7 @@ if hooks_parent_directory not in sys.path:
36
36
  sys.path.insert(0, hooks_parent_directory)
37
37
 
38
38
  from _gh_pr_author_swap_utils import ( # noqa: E402
39
+ _build_restore_failure_message,
39
40
  _command_invokes_gh_pr_create_in_stripped,
40
41
  _delete_state_file,
41
42
  _preprocess_command_for_matching,
@@ -89,8 +90,12 @@ def main() -> None:
89
90
  _delete_state_file(state_file)
90
91
  else:
91
92
  _write_line(
92
- f"[gh-pr-author-restore] failed to restore active gh account to {original_account!r}; "
93
- f"state file {state_file} left in place so the SessionStart cleanup hook can retry",
93
+ _build_restore_failure_message(
94
+ "gh-pr-author-restore",
95
+ original_account,
96
+ state_file,
97
+ "state file",
98
+ ),
94
99
  sys.stderr,
95
100
  )
96
101
  sys.exit(0)
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """
3
- Stop hook that blocks Claude responses ending on a promise about undone work.
2
+ """Stop hook that blocks a turn ending with a promise of future work.
4
3
 
5
- When a turn ends on a forward-looking statement of intent ("I'll now run the
6
- tests", "Let me implement the fix") instead of actually doing the work, the
7
- agent is forced to do the work now with tool calls, or - when genuinely blocked
8
- on input only the user can supply - route through AskUserQuestion and end
9
- cleanly. The rule name is long-horizon-autonomy.
4
+ When a turn ends with a forward-looking statement of intent ("I'll now run the
5
+ tests", "Let me implement the fix"), complete the work with tool calls during
6
+ the turn. Route work requiring user-only input through AskUserQuestion. The
7
+ rule name is long-horizon-autonomy.
10
8
  """
11
9
 
12
10
  import json
@@ -104,7 +102,7 @@ def find_intent_only_ending(text: str) -> bool:
104
102
 
105
103
 
106
104
  def main() -> None:
107
- """Read the stop-hook payload and block turns that end on a promise of undone work."""
105
+ """Read the stop-hook payload and block turns that promise future work."""
108
106
  try:
109
107
  hook_input = json.load(sys.stdin)
110
108
  except json.JSONDecodeError:
@@ -125,14 +123,11 @@ def main() -> None:
125
123
  sys.exit(0)
126
124
 
127
125
  block_reason = (
128
- "LONG-HORIZON-AUTONOMY GUARDRAIL: Your turn ends on a promise about work "
129
- "that is not yet done, rather than doing it. Do the work NOW with tool calls "
130
- "instead of describing what you are about to do.\n\n"
131
- "If the work is genuinely blocked on input only the user can give, route the "
132
- "ask through an AskUserQuestion tool call and end the turn cleanly. Otherwise, "
133
- "carry out the stated action this turn.\n\n"
134
- "You MUST re-output the complete response with the work actually performed, "
135
- "per the long-horizon-autonomy rule."
126
+ "LONG-HORIZON-AUTONOMY GUARDRAIL: This turn ends with a promise of future "
127
+ "work. Complete that work with tool calls during this turn. Route questions "
128
+ "requiring user-only input through AskUserQuestion, then end the turn cleanly.\n\n"
129
+ "Re-output the complete response with the work performed, per the "
130
+ "long-horizon-autonomy rule."
136
131
  )
137
132
  block_response = {
138
133
  "decision": "block",
@@ -0,0 +1,26 @@
1
+ # inventory_intent_records
2
+
3
+ The shared pending-intent store that lets `package_inventory_stale_blocker.py`
4
+ and `claude_md_orphan_file_blocker.py` add a new file and its inventory row in
5
+ one change, in either order. When one blocker denies, it records a note; the
6
+ sibling blocker reads that note to allow the matching second write.
7
+
8
+ ## Modules
9
+
10
+ | File | Purpose |
11
+ |---|---|
12
+ | `records.py` | Read, record, peek, and consume the per-session file and row intents; a missing or corrupt store reads as no notes |
13
+ | `__init__.py` | Package marker |
14
+
15
+ ## Subdirectories
16
+
17
+ | Entry | Description |
18
+ |---|---|
19
+ | `config/` | The records-file name shape, the freshness window, the list keys, and the note field names (`intent_records_constants.py`) |
20
+ | `tests/` | pytest suite for the records store |
21
+
22
+ ## Running tests
23
+
24
+ ```bash
25
+ python -m pytest packages/claude-dev-env/hooks/blocking/inventory_intent_records/tests/
26
+ ```
@@ -1,26 +1 @@
1
- # inventory_intent_records
2
-
3
- The shared pending-intent store that lets `package_inventory_stale_blocker.py`
4
- and `claude_md_orphan_file_blocker.py` add a new file and its inventory row in
5
- one change, in either order. When one blocker denies, it records a note; the
6
- sibling blocker reads that note to allow the matching second write.
7
-
8
- ## Modules
9
-
10
- | File | Purpose |
11
- |---|---|
12
- | `records.py` | Read, record, peek, and consume the per-session file and row intents; a missing or corrupt store reads as no notes |
13
- | `__init__.py` | Package marker |
14
-
15
- ## Subdirectories
16
-
17
- | Entry | Description |
18
- |---|---|
19
- | `config/` | The records-file name shape, the freshness window, the list keys, and the note field names (`intent_records_constants.py`) |
20
- | `tests/` | pytest suite for the records store |
21
-
22
- ## Running tests
23
-
24
- ```bash
25
- python -m pytest packages/claude-dev-env/hooks/blocking/inventory_intent_records/tests/
26
- ```
1
+ @AGENTS.md
@@ -0,0 +1,26 @@
1
+ # package_inventory_stale_blocker_parts
2
+
3
+ The concern modules `package_inventory_stale_blocker.py` wires together to block a
4
+ new production file its package inventory omits. The entry hook imports them and
5
+ re-exports their surface for the test suite.
6
+
7
+ ## Modules
8
+
9
+ | File | Purpose |
10
+ |---|---|
11
+ | `inventory_detection.py` | Surveys a directory's `README.md`/`CLAUDE.md`/`SKILL.md`, collects the filenames they name, and reports whether a maintained inventory omits the file being written |
12
+ | `decision.py` | Builds the deny payload naming the omitted file and closing with the retry hint, and emits the decision JSON |
13
+ | `__init__.py` | Package marker |
14
+
15
+ ## Subdirectories
16
+
17
+ | Entry | Description |
18
+ |---|---|
19
+ | `config/` | The retry hint the deny reason closes with and the inventory-name join separator (`inventory_blocker_constants.py`) |
20
+ | `tests/` | pytest suite with one test module per module above |
21
+
22
+ ## Running tests
23
+
24
+ ```bash
25
+ python -m pytest packages/claude-dev-env/hooks/blocking/package_inventory_stale_blocker_parts/tests/
26
+ ```
@@ -1,26 +1 @@
1
- # package_inventory_stale_blocker_parts
2
-
3
- The concern modules `package_inventory_stale_blocker.py` wires together to block a
4
- new production file its package inventory omits. The entry hook imports them and
5
- re-exports their surface for the test suite.
6
-
7
- ## Modules
8
-
9
- | File | Purpose |
10
- |---|---|
11
- | `inventory_detection.py` | Surveys a directory's `README.md`/`CLAUDE.md`/`SKILL.md`, collects the filenames they name, and reports whether a maintained inventory omits the file being written |
12
- | `decision.py` | Builds the deny payload naming the omitted file and closing with the retry hint, and emits the decision JSON |
13
- | `__init__.py` | Package marker |
14
-
15
- ## Subdirectories
16
-
17
- | Entry | Description |
18
- |---|---|
19
- | `config/` | The retry hint the deny reason closes with and the inventory-name join separator (`inventory_blocker_constants.py`) |
20
- | `tests/` | pytest suite with one test module per module above |
21
-
22
- ## Running tests
23
-
24
- ```bash
25
- python -m pytest packages/claude-dev-env/hooks/blocking/package_inventory_stale_blocker_parts/tests/
26
- ```
1
+ @AGENTS.md
@@ -0,0 +1,24 @@
1
+ # pii_prevention_blocker_parts
2
+
3
+ The concern modules `pii_prevention_blocker.py` wires together. The entry hook
4
+ imports them and re-exports their surface for the test suite.
5
+
6
+ ## Modules
7
+
8
+ | File | Purpose |
9
+ |---|---|
10
+ | `repository_exemption.py` | Resolves a commit's origin owner/repo slug, decides whether the repository skips the staged PII scan, and resolves the exact values that repository allows past the scan |
11
+ | `repository_resolution.py` | Reads the repository a commit command targets — composing multiple `-C` values and a leading `cd`/`pushd` — and builds the deny reason naming the path when the repository root does not resolve |
12
+ | `__init__.py` | Package marker |
13
+
14
+ ## Subdirectories
15
+
16
+ | Entry | Description |
17
+ |---|---|
18
+ | `config/` | The repository-resolution deny-message template and the session-cwd label (`repository_resolution_constants.py`) |
19
+
20
+ ## Running tests
21
+
22
+ ```bash
23
+ python -m pytest packages/claude-dev-env/hooks/blocking/tests/
24
+ ```
@@ -1,24 +1 @@
1
- # pii_prevention_blocker_parts
2
-
3
- The concern modules `pii_prevention_blocker.py` wires together. The entry hook
4
- imports them and re-exports their surface for the test suite.
5
-
6
- ## Modules
7
-
8
- | File | Purpose |
9
- |---|---|
10
- | `repository_exemption.py` | Resolves a commit's origin owner/repo slug, decides whether the repository skips the staged PII scan, and resolves the exact values that repository allows past the scan |
11
- | `repository_resolution.py` | Reads the repository a commit command targets — composing multiple `-C` values and a leading `cd`/`pushd` — and builds the deny reason naming the path when the repository root does not resolve |
12
- | `__init__.py` | Package marker |
13
-
14
- ## Subdirectories
15
-
16
- | Entry | Description |
17
- |---|---|
18
- | `config/` | The repository-resolution deny-message template and the session-cwd label (`repository_resolution_constants.py`) |
19
-
20
- ## Running tests
21
-
22
- ```bash
23
- python -m pytest packages/claude-dev-env/hooks/blocking/tests/
24
- ```
1
+ @AGENTS.md
@@ -0,0 +1,30 @@
1
+ # tdd_enforcer_parts
2
+
3
+ The concern modules `tdd_enforcer.py` wires together to run the TDD gate. Each
4
+ module owns one concern; the entry hook imports them and re-exports their
5
+ surface for the test suite.
6
+
7
+ ## Modules
8
+
9
+ | File | Purpose |
10
+ |---|---|
11
+ | `path_classification.py` | Classifies a write target (docs, tests, `.claude` trees) the gate skips, and extracts the written text from a Write, Edit, or MultiEdit payload |
12
+ | `content_analysis.py` | Decides whether a payload is constants-only, and whether an Edit or MultiEdit merely removes or reorders imports |
13
+ | `candidate_paths.py` | Resolves the candidate test files whose freshness can satisfy the gate for a production file |
14
+ | `freshness.py` | Checks whether a candidate test was modified within the window and holds a real test function |
15
+ | `git_tracking.py` | Detects a write that restores a git-tracked file absent on disk, so a remove-then-Write rewrite is exempt from the gate |
16
+ | `decisions.py` | Builds the deny reason and writes the allow or deny decision JSON to stdout |
17
+ | `__init__.py` | Package marker |
18
+
19
+ ## Subdirectories
20
+
21
+ | Entry | Description |
22
+ |---|---|
23
+ | `config/` | The freshness window, the ancestor-walk limit, the git-tracking command tokens, the source-file extension sets, and the join separator (`tdd_enforcer_constants.py`) |
24
+ | `tests/` | pytest suite with one test module per module above |
25
+
26
+ ## Running tests
27
+
28
+ ```bash
29
+ python -m pytest packages/claude-dev-env/hooks/blocking/tdd_enforcer_parts/tests/
30
+ ```
@@ -1,30 +1 @@
1
- # tdd_enforcer_parts
2
-
3
- The concern modules `tdd_enforcer.py` wires together to run the TDD gate. Each
4
- module owns one concern; the entry hook imports them and re-exports their
5
- surface for the test suite.
6
-
7
- ## Modules
8
-
9
- | File | Purpose |
10
- |---|---|
11
- | `path_classification.py` | Classifies a write target (docs, tests, `.claude` trees) the gate skips, and extracts the written text from a Write, Edit, or MultiEdit payload |
12
- | `content_analysis.py` | Decides whether a payload is constants-only, and whether an Edit or MultiEdit merely removes or reorders imports |
13
- | `candidate_paths.py` | Resolves the candidate test files whose freshness can satisfy the gate for a production file |
14
- | `freshness.py` | Checks whether a candidate test was modified within the window and holds a real test function |
15
- | `git_tracking.py` | Detects a write that restores a git-tracked file absent on disk, so a remove-then-Write rewrite is exempt from the gate |
16
- | `decisions.py` | Builds the deny reason and writes the allow or deny decision JSON to stdout |
17
- | `__init__.py` | Package marker |
18
-
19
- ## Subdirectories
20
-
21
- | Entry | Description |
22
- |---|---|
23
- | `config/` | The freshness window, the ancestor-walk limit, the git-tracking command tokens, the source-file extension sets, and the join separator (`tdd_enforcer_constants.py`) |
24
- | `tests/` | pytest suite with one test module per module above |
25
-
26
- ## Running tests
27
-
28
- ```bash
29
- python -m pytest packages/claude-dev-env/hooks/blocking/tdd_enforcer_parts/tests/
30
- ```
1
+ @AGENTS.md
@@ -47,7 +47,7 @@ TABLE_WITH_SLASH_COMMAND_AND_SUBDIR = (
47
47
  "# example\n\n"
48
48
  "| Entry | Description |\n"
49
49
  "|---|---|\n"
50
- "| `/commit` | Slash command |\n"
50
+ "| `/sr-loop` | Slash command |\n"
51
51
  "| `scripts/` | A subdirectory |\n"
52
52
  "| Plain prose, no backticks | Not a file |\n"
53
53
  )
@@ -1,6 +1,6 @@
1
1
  """Tests for check_banned_prefixes — flags function names with generic prefixes.
2
2
 
3
- CODE_RULES.md §5 / AGENTS.md "Naming → Function names use specific verbs:
3
+ CODE_RULES.md §5 "Naming → Function names use specific verbs:
4
4
  parse_invoice, dispatch_event, migrate_schema. Generic prefixes to replace:
5
5
  handle_, process_, manage_, do_."
6
6