claude-dev-env 2.9.0 → 2.10.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 (289) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +58 -548
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +32 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/references/CLAUDE.md +1 -0
  44. package/docs/references/advisor-tool.md +1 -1
  45. package/docs/references/prose-style-enforcement.md +25 -0
  46. package/hooks/blocking/CLAUDE.md +6 -6
  47. package/hooks/blocking/_path_setup.py +9 -5
  48. package/hooks/blocking/code_rules_docstrings.py +124 -30
  49. package/hooks/blocking/code_rules_enforcer.py +161 -16
  50. package/hooks/blocking/code_rules_shared.py +40 -23
  51. package/hooks/blocking/config/CLAUDE.md +3 -5
  52. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  53. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  54. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  55. package/hooks/blocking/hedging_language_blocker.py +103 -20
  56. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  57. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  58. package/hooks/blocking/plain_language_blocker.py +139 -20
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  60. package/hooks/blocking/state_description_blocker.py +7 -1
  61. package/hooks/blocking/tdd_enforcer.py +8 -0
  62. package/hooks/blocking/test__path_setup.py +28 -0
  63. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  64. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  65. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  66. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  67. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  68. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  69. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  70. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  71. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  72. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  73. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  74. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  75. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  76. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  77. package/hooks/blocking/test_state_description_blocker.py +45 -2
  78. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  79. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  80. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  81. package/hooks/hooks.json +15 -0
  82. package/hooks/hooks_constants/CLAUDE.md +14 -3
  83. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  84. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  85. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  86. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  87. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  88. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  89. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  90. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  91. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  92. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  93. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  94. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  95. package/hooks/hooks_constants/session_start_injector.py +163 -0
  96. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  97. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  98. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  99. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  100. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  101. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  102. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  103. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  104. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  105. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  106. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  107. package/hooks/observability/CLAUDE.md +2 -0
  108. package/hooks/observability/prose_matcher_advisory.py +237 -0
  109. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  110. package/hooks/session/CLAUDE.md +9 -1
  111. package/hooks/session/_path_setup.py +13 -0
  112. package/hooks/session/issue_tracker_session_starter.py +135 -0
  113. package/hooks/session/orchestrator_auto_starter.py +100 -0
  114. package/hooks/session/test__path_setup.py +28 -0
  115. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  116. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  117. package/hooks/session/test_working_style_prompt.py +47 -0
  118. package/hooks/session/untracked_repo_detector.py +1 -24
  119. package/hooks/session/working_style_prompt.py +36 -0
  120. package/hooks/validators/_path_setup.py +19 -0
  121. package/hooks/validators/run_all_validators.py +8 -13
  122. package/installable-surfaces.manifest.json +21 -0
  123. package/output-styles/CLAUDE.md +1 -3
  124. package/package.json +4 -2
  125. package/rules/CLAUDE.md +1 -0
  126. package/rules/durable-post-artifacts.md +2 -2
  127. package/rules/eli11-replies.md +6 -1
  128. package/rules/hedging-claims.md +4 -2
  129. package/rules/long-horizon-autonomy.md +3 -1
  130. package/rules/opus5-communication-contract.md +45 -0
  131. package/rules/plain-language.md +2 -2
  132. package/rules/research-mode.md +1 -1
  133. package/scripts/CLAUDE.md +11 -0
  134. package/scripts/Sync-RepoMain.ps1 +215 -0
  135. package/scripts/active_capability_references.py +218 -0
  136. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  137. package/scripts/claude_chain_runner.py +394 -6
  138. package/scripts/claude_chain_usage.py +1 -1
  139. package/scripts/codex_compat_materializer.py +105 -85
  140. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  141. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  142. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  143. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  144. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  145. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  146. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  147. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  148. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  149. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  150. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  151. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  152. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  153. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  154. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  155. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  156. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  157. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  158. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  159. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  160. package/scripts/spawn_grok_batch.py +3 -0
  161. package/scripts/test_active_capability_references.py +108 -0
  162. package/scripts/test_claude_chain_runner.py +414 -82
  163. package/scripts/test_claude_chain_usage.py +12 -12
  164. package/scripts/test_resolve_worker_spawn.py +2 -2
  165. package/scripts/test_verify_installable_package.py +208 -0
  166. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  167. package/scripts/verify_installable_package.py +612 -0
  168. package/settings.json +10 -0
  169. package/skills/CLAUDE.md +2 -0
  170. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  171. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  172. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  173. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  174. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  175. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  176. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  177. package/skills/autoconverge/reference/convergence.md +2 -1
  178. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  179. package/skills/beat-sheet/SKILL.md +54 -0
  180. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  181. package/skills/bugteam/CONSTRAINTS.md +4 -4
  182. package/skills/bugteam/EXAMPLES.md +1 -1
  183. package/skills/bugteam/reference/README.md +1 -1
  184. package/skills/e-code-review/SKILL.md +26 -5
  185. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  186. package/skills/e-code-review/reference/medium.md +15 -4
  187. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  188. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  189. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  190. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  191. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  192. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  193. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  194. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  195. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  196. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  197. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  198. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  199. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  200. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  201. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  202. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  203. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  204. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  205. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  206. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  207. package/skills/pr-converge/scripts/conftest.py +35 -0
  208. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  209. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  210. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  211. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  212. package/skills/rebase/SKILL.md +15 -3
  213. package/skills/reviewer-gates/SKILL.md +2 -2
  214. package/skills/show/SKILL.md +51 -0
  215. package/skills/show/references/accessibility.md +7 -0
  216. package/skills/show/references/art.md +3 -0
  217. package/skills/show/references/charts.md +3 -0
  218. package/skills/show/references/core-design.md +14 -0
  219. package/skills/show/references/erds.md +3 -0
  220. package/skills/show/references/flowcharts.md +3 -0
  221. package/skills/show/references/host-and-html.md +3 -0
  222. package/skills/show/references/illustrative-diagrams.md +10 -0
  223. package/skills/show/references/interaction.md +3 -0
  224. package/skills/show/references/mockups.md +3 -0
  225. package/skills/show/references/quality-gates.md +7 -0
  226. package/skills/show/references/structural-diagrams.md +3 -0
  227. package/skills/show/references/subject-inventory.md +21 -0
  228. package/skills/show/references/svg-contract.md +22 -0
  229. package/skills/show/routing.yaml +30 -0
  230. package/skills/show/samples/pr1262-v2.svg +222 -0
  231. package/skills/show/scripts/README.md +6 -0
  232. package/skills/show/scripts/validate-artifact.py +91 -0
  233. package/skills/show/scripts/validate-package.py +18 -0
  234. package/skills/show/templates/html-widget.html +4 -0
  235. package/skills/show/templates/svg-base.svg +19 -0
  236. package/skills/show/tests/fixtures/css-var.svg +6 -0
  237. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  238. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  239. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  240. package/skills/show/tests/fixtures/invalid.svg +1 -0
  241. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  242. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  243. package/skills/show/tests/fixtures/valid.html +1 -0
  244. package/skills/show/tests/test_validate-artifact.py +74 -0
  245. package/skills/show/tests/test_validators.py +59 -0
  246. package/skills/show/workflows/create-visual.md +13 -0
  247. package/skills/show/workflows/review-visual.md +20 -0
  248. package/skills/split-pr/SKILL.md +85 -0
  249. package/skills/split-pr/reference/path-layers.md +16 -0
  250. package/skills/split-pr/reference/proposal-format.md +15 -0
  251. package/skills/split-pr/reference/split-further-loop.md +10 -0
  252. package/skills/split-pr/reference/splitting-principles.md +26 -0
  253. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  254. package/skills/split-pr/scripts/categorize_files.py +106 -0
  255. package/skills/split-pr/scripts/config/__init__.py +1 -0
  256. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  257. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  258. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  259. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  260. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  261. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  262. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  263. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  264. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  265. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  266. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  267. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  268. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  269. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  270. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  271. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  272. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  273. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  274. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  275. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  276. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  277. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  278. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  279. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  280. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  281. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  282. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  283. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  284. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  285. package/skills/split-pr/scripts/verify_plan.py +139 -0
  286. package/system-prompts/software-engineer.xml +11 -2
  287. package/commands/initialize.md +0 -90
  288. package/commands/stubcheck.md +0 -88
  289. package/output-styles/caveman-agent.md +0 -37
@@ -1,13 +1,15 @@
1
- """Tests for check_docstring_args_single_line_scope_vs_span — Category O6 drift.
2
-
3
- A docstring Args: entry that scopes a finding to one named line ("only when its
4
- block-anchor line is among the changed lines") while the body builds a range()
5
- span and routes it through a span-intersection scoper claims a narrower scope
6
- than the code applies. The body blocks when ANY line of the span is among the
7
- changed lines, so an edit touching a non-anchor line of the span still blocks —
8
- contradicting the single-line Args sentence. This is the deterministic slice of
9
- Category O6 (free-form docstring-vs-implementation drift) for an Args entry whose
10
- single-line scope claim disagrees with a span-intersection body.
1
+ """Span-scope tests for docstring checks.
2
+
3
+ Covers two surfaces:
4
+
5
+ 1. ``check_docstring_args_single_line_scope_vs_span`` (Category O6) an Args
6
+ entry that claims single-line (block-anchor) scope while the body builds a
7
+ multi-line span and routes it through span intersection.
8
+
9
+ 2. Changed-span grading for ``check_docstring_runon_sentence`` and
10
+ ``check_docstring_prose_wall_without_illustration`` (issue #237) far-away
11
+ no-op grandfathering, introduced-violation blocking, and
12
+ ``defer_scope_to_caller`` pass-through.
11
13
  """
12
14
 
13
15
  from __future__ import annotations
@@ -201,3 +203,223 @@ def test_hook_infrastructure_is_not_exempt() -> None:
201
203
  assert (
202
204
  len(check_docstring_args_single_line_scope_vs_span(content, HOOK_INFRASTRUCTURE_PATH)) == 1
203
205
  )
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Changed-span grading for the two whole-file docstring checks (issue #237 / P-41)
209
+ # check_docstring_runon_sentence and check_docstring_prose_wall_without_illustration
210
+ # ---------------------------------------------------------------------------
211
+
212
+
213
+ def check_docstring_runon_sentence(
214
+ content: str,
215
+ file_path: str,
216
+ all_changed_lines: set[int] | None = None,
217
+ defer_scope_to_caller: bool = False,
218
+ ) -> list[str]:
219
+ return code_rules_enforcer.check_docstring_runon_sentence(
220
+ content, file_path, all_changed_lines, defer_scope_to_caller
221
+ )
222
+
223
+
224
+ def check_docstring_prose_wall_without_illustration(
225
+ content: str,
226
+ file_path: str,
227
+ all_changed_lines: set[int] | None = None,
228
+ defer_scope_to_caller: bool = False,
229
+ ) -> list[str]:
230
+ return code_rules_enforcer.check_docstring_prose_wall_without_illustration(
231
+ content, file_path, all_changed_lines, defer_scope_to_caller
232
+ )
233
+
234
+
235
+ _CLEAN_HELPER_TAIL = (
236
+ "\n"
237
+ "def clean_helper() -> str:\n"
238
+ ' """Return a short status token for the board.\n'
239
+ "\n"
240
+ " Each call names one vessel and its final port.\n"
241
+ ' """\n'
242
+ ' return "ok"\n'
243
+ )
244
+
245
+
246
+ def _module_with_far_away_runon_and_clean_helper() -> str:
247
+ return (
248
+ '"""Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
249
+ "registration, and the interrupted-run finalizer — the non-promoter-specific\n"
250
+ "machinery that brackets a run so the JSONL artifact always carries a terminal\n"
251
+ "record and an in-flight theme record on interrupt.\n"
252
+ '"""\n'
253
+ + _CLEAN_HELPER_TAIL
254
+ )
255
+
256
+
257
+ def _module_with_far_away_prose_wall_and_clean_helper() -> str:
258
+ return (
259
+ '"""Assemble the nightly voyage tally from the harbor scans.\n'
260
+ "\n"
261
+ "A scan names one vessel and where it dropped anchor.\n"
262
+ "The tally walks the scans in arrival order and keeps that order.\n"
263
+ "A calm voyage ends well for every vessel it carried.\n"
264
+ "A halted voyage marks the vessel it was near when the storm arrived.\n"
265
+ "A wrecked voyage marks the vessel that sank and stops the walk there.\n"
266
+ "The tally groups the vessels by their final port for the harbor.\n"
267
+ "The harbor reads the tally and sees every arrival at a glance.\n"
268
+ '"""\n'
269
+ + _CLEAN_HELPER_TAIL
270
+ )
271
+
272
+
273
+ def test_runon_far_away_no_op_is_grandfathered() -> None:
274
+ content = _module_with_far_away_runon_and_clean_helper()
275
+ clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
276
+ all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
277
+ unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
278
+ assert any("run-on" in each for each in unscoped), (
279
+ f"control: unscoped must still see the far-away run-on, got {unscoped!r}"
280
+ )
281
+ scoped = check_docstring_runon_sentence(
282
+ content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
283
+ )
284
+ assert scoped == [], (
285
+ f"far-away run-on must be grandfathered on a clean-helper edit, got {scoped!r}"
286
+ )
287
+
288
+
289
+ def test_runon_introduced_violation_blocks() -> None:
290
+ content = _module_with_far_away_runon_and_clean_helper()
291
+ unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
292
+ assert any("run-on" in each for each in unscoped)
293
+ scoped = check_docstring_runon_sentence(
294
+ content, PRODUCTION_FILE_PATH, all_changed_lines={1}
295
+ )
296
+ assert any("run-on" in each for each in scoped), (
297
+ f"introduced/touched run-on must block, got {scoped!r}"
298
+ )
299
+
300
+
301
+ def test_prose_wall_far_away_no_op_is_grandfathered() -> None:
302
+ content = _module_with_far_away_prose_wall_and_clean_helper()
303
+ clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
304
+ all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
305
+ unscoped = check_docstring_prose_wall_without_illustration(
306
+ content, PRODUCTION_FILE_PATH
307
+ )
308
+ assert any("worked example" in each for each in unscoped), (
309
+ f"control: unscoped must still see the far-away wall, got {unscoped!r}"
310
+ )
311
+ scoped = check_docstring_prose_wall_without_illustration(
312
+ content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
313
+ )
314
+ assert scoped == [], (
315
+ f"far-away wall must be grandfathered on a clean-helper edit, got {scoped!r}"
316
+ )
317
+
318
+
319
+ def test_prose_wall_introduced_violation_blocks() -> None:
320
+ content = _module_with_far_away_prose_wall_and_clean_helper()
321
+ unscoped = check_docstring_prose_wall_without_illustration(
322
+ content, PRODUCTION_FILE_PATH
323
+ )
324
+ assert any("worked example" in each for each in unscoped)
325
+ scoped = check_docstring_prose_wall_without_illustration(
326
+ content, PRODUCTION_FILE_PATH, all_changed_lines={1}
327
+ )
328
+ assert any("worked example" in each for each in scoped), (
329
+ f"introduced/touched wall must block, got {scoped!r}"
330
+ )
331
+
332
+
333
+ def test_runon_defer_scope_returns_all_violations() -> None:
334
+ content = _module_with_far_away_runon_and_clean_helper()
335
+ deferred = check_docstring_runon_sentence(
336
+ content,
337
+ PRODUCTION_FILE_PATH,
338
+ all_changed_lines={999},
339
+ defer_scope_to_caller=True,
340
+ )
341
+ assert any("run-on" in each for each in deferred)
342
+
343
+
344
+ def _function_with_runon_docstring() -> str:
345
+ return (
346
+ "def clean_helper() -> str:\n"
347
+ ' """Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
348
+ " registration, and the interrupted-run finalizer — the non-promoter-specific\n"
349
+ " machinery that brackets a run so the JSONL artifact always carries a terminal\n"
350
+ " record and an in-flight theme record on interrupt.\n"
351
+ ' """\n'
352
+ ' return "ok"\n'
353
+ )
354
+
355
+
356
+ def test_runon_docstring_body_edit_blocks_without_def_line() -> None:
357
+ content = _function_with_runon_docstring()
358
+ unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
359
+ assert any("run-on" in each for each in unscoped), (
360
+ f"control: unscoped must see the function run-on, got {unscoped!r}"
361
+ )
362
+ docstring_body_lines = {2, 3, 4, 5}
363
+ scoped = check_docstring_runon_sentence(
364
+ content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
365
+ )
366
+ assert any("run-on" in each for each in scoped), (
367
+ f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
368
+ )
369
+
370
+
371
+ def test_runon_post_docstring_body_edit_is_grandfathered() -> None:
372
+ content = _function_with_runon_docstring()
373
+ return_line = content.splitlines().index(' return "ok"') + 1
374
+ scoped = check_docstring_runon_sentence(
375
+ content, PRODUCTION_FILE_PATH, all_changed_lines={return_line}
376
+ )
377
+ assert scoped == [], (
378
+ f"post-docstring body edit must grandfather the pre-existing run-on, got {scoped!r}"
379
+ )
380
+
381
+
382
+ def _function_with_prose_wall_docstring() -> str:
383
+ return (
384
+ "def clean_helper() -> str:\n"
385
+ ' """Assemble the nightly voyage tally from the harbor scans.\n'
386
+ "\n"
387
+ " A scan names one vessel and where it dropped anchor.\n"
388
+ " The tally walks the scans in arrival order and keeps that order.\n"
389
+ " A calm voyage ends well for every vessel it carried.\n"
390
+ " A halted voyage marks the vessel it was near when the storm arrived.\n"
391
+ " A wrecked voyage marks the vessel that sank and stops the walk there.\n"
392
+ " The tally groups the vessels by their final port for the harbor.\n"
393
+ " The harbor reads the tally and sees every arrival at a glance.\n"
394
+ ' """\n'
395
+ ' return "ok"\n'
396
+ )
397
+
398
+
399
+ def test_prose_wall_docstring_body_edit_blocks_without_def_line() -> None:
400
+ content = _function_with_prose_wall_docstring()
401
+ unscoped = check_docstring_prose_wall_without_illustration(
402
+ content, PRODUCTION_FILE_PATH
403
+ )
404
+ assert any("worked example" in each for each in unscoped), (
405
+ f"control: unscoped must see the function wall, got {unscoped!r}"
406
+ )
407
+ docstring_body_lines = set(range(2, 12))
408
+ scoped = check_docstring_prose_wall_without_illustration(
409
+ content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
410
+ )
411
+ assert any("worked example" in each for each in scoped), (
412
+ f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
413
+ )
414
+
415
+
416
+ def test_prose_wall_defer_scope_returns_all_violations() -> None:
417
+ content = _module_with_far_away_prose_wall_and_clean_helper()
418
+ deferred = check_docstring_prose_wall_without_illustration(
419
+ content,
420
+ PRODUCTION_FILE_PATH,
421
+ all_changed_lines={999},
422
+ defer_scope_to_caller=True,
423
+ )
424
+ assert any("worked example" in each for each in deferred)
425
+
@@ -186,7 +186,7 @@ def test_should_return_false_for_pytest_tmp_path_when_job_dir_elsewhere(
186
186
  temp directory was an ephemeral source: pytest tmp_path fixtures live
187
187
  under tempfile.gettempdir(), so the enforcer's own test targets must not
188
188
  classify as ephemeral. CLAUDE_JOB_DIR points elsewhere (its scratch dir
189
- lives under .claude-ev/jobs, not %TEMP%).
189
+ lives under .claude-profile-c/jobs, not %TEMP%).
190
190
  """
191
191
  monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
192
192
  monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path / "elsewhere"))
@@ -65,3 +65,44 @@ def test_should_report_join_separator_line_number() -> None:
65
65
  assert any("Line 2" in each_issue for each_issue in issues), (
66
66
  f"Expected line 2 reported, got: {issues}"
67
67
  )
68
+
69
+
70
+ def test_should_grandfather_unchanged_join_separator_in_wide_edit() -> None:
71
+ prior_fragment = (
72
+ "def render(all_paths: list) -> str:\n"
73
+ " return ', '.join(str(each_path) for each_path in all_paths)\n"
74
+ )
75
+ proposed_fragment = (
76
+ "def render(all_paths: list) -> str:\n"
77
+ " # keep the join separator the same\n"
78
+ " return ', '.join(str(each_path) for each_path in all_paths)\n"
79
+ )
80
+ issues = code_rules_enforcer.validate_content(
81
+ proposed_fragment,
82
+ PRODUCTION_FILE_PATH,
83
+ prior_fragment,
84
+ )
85
+ join_issues = [each for each in issues if "join" in each]
86
+ assert join_issues == [], (
87
+ f"unchanged enclosed join separator must be grandfathered, got: {join_issues}"
88
+ )
89
+
90
+
91
+ def test_should_block_new_join_separator_in_wide_edit() -> None:
92
+ prior_fragment = (
93
+ "def render(all_paths: list) -> str:\n"
94
+ " return JOIN_DELIMITER.join(all_paths)\n"
95
+ )
96
+ proposed_fragment = (
97
+ "def render(all_paths: list) -> str:\n"
98
+ " return ', '.join(all_paths)\n"
99
+ )
100
+ issues = code_rules_enforcer.validate_content(
101
+ proposed_fragment,
102
+ PRODUCTION_FILE_PATH,
103
+ prior_fragment,
104
+ )
105
+ join_issues = [each for each in issues if "join" in each]
106
+ assert any("Line 2" in each_issue for each_issue in join_issues), (
107
+ f"introduced join separator must block at its new location, got: {join_issues}"
108
+ )
@@ -232,3 +232,101 @@ def test_should_still_flag_literal_in_nested_function_body() -> None:
232
232
  assert len(issues) == 1, (
233
233
  f"Inner literal must be flagged exactly once (no duplicate from outer walk), got: {issues}"
234
234
  )
235
+
236
+
237
+ def test_should_grandfather_unchanged_string_magic_in_wide_edit() -> None:
238
+ prior_fragment = (
239
+ "import os\n"
240
+ "\n"
241
+ "def fetch_secret() -> str:\n"
242
+ " return os.environ['STRIPE_SECRET']\n"
243
+ )
244
+ proposed_fragment = (
245
+ "import os\n"
246
+ "\n"
247
+ "def fetch_secret() -> str:\n"
248
+ " # wide edit that leaves the magic string in place\n"
249
+ " return os.environ['STRIPE_SECRET']\n"
250
+ )
251
+ issues = code_rules_enforcer.validate_content(
252
+ proposed_fragment,
253
+ PRODUCTION_FILE_PATH,
254
+ prior_fragment,
255
+ )
256
+ magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
257
+ assert magic_issues == [], (
258
+ f"unchanged enclosed string magic must be grandfathered, got: {magic_issues}"
259
+ )
260
+
261
+
262
+ def test_should_block_new_string_magic_in_wide_edit() -> None:
263
+ prior_fragment = (
264
+ "import os\n"
265
+ "\n"
266
+ "def fetch_secret() -> str:\n"
267
+ " return os.environ.get('missing', '')\n"
268
+ )
269
+ proposed_fragment = (
270
+ "import os\n"
271
+ "\n"
272
+ "def fetch_secret() -> str:\n"
273
+ " return os.environ['STRIPE_SECRET']\n"
274
+ )
275
+ issues = code_rules_enforcer.validate_content(
276
+ proposed_fragment,
277
+ PRODUCTION_FILE_PATH,
278
+ prior_fragment,
279
+ )
280
+ magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
281
+ assert any("Line " in each_issue for each_issue in magic_issues), (
282
+ f"introduced string magic must block with a line locator, got: {magic_issues}"
283
+ )
284
+ assert any("STRIPE_SECRET" in each_issue for each_issue in magic_issues)
285
+
286
+
287
+ def test_should_block_second_identical_string_magic_per_occurrence() -> None:
288
+ prior_fragment = (
289
+ "import os\n"
290
+ "\n"
291
+ "def fetch_secret() -> str:\n"
292
+ " return os.environ['STRIPE_SECRET']\n"
293
+ )
294
+ proposed_fragment = (
295
+ "import os\n"
296
+ "\n"
297
+ "def fetch_secret() -> str:\n"
298
+ " return os.environ['STRIPE_SECRET']\n"
299
+ "\n"
300
+ "def fetch_backup() -> str:\n"
301
+ " return os.environ['STRIPE_SECRET']\n"
302
+ )
303
+ issues = code_rules_enforcer.validate_content(
304
+ proposed_fragment,
305
+ PRODUCTION_FILE_PATH,
306
+ prior_fragment,
307
+ )
308
+ magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
309
+ assert len(magic_issues) == 1, (
310
+ f"exactly one new identical magic must block (per-occurrence), got: {magic_issues}"
311
+ )
312
+ assert any("Line 7" in each_issue for each_issue in magic_issues), (
313
+ f"second occurrence must keep proposed-fragment line, got: {magic_issues}"
314
+ )
315
+
316
+
317
+ def test_should_surface_magic_when_gate_defers_scope_with_head_as_old() -> None:
318
+ source = (
319
+ "def compute() -> int:\n"
320
+ " old_number = 9999\n"
321
+ " return old_number\n"
322
+ )
323
+ issues = code_rules_enforcer.validate_content(
324
+ source,
325
+ PRODUCTION_FILE_PATH,
326
+ source,
327
+ defer_scope_to_caller=True,
328
+ )
329
+ assert any("9999" in each_issue for each_issue in issues), (
330
+ f"gate path must still surface whole-file magic when old_content equals content, "
331
+ f"got: {issues}"
332
+ )