claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,123 @@
1
+ """Regression tests: staged validation resolves ruff config from the original path.
2
+
3
+ The PreToolUse gate stages proposed content to an OS-temp file, so ruff would
4
+ discover its config by walking up from that temp path and never reach
5
+ ``packages/claude-dev-env/hooks/pyproject.toml``. Its ``[tool.ruff.lint]`` selects
6
+ B and PL, so a dropped config leaves the staged run less strict than the project.
7
+
8
+ ``validate_proposed_file`` resolves the config from the ORIGINAL target path, so
9
+ the project ruff config applies to the staged copy from any working directory.
10
+
11
+ ::
12
+
13
+ original path .../validators/config_probe_module.py, DIRTY_SOURCE (assert False)
14
+ flag (defect): staged temp copy -> no [tool.ruff] up-tree -> B011 not selected
15
+ ok (fixed): config resolved from original path -> B011 fires
16
+
17
+ original path .../validators/test_config_probe_module.py, same DIRTY_SOURCE
18
+ ok: the test_*.py per-file-ignore reaches the staged copy -> B011 suppressed
19
+ """
20
+
21
+ from pathlib import Path
22
+
23
+ import pytest
24
+
25
+ from .run_all_validators import ValidatorResult, get_project_root, validate_proposed_file
26
+
27
+ VALIDATORS_DIRECTORY = Path(__file__).parent
28
+ NON_TEST_PROBE_PATH = VALIDATORS_DIRECTORY / "config_probe_module.py"
29
+ TEST_PROBE_PATH = VALIDATORS_DIRECTORY / "test_config_probe_module.py"
30
+
31
+ RUN_ALL_VALIDATORS_PATH = VALIDATORS_DIRECTORY / "run_all_validators.py"
32
+ EXEMPT_PATHS_PATH = VALIDATORS_DIRECTORY / "exempt_paths.py"
33
+
34
+ DIRTY_SOURCE = (
35
+ "def probe_condition(observed_total: int) -> None:\n assert False, observed_total\n"
36
+ )
37
+
38
+ UNCHECKED_SUBPROCESS_SOURCE = (
39
+ "import subprocess\n\n\ndef run_listing() -> None:\n subprocess.run([\"ls\"])\n"
40
+ )
41
+
42
+ UNUSED_IMPORT_SOURCE = "import os\n"
43
+
44
+
45
+ def _ruff_result(all_results: list[ValidatorResult]) -> ValidatorResult:
46
+ """Return the Ruff validator result from a validate_proposed_file run."""
47
+ for each_result in all_results:
48
+ if each_result.name == "Ruff":
49
+ return each_result
50
+ raise AssertionError("no Ruff validator result was produced")
51
+
52
+
53
+ def _working_directory_for(cwd_kind: str, outside_repo_directory: Path) -> Path:
54
+ """Resolve the cwd a parametrized case runs from — repo root or outside it."""
55
+ if cwd_kind == "repo_root":
56
+ project_root = get_project_root()
57
+ assert project_root is not None, "repo root must resolve for this test"
58
+ return project_root
59
+ return outside_repo_directory
60
+
61
+
62
+ @pytest.mark.parametrize("cwd_kind", ["repo_root", "outside_repo"])
63
+ def test_staged_non_test_file_reports_b011_from_every_cwd(
64
+ cwd_kind: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
65
+ ) -> None:
66
+ monkeypatch.chdir(_working_directory_for(cwd_kind, tmp_path))
67
+
68
+ ruff_result = _ruff_result(validate_proposed_file(str(NON_TEST_PROBE_PATH), DIRTY_SOURCE))
69
+
70
+ assert "B011" in ruff_result.output
71
+
72
+
73
+ def test_staged_test_file_suppresses_b011_via_per_file_ignore(
74
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
75
+ ) -> None:
76
+ monkeypatch.chdir(tmp_path)
77
+
78
+ non_test_ruff_result = _ruff_result(
79
+ validate_proposed_file(str(NON_TEST_PROBE_PATH), DIRTY_SOURCE)
80
+ )
81
+ test_ruff_result = _ruff_result(validate_proposed_file(str(TEST_PROBE_PATH), DIRTY_SOURCE))
82
+
83
+ assert "B011" in non_test_ruff_result.output
84
+ assert "B011" not in test_ruff_result.output
85
+
86
+
87
+ @pytest.mark.parametrize("cwd_kind", ["repo_root", "outside_repo"])
88
+ def test_staged_run_all_validators_suppresses_plw1510_via_path_ignore(
89
+ cwd_kind: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
90
+ ) -> None:
91
+ monkeypatch.chdir(_working_directory_for(cwd_kind, tmp_path))
92
+
93
+ ruff_result = _ruff_result(
94
+ validate_proposed_file(str(RUN_ALL_VALIDATORS_PATH), UNCHECKED_SUBPROCESS_SOURCE)
95
+ )
96
+
97
+ assert "PLW1510" not in ruff_result.output
98
+ assert ruff_result.passed
99
+
100
+
101
+ def test_staged_exempt_paths_suppresses_f401_via_path_ignore(
102
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
103
+ ) -> None:
104
+ monkeypatch.chdir(tmp_path)
105
+
106
+ ruff_result = _ruff_result(
107
+ validate_proposed_file(str(EXEMPT_PATHS_PATH), UNUSED_IMPORT_SOURCE)
108
+ )
109
+
110
+ assert "F401" not in ruff_result.output
111
+ assert ruff_result.passed
112
+
113
+
114
+ def test_staged_non_carved_path_still_reports_plw1510(
115
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
116
+ ) -> None:
117
+ monkeypatch.chdir(tmp_path)
118
+
119
+ ruff_result = _ruff_result(
120
+ validate_proposed_file(str(NON_TEST_PROBE_PATH), UNCHECKED_SUBPROCESS_SOURCE)
121
+ )
122
+
123
+ assert "PLW1510" in ruff_result.output
@@ -8,6 +8,7 @@ when that content violates a validator, rather than grading the whole branch.
8
8
  import json
9
9
  import subprocess
10
10
  import sys
11
+ import tempfile
11
12
  from collections import Counter
12
13
  from pathlib import Path
13
14
  from unittest.mock import patch
@@ -16,12 +17,21 @@ import pytest
16
17
 
17
18
  from .run_all_validators import (
18
19
  ValidatorResult,
20
+ _escapes_temporary_root,
21
+ _hooks_subprocess_working_directory_and_environment,
19
22
  _scope_new_and_preexisting,
23
+ _temporary_path_preserving_directory_signal,
20
24
  _violation_line_number,
21
25
  main,
22
26
  run_validators_entrypoint_subprocess,
23
27
  )
24
28
 
29
+ CONFIG_DIR_TARGET_PATH = (
30
+ "CDP Automations/os_update_workflow/config/submission_constants.py"
31
+ )
32
+ PARENT_TRAVERSAL_TARGET_PATH = "../../escape_target.py"
33
+ RELATIVE_CONFIG_TARGET_PATH = "config/x.py"
34
+
25
35
  CLEAN_PYTHON_SOURCE = (
26
36
  "def add_two_numbers(first_number: int, second_number: int) -> int:\n"
27
37
  " return first_number + second_number\n"
@@ -85,7 +95,9 @@ class TestPreToolUseGate:
85
95
  def test_edit_validates_reconstructed_post_edit_content(
86
96
  self, tmp_path: Path
87
97
  ) -> None:
88
- target_file = tmp_path / "calculate.py"
98
+ target_directory = tmp_path / "neutral_edit_target"
99
+ target_directory.mkdir(exist_ok=True)
100
+ target_file = target_directory / "calculate.py"
89
101
  target_file.write_text(CLEAN_PYTHON_SOURCE, encoding="utf-8")
90
102
  completed = run_gate(
91
103
  {
@@ -107,6 +119,152 @@ class TestPreToolUseGate:
107
119
  assert completed.returncode == 0, completed.stderr
108
120
  assert "deny" not in completed.stdout
109
121
 
122
+ def test_write_to_config_dir_path_is_not_denied(self) -> None:
123
+ completed = run_gate(
124
+ {
125
+ "tool_name": "Write",
126
+ "tool_input": {
127
+ "file_path": CONFIG_DIR_TARGET_PATH,
128
+ "content": VIOLATING_PYTHON_SOURCE,
129
+ },
130
+ }
131
+ )
132
+ assert completed.returncode == 0, completed.stderr
133
+ assert "deny" not in completed.stdout
134
+
135
+ def test_write_relative_config_path_not_denied_when_cwd_under_system_temp(
136
+ self, monkeypatch: pytest.MonkeyPatch
137
+ ) -> None:
138
+ system_temp_root = Path(tempfile.gettempdir()).resolve()
139
+ monkeypatch.setenv("TEMP", str(system_temp_root))
140
+ monkeypatch.setenv("TMP", str(system_temp_root))
141
+ monkeypatch.setenv("TMPDIR", str(system_temp_root))
142
+ with tempfile.TemporaryDirectory(
143
+ dir=str(system_temp_root), prefix="gate_cwd_"
144
+ ) as temporary_root_string:
145
+ temporary_cwd = Path(temporary_root_string) / "gate_cwd"
146
+ temporary_cwd.mkdir()
147
+
148
+ def working_directory_under_system_temp() -> tuple[str, dict[str, str]]:
149
+ _working_directory, environment = (
150
+ _hooks_subprocess_working_directory_and_environment()
151
+ )
152
+ return str(temporary_cwd), environment
153
+
154
+ with patch(
155
+ "validators.run_all_validators._hooks_subprocess_working_directory_and_environment",
156
+ side_effect=working_directory_under_system_temp,
157
+ ):
158
+ completed = run_gate(
159
+ {
160
+ "tool_name": "Write",
161
+ "tool_input": {
162
+ "file_path": RELATIVE_CONFIG_TARGET_PATH,
163
+ "content": VIOLATING_PYTHON_SOURCE,
164
+ },
165
+ }
166
+ )
167
+ assert completed.returncode == 0, completed.stderr
168
+ assert "deny" not in completed.stdout
169
+
170
+ def test_write_to_parent_traversal_path_still_validates(self) -> None:
171
+ completed = run_gate(
172
+ {
173
+ "tool_name": "Write",
174
+ "tool_input": {
175
+ "file_path": PARENT_TRAVERSAL_TARGET_PATH,
176
+ "content": VIOLATING_PYTHON_SOURCE,
177
+ },
178
+ }
179
+ )
180
+ assert completed.returncode == 0, completed.stderr
181
+ assert '"permissionDecision": "deny"' in completed.stdout
182
+ assert "Magic Values" in completed.stdout
183
+
184
+
185
+ class TestTemporaryPathPreservingDirectorySignal:
186
+ def test_strips_parent_traversal_segments(self, tmp_path: Path) -> None:
187
+ assert _escapes_temporary_root("..") is True
188
+ staged_path = _temporary_path_preserving_directory_signal(
189
+ tmp_path, PARENT_TRAVERSAL_TARGET_PATH
190
+ )
191
+ assert staged_path == tmp_path / "escape_target.py"
192
+ assert staged_path.resolve().is_relative_to(tmp_path.resolve())
193
+
194
+ def test_keeps_config_directory_segment(self, tmp_path: Path) -> None:
195
+ staged_path = _temporary_path_preserving_directory_signal(
196
+ tmp_path, CONFIG_DIR_TARGET_PATH
197
+ )
198
+ assert staged_path.name == "submission_constants.py"
199
+ assert "config" in staged_path.parts
200
+ assert staged_path == tmp_path / "config" / "submission_constants.py"
201
+
202
+ def test_drops_leading_anchor(self, tmp_path: Path) -> None:
203
+ absolute_target = tmp_path / "config" / "submission_constants.py"
204
+ staging_root = tmp_path / "staging_root"
205
+ staging_root.mkdir()
206
+ staged_path = _temporary_path_preserving_directory_signal(
207
+ staging_root, str(absolute_target)
208
+ )
209
+ relative_parts = staged_path.relative_to(staging_root).parts
210
+ assert relative_parts == ("config", "submission_constants.py")
211
+ assert absolute_target.anchor not in relative_parts
212
+ assert staged_path.resolve().is_relative_to(staging_root.resolve())
213
+
214
+ def test_path_without_exemption_directory_stages_flat_basename(
215
+ self, tmp_path: Path
216
+ ) -> None:
217
+ pytest_shaped_target = (
218
+ tmp_path / "test_edit_introducing_new_viol0" / "legacy_module.py"
219
+ )
220
+ pytest_shaped_target.parent.mkdir(parents=True, exist_ok=True)
221
+ pytest_shaped_target.write_text(CLEAN_MARKER_FUNCTION, encoding="utf-8")
222
+ staging_root = tmp_path / "staging_root"
223
+ staging_root.mkdir()
224
+ staged_path = _temporary_path_preserving_directory_signal(
225
+ staging_root, str(pytest_shaped_target)
226
+ )
227
+ assert staged_path == staging_root / "legacy_module.py"
228
+
229
+ def test_relative_config_path_preserves_config_under_system_temp_cwd(
230
+ self, monkeypatch: pytest.MonkeyPatch
231
+ ) -> None:
232
+ system_temp_root = Path(tempfile.gettempdir()).resolve()
233
+ monkeypatch.setenv("TEMP", str(system_temp_root))
234
+ monkeypatch.setenv("TMP", str(system_temp_root))
235
+ monkeypatch.setenv("TMPDIR", str(system_temp_root))
236
+ with tempfile.TemporaryDirectory(
237
+ dir=str(system_temp_root), prefix="staging_cwd_"
238
+ ) as temporary_root_string:
239
+ temporary_root = Path(temporary_root_string)
240
+ previous_cwd = Path.cwd()
241
+ try:
242
+ monkeypatch.chdir(temporary_root)
243
+ staging_root = temporary_root / "staging_root"
244
+ staging_root.mkdir()
245
+ staged_path = _temporary_path_preserving_directory_signal(
246
+ staging_root, RELATIVE_CONFIG_TARGET_PATH
247
+ )
248
+ assert staged_path == staging_root / "config" / "x.py"
249
+ assert "config" in staged_path.parts
250
+ assert staged_path.name == "x.py"
251
+ finally:
252
+ monkeypatch.chdir(previous_cwd)
253
+
254
+ def test_escapes_temporary_root_rejects_absolute_and_parent_parts(
255
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
256
+ ) -> None:
257
+ sandbox_temp_root = tmp_path / "system_temp"
258
+ sandbox_temp_root.mkdir()
259
+ monkeypatch.setenv("TEMP", str(sandbox_temp_root))
260
+ monkeypatch.setenv("TMP", str(sandbox_temp_root))
261
+ monkeypatch.setenv("TMPDIR", str(sandbox_temp_root))
262
+ assert _escapes_temporary_root("..") is True
263
+ assert _escapes_temporary_root("scripts") is False
264
+ assert _escapes_temporary_root("config") is False
265
+ absolute_part = str(Path(tempfile.gettempdir()).resolve().anchor) or "/"
266
+ assert _escapes_temporary_root(absolute_part) is True
267
+
110
268
 
111
269
  class TestCliModeRegression:
112
270
  def test_cli_mode_reports_violations_and_exits_one(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,7 +21,15 @@
21
21
  "scripts/",
22
22
  "_shared/",
23
23
  "audit-rubrics/",
24
- "CLAUDE.md"
24
+ "CLAUDE.md",
25
+ "!**/__pycache__/**",
26
+ "!**/*.py[cod]",
27
+ "!**/.pytest_cache/**",
28
+ "!**/.mypy_cache/**",
29
+ "!**/.ruff_cache/**",
30
+ "!**/*.log",
31
+ "!**/debug-*.log",
32
+ "!**/*.egg-info/**"
25
33
  ],
26
34
  "keywords": [
27
35
  "claude-code",
package/rules/CLAUDE.md CHANGED
@@ -51,6 +51,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. A rule withou
51
51
  | `right-sized-engineering.md` | Simple over clever; functions over classes; concrete over abstract |
52
52
  | `self-contained-docs.md` | Every document is fully self-contained; no references to the conversation that produced it |
53
53
  | `shell-invocation-policy.md` | All Windows shell commands use `pwsh`; `Audit-ShellPolicy.ps1` reports the non-`pwsh` forms in the `settings.json` permission rules and `Migrate-ShellPolicy.ps1` rewrites them to `pwsh`, both run on demand, not as a live gate |
54
+ | `state-what-is.md` | Assert claims, decisions, and actions directly; replace exclusion (naming what a claim is not) and elimination (walking rejected candidates) with the claim or answer itself |
54
55
  | `tdd.md` | Test-driven development: red → green → refactor, no production code before a failing test |
55
56
  | `testing.md` | Test quality and infrastructure standards |
56
57
  | `vault-context.md` | Search Obsidian vault for prior sessions and decisions before substantive project work |
@@ -1,67 +1,45 @@
1
- ---
2
- paths:
3
- - "**/*.py"
4
- - "**/*.md"
5
- ---
6
-
7
- # Docstring Prose Matches Implementation
8
-
9
- **When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps. It applies equally to a skill's companion `SKILL.md` (or any sibling `.md`) that describes a producer the skill's `scripts/` carry out: a doc sentence that claims a produced artifact's ordering or content is the prose this rule governs, and it tracks the producer function's own docstring and body.
10
-
11
- ## Rule
12
-
13
- When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
14
-
15
- The gate validator `check_docstring_args_match_signature` covers the `Args:` section parameter names. Eleven more gate validators each cover one deterministic slice of the free-form prose. `check_docstring_delegation_summary_enumeration_drift` covers a thin delegating wrapper's docstring summary enumerating actions that the same-named function's summary in the named sibling file omits, compared from both save directions. `check_docstring_names_absent_type_checking_gate` covers a module or function docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, while no identifier in the module's code carries the `type_checking` marker — the drift where the prose points a reader at a gate the body never performs; drop the `TYPE_CHECKING` gate wording, or add the detection the prose describes. `check_docstring_length_constant_superlative_vs_exact_gate` covers a length-constant module whose docstring describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`) while the only code consuming the constant compares `len(...)` against it with `==`/`!=` — an exact-length gate that rejects every other length — and never with an ordered operator; the check scans the constant module's package tree (its own directory, or the parent package when the module sits in a `config/` subdirectory), so the mismatching consumer may sit in a sibling module. `check_docstring_fallback_branch_coverage` covers a summary that scopes a fallback to a single condition (`only when`, `falls back to ... when`) while the body routes to that same fallback call from two or more distinct early-return guards. `check_class_docstring_names_public_methods` covers a class whose docstring is a single summary line while the class exposes two or more public methods whose names the summary never spells out — the drift where a one-line class summary keeps naming its first feature after the class grows a second public entry point. `check_docstring_no_consumer_claim` covers a producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) — a transitional claim that drifts the moment a reader lands and contradicts any companion `SKILL.md` that documents the consumer; this is the deterministic slice of the O8 companion-doc producer/consumer drift below. `check_docstring_returns_plural_cardinality` covers a `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) while the returned dict literal holds exactly one key in that family (`sheen_mid`) — the drift where a single-key family carries a plural noun, so the prose claims a cardinality of two or more that the dict does not hold. `check_docstring_args_single_line_scope_vs_span` covers an `Args:` entry whose prose scopes a finding to a single named line (`only when its block-anchor line is among the changed lines`) while the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper — the drift where the Args entry claims a narrower single-line scope than the span-intersection body applies, so an edit touching any non-anchor line of the span still blocks. `check_docstring_cardinal_count_matches_constant_family` covers a docstring that states a cardinal count of an outcome family (`Covers the four outcome branches: ...`) and lists those members, while the module references more members of the same `UPPER_SNAKE` constant family than the count names (`OUTCOME_OFFENDER_UNREADABLE` is imported and exercised, yet the summary stops at four) — the drift where a summary keeps the old count after the code grows another branch; this gate runs on test modules as well as production modules. `check_docstring_raises_unraisable_largezipfile` covers a `Raises:` clause that names `zipfile.LargeZipFile` while the function opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` left at its default of True — `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False, so a writer that allows ZIP64 documents an exception the body cannot produce. `check_docstring_no_network_claim_with_metadata_access` covers a function docstring promising a code path returns `without touching the network` (or a sibling no-network phrase) while the body calls a path-metadata method (`is_file`, `is_dir`, `exists`, `stat`, `lstat`) — on a network share each metadata call is a round-trip over the wire, so a cache-hit path the docstring swore avoids the network still pays a stat on every call; reword the claim to state the path is stat-checked on every call, or short-circuit to the cached path before the share is touched. The remaining free-form prose — `"a field counts as read when ..."`, `"resolves to shared temp only"`, `"strip ceremony, then drop blockquotes"`, and the broader module-responsibility paragraph outside the user-facing-text-scope slice the checklist below names — has no signature, method roster, or single structural shape to compare against, so the gate cannot catch its drift. This rule is the judgment standard for that prose; the audit lane below is the enforcement for everything outside the twelve gated slices.
16
-
17
- ## What to check before you write the docstring
18
-
19
- Read the body and the docstring side by side:
20
-
21
- - **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration. A union member the code applies but the prose omits is a gap.
22
- - **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
23
- - **Shared fallback routes.** A summary that scopes a fallback call to one condition names every condition that reaches that call. When the body routes to the same fallback from two or more early-return guards (`if a is None: fallback(); return` and `if random() < p: fallback(); return`), the prose enumerates both guards. The `check_docstring_fallback_branch_coverage` gate blocks the single-condition form of this drift at Write/Edit time.
24
- - **Step order.** A docstring that says `A then B then C` matches the call order in the body. A step enumeration that names the body's linear steps also names every corrective step the body guards inside an `if`/`elif` branch (`if not await cancel_and_reinitiate_update(...): return`). The `check_docstring_step_enumeration_dispatch_coverage` gate blocks the branch-guarded-dispatch form of this drift — a step-enumeration docstring that omits a two-or-more-token dispatch step the body guards inside a branch — at Write/Edit time.
25
- - **Delegation pointer summaries.** A thin delegating method whose docstring names its actions and points at the home of the real body (Apply Russia, review note, publication edits; full doc on `listing_edit_flow`) lists the same actions the delegated function's own summary lists. When an edit moves one action out of the delegated body, the same edit rewords both summaries. The `check_docstring_delegation_summary_enumeration_drift` gate blocks this drift as either side lands: a saved wrapper docstring gets compared against the delegated body beside it, and a saved delegated body gets compared against every neighboring wrapper docstring pointing at it. A conditional bullet in the delegated prose (`re-apply the privacy policy after the upload`) also names every exception the body honors (`unless the reopened button reads Re-register`) — that conditional-completeness slice stays a judgment call for the audit lane.
26
- - **JS/`.mjs` resume-task enumerations.** A `spawn<Role>Agent` JSDoc that enumerates its sibling `resume<Role>Agent`'s resume tasks in a parenthetical `resume (repair-verify, hardening-verify)` list names every `task === '<name>'` branch the resume body dispatches on. The `check_js_resume_task_enumeration_coverage` gate blocks the JavaScript form of this drift — a spawn JSDoc whose resume enumeration omits a dispatched task — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
27
- - **JS/`.mjs` `@returns` object with a schema-less branch.** A `function` whose JSDoc `@returns {Promise<object>}` promises a structured object names a return type every branch honors. When the body returns one agent-spawn helper both with a `schema` options object and without one, the schema-less branch resolves to a transcript string, not the object the JSDoc claims. The `check_js_returns_object_schemaless_branch` gate blocks this drift — a `Promise<object>` JSDoc whose body returns the same helper with and without a `schema` key — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
28
- - **JS/`.mjs` sibling return-object key drift.** Every return path of a function that yields the same record carries the same keys. When an early return in a workflow body omits exactly one key its sibling tail return carries (`allDeferredPrs`), a caller reading that key off the short path gets undefined where the documented contract and the sibling return both promise it. The `check_js_sibling_return_object_key_drift` gate blocks this drift — a `return { ... }` object literal whose key set misses exactly one key of a sibling return in the same function or module scope, with discriminated-union variants and two-or-more-key exit shapes left alone — at Write/Edit time. This is the `.mjs` slice of the same Category O standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
29
- - **JS/`.mjs` bare-flag return directive vs a stated full-result contract.** A converge-workflow preamble that states a full result-object contract (`{sha, clean:false, down:true, findings:[]}`) and adds "never a bare down flag" matches any `return <flag>: true`/`false` prose directive elsewhere in the file that repeats a status flag the stated contract rules out. When a step's prose says to "return down: true" once a budget runs out, that directive repeats the bare flag the preamble rules out — a StructuredOutput run whose schema needs every field would reject a lone `{down:true}`. The `check_js_bare_flag_return_directive` gate blocks this drift — a `return <name>: true`/`false` prose directive anywhere in the file that repeats a status flag a stated contract rules out, with no proximity or ordering check between the two — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
30
- - **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) matches the count of keys in that family in the returned dict literal. When the dict holds one key in the family (`sheen_mid`), the noun is singular (`the sheen stop`); a plural noun there claims two or more entries the dict does not hold. The `check_docstring_returns_plural_cardinality` gate blocks the single-key-with-plural-noun form of this drift at Write/Edit time.
31
- - **Length-constant superlative vs exact gate.** A module docstring that describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`, `no longer than`) matches how the code consumes the constant. When the only consumer compares `len(...)` against the constant with `==`/`!=` — an exact-length gate where every other length is rejected, not accepted at a shorter length — the superlative prose claims a range of accepted lengths the code never allows. State the exact required length (`the exact #AARRGGBB length`), not a longest/range form. The `check_docstring_length_constant_superlative_vs_exact_gate` gate blocks this drift at Write/Edit time, scanning the constant module's package tree so it sees a consumer that lives in a sibling module; a constant genuinely used as a ceiling (`len(x) <= LIMIT`) is left alone.
32
- - **Args single-line scope vs span body.** An `Args:` entry that scopes a finding to one named line (`a finding blocks only when its block-anchor line is among the changed lines`) matches the line breadth the body scopes by. When the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper that blocks when any line of the span is among the changed lines, the single-line Args wording understates the scope: an edit touching a non-anchor line of the span still blocks. State the Args entry on the same span breadth the body uses (`a finding blocks when any line of its block span is among the changed lines`). The `check_docstring_args_single_line_scope_vs_span` gate blocks the single-line-Args-over-span-body form of this drift at Write/Edit time.
33
- - **Cardinal-count enumerations.** A docstring that states a count of an outcome family (`the four outcome branches`) and lists those members names every member of that family the module references. When the module imports and exercises a fifth `OUTCOME_*` constant the summary leaves out, the count and the list both under-describe the code. The `check_docstring_cardinal_count_matches_constant_family` gate blocks this drift — a cardinal-count docstring that names two or more members of a referenced `UPPER_SNAKE` constant family, leaves at least one referenced member out, and states a count that differs from the family size (a count above it and one below it both trip) — at Write/Edit time, on test modules as well as production modules.
34
- - **Raises-clause reachability for `LargeZipFile`.** A `Raises:` clause that names `zipfile.LargeZipFile` matches a writer the body opens with ZIP64 forbidden. `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False; a function that opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` at its default of True allows ZIP64 and never raises it, so the clause documents an unreachable exception. Drop the entry, or pass `allowZip64=False` when forbidding ZIP64 is the goal. The `check_docstring_raises_unraisable_largezipfile` gate blocks the default-ZIP64-writer form of this drift at Write/Edit time; a writer that forbids ZIP64 on any open, a read-only open, and a function that opens no writer are all left alone.
35
- - **Module summary scope versus data-schema constants.** A module whose one-line docstring scopes its contents to user-facing text (`User-facing strings: CLI flag names, help text, and log messages`) names every category of constant the body holds. When the body also defines serialization field keys (`JSONL_FIELD_*`), run-metadata schema keys (`RUN_METADATA_CLI_ARG_KEY_*`), or runtime config (`STDOUT_ENCODING`, `MAIN_LOGGING_FORMAT_STRING`), the strings-only summary under-describes the module — the module-responsibility drift the repo flags. Broaden the summary to name the data-schema keys and runtime config. The `check_module_docstring_scope_omits_data_schema_constants` gate blocks this drift at Write/Edit time, and fires only when the summary claims a user-facing-text scope and names no data-schema or runtime-config category, so a summary that already names `field keys`, `schema`, or `runtime config` passes.
36
- - **Field meaning: run mode versus per record.** A dataclass or `TypedDict` field documented in the class `Attributes:` block states what the field means for one record. When the code sets that field the same way for every record (a run-mode flag such as `is_dry_run = not is_execute` at each write site), the description states the run-mode meaning, not a per-record outcome. A field named `is_dry_run` documented as `True when no STP was written` reads as a per-record write result, but the value tracks the run mode, so a record that writes no file during an execute run still stores `False`. State the run-mode meaning the assignment gives the field. The `check_docstring_field_runmode_outcome` gate blocks the single-file shape of this drift at Write/Edit time — an `Attributes:` entry for a run-mode flag field (a name carrying `dry_run`) whose description carries a per-record write-outcome phrase and no run-mode phrase. The assignment that sets the field sits in another module, out of reach of that single-file gate, so any shape the gate cannot key on stays an O6 audit-lane judgment finding.
37
- - **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
38
- - **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
39
- - **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order (`sorted`, `alphabetical`, `in sorted order`) or its content (`the at-risk names`, `just the current set`) matches the producer function's docstring and body for that same artifact. A producer that builds the artifact by merging stored names with new names and appending — preserving file order, not re-sorting the union — leaves a doc that still says `sorted` drifted on both counts: the order claim is wrong, and the content claim hides the merged-in prior entries. When the producer's ordering or union changes, the same change updates the companion doc. The two move together in one commit, even when the producer edit does not touch the `.md` file.
40
- - **TYPE_CHECKING gate claim vs code.** A docstring that names a `TYPE_CHECKING` gate-detection step, or a `type-checking-gate` helper family, matches a module whose code handles TYPE_CHECKING. When no identifier in the body carries the `type_checking` marker — no `TYPE_CHECKING` load, import alias, attribute, or helper name — the prose points a reader at a gate the module never performs. State what the module does, or add the detection the prose describes. The `check_docstring_names_absent_type_checking_gate` gate blocks this drift at Write/Edit time, and covers hook infrastructure, where the import-scan gates that carry this drift class live.
41
-
42
- When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
43
-
44
- ## Worked example
45
-
46
- A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
47
-
48
- ```python
49
- read_names = (
50
- attribute_read_names
51
- | dynamic_literal_names
52
- | _match_pattern_attribute_names(tree)
53
- | _exported_names(tree)
54
- )
55
- ```
56
-
57
- A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
58
-
59
- ## Enforcement (audit lane)
60
-
61
- This drift class is sub-bucket **O6** in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` (free-form `Note:` / `Returns:` / responsibility-list claims). The audit teammate lists every prose enumeration in a changed docstring and verifies each item against the body, and lists every union member / suppressor / step in the body and verifies each appears in the prose. A union member or suppressor in the body that the prose omits is an O6 finding. The single-condition shared-fallback shape of this drift is gated deterministically by `check_docstring_fallback_branch_coverage` (`packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py`); the audit lane covers every O6 shape the gate cannot match.
62
-
63
- When a changed PR touches a producer function whose ordering or union shifts, the O8 audit lane also reads that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact. A doc sentence that claims the artifact is `sorted` or holds `just the at-risk names` while the producer merges prior names and appends without re-sorting is an O8 finding, even when the PR diff never touched the `.md` file — the behavior change orphaned the doc claim.
64
-
65
- ## Why
66
-
67
- A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have. Naming this standard makes the gap a first-class finding at write time and at audit, rather than a surprise a reader hits months later.
1
+ ---
2
+ paths:
3
+ - "**/*.py"
4
+ - "**/*.md"
5
+ ---
6
+
7
+ # Docstring Prose Matches Implementation
8
+
9
+ **When this applies:** Any Write or Edit to a public function, method, class, or module whose docstring prose makes an enumerable claim about behavior — a list of inputs the code handles, the conditions it treats as a match, the cases it skips, or the order of its steps. It applies equally to a skill's companion `SKILL.md` (or any sibling `.md`) that describes a producer the skill's `scripts/` carry out: a doc sentence that claims a produced artifact's ordering or content is the prose this rule governs, and it tracks the producer function's own docstring and body.
10
+
11
+ ## Rule
12
+
13
+ When a docstring enumerates the behaviors a body applies, the enumeration covers every behavior the body applies. A reader trusts the list to be complete: an item the code applies but the prose omits is a silent gap that misleads every future reader and reviewer.
14
+
15
+ When the body changes the set of behaviors it applies, the same edit updates the prose enumeration. The two move together in one commit.
16
+
17
+ ## Write-time checks
18
+
19
+ Read the body and the docstring side by side. Apply each check that matches the prose:
20
+
21
+ - **Unions / match sources** every member of a "what counts" union appears in the prose.
22
+ - **Suppressors / skip lists** every early-return suppressor appears in the prose.
23
+ - **Step order** named order matches call order; branch-guarded corrective steps are named too.
24
+ - **Shared fallbacks** every condition that reaches a fallback call is named.
25
+ - **Predicate breadth** the body accepts only the inputs the prose names.
26
+ - **Exclusion axis** an exclusion clause keys on the same axis the body classifies on.
27
+ - **Companion docs** a `SKILL.md` (or sibling) order/content claim matches the producer body.
28
+ - **Gate-outcome status flags** an outcome routed to a blocker (`blocker = ...; break`) reads as blocked everywhere, never as a bypass.
29
+ - **Returns / Raises / Note claims** each free-form claim matches the body.
30
+
31
+ Many deterministic shapes of this drift have Write/Edit gates in `packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py` (and the JS/`.mjs` slices in `code_rules_imports_logging.py`). Free-form rest is judgment.
32
+
33
+ ## Full standard
34
+
35
+ The full Category O judgment standard sub-buckets O1–O9, the complete write-time gate inventory, free-form checklists, and worked examples lives in:
36
+
37
+ `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md`
38
+
39
+ ## Division of labor
40
+
41
+ | Surface | Role |
42
+ |---|---|
43
+ | **This rule** | Always-on write-time policy and the compact checklist above. |
44
+ | Category O rubric | Single thick source for the full standard (on demand). |
45
+ | Category O prompt | Audit template; points at the rubric for judgment. |
@@ -26,6 +26,13 @@ Handle the two kinds of content differently:
26
26
 
27
27
  Both slash directions count.
28
28
 
29
+ The worktree and job-scratch entries count as a path when either:
30
+
31
+ - A `/` or `\` sits right before them — a drive-letter path (`C:\Users\me\.claude\worktrees\wt\f.py`), a home path (`~/.claude/worktrees/wt`), or a POSIX absolute path (`/home/me/.claude-editor/jobs/j/log.txt`).
32
+ - A path segment follows them — a relative path that names a child under that directory (`see .claude/worktrees/wt-1/notes.md`, a markdown link target, or `cd .claude/worktrees/wt-199`).
33
+
34
+ With neither anchor the text names the directory rather than something inside it, and it posts — a quoted config constant, a backticked directory name, or a placeholder form such as `.claude/worktrees/<name>`. Un-backticked prose that puts a word immediately after the marker reads the same as a relative path and is blocked; the placeholder form is the documented escape.
35
+
29
36
  ## Enforcement
30
37
 
31
38
  The `volatile_path_in_post_blocker` PreToolUse hook reads the body of each `gh` post command and each GitHub MCP post call, scans it for these markers, and blocks the post when it finds one. For a `--body-file`, the hook reads the file and scans its contents, so writing the body to a temp file and passing it with `--body-file` stays allowed — what the hook rejects is a volatile path inside the text that gets posted.
@@ -0,0 +1,25 @@
1
+ ---
2
+ paths:
3
+ - "**/*.md"
4
+ - "**/*.py"
5
+ - "**/*.mjs"
6
+ - "**/*.js"
7
+ - "**/*.ts"
8
+ - "**/*.ps1"
9
+ - "**/*.sh"
10
+ ---
11
+
12
+ # State What Is
13
+
14
+ Write settled facts, decisions, and actions that are load-bearing for the reader now. Assert each claim, decision, or action directly, and let it stand on its own evidence.
15
+
16
+ A detail earns its place by mattering now, or by being timeless enough to matter for future work or plans. Omit everything else as noise.
17
+
18
+ ## Two patterns to replace on sight
19
+
20
+ - **Exclusion** — establishing a claim by naming what it is not, or by listing what is out of bounds. Replace it with the claim itself.
21
+ - **Elimination** — walking rejected candidates (causes, options, paths, explanations) to reach the answer. Replace it with the answer and the evidence that supports it.
22
+
23
+ ## Sibling rules
24
+
25
+ This rule sits beside the other prose-quality rules in this directory: [`no-historical-clutter.md`](no-historical-clutter.md) cuts references to old state, [`self-contained-docs.md`](self-contained-docs.md) cuts references to the conversation that produced a doc, [`no-justification-noise.md`](no-justification-noise.md) cuts sentences that only justify a stated choice, and [`plain-language.md`](plain-language.md) swaps heavy words for everyday ones. This rule governs how a claim gets established — directly, not by elimination or exclusion.
@@ -17,7 +17,7 @@ That situation arises when the verdict fails to cover the current surface even t
17
17
 
18
18
  ## When the marker is not allowed
19
19
 
20
- Every other case runs the verification: spawn the `code-verifier` agent and let the SubagentStop hook mint the verdict. In particular, never use the marker to:
20
+ Every other case runs the verification: spawn the `code-verifier` agent — `model: sonnet`, worker-model routing per [`skills/orchestrator/SKILL.md`](../skills/orchestrator/SKILL.md#workflow-agent-routing); resolver-supplied sonnet-equivalent on third-party hosts — and let the SubagentStop hook mint the verdict. In particular, never use the marker to:
21
21
 
22
22
  - Skip a branch's first verification.
23
23
  - Commit or push after any real code change since the last clean verdict — one changed line means a fresh verification.
package/scripts/CLAUDE.md CHANGED
@@ -27,6 +27,7 @@ Utility scripts installed into `~/.claude/scripts/` by `bin/install.mjs`. Each s
27
27
  | `check.ps1` | Runs the full code-quality check suite |
28
28
  | `Show-Asset.ps1` | Opens files on screen, sizing each image window to the image's pixel dimensions (scaled to fit the screen); non-image files open in their default application |
29
29
  | `Get-SessionAccount.ps1` | Reports which Claude account the current session is actually logged into by comparing `~/.claude.json`'s CLI login against a `CLAUDE_USER_DATA_DIR` desktop profile's `lastKnownAccountUuid`, recovering the desktop account's email from profile storage when the two accounts differ |
30
+ | `Capture-PoolHealth.ps1` | Captures Windows memory pool counters, high-handle processes, and kernel pool tags (via `NtQuerySystemInformation` class 22), prints a threshold verdict with a remediation map, and exits non-zero when any alert threshold fires |
30
31
 
31
32
  ## Subdirectories
32
33