claude-dev-env 2.9.0 → 2.11.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 (318) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/advisor/CLAUDE.md +3 -2
  3. package/_shared/advisor/advisor-protocol.md +74 -108
  4. package/_shared/advisor/reference/advisor-block.md +37 -0
  5. package/_shared/advisor/reference/cli-chain.md +45 -0
  6. package/_shared/advisor/reference/consult-format.md +41 -0
  7. package/_shared/advisor/reference/lifecycle.md +21 -0
  8. package/_shared/advisor/reference/sol-rung.md +31 -0
  9. package/_shared/advisor/reference/spawn-walk-log.md +31 -0
  10. package/_shared/advisor/reference/third-party-bind.md +30 -0
  11. package/_shared/advisor/reference/warm-up.md +33 -0
  12. package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
  13. package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
  14. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
  15. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
  16. package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
  17. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
  18. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
  19. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
  20. package/_shared/advisor/scripts/tier_model_ids.py +24 -0
  21. package/_shared/pr-loop/CLAUDE.md +1 -1
  22. package/_shared/pr-loop/audit-contract.md +17 -6
  23. package/_shared/pr-loop/audit-reply-template.md +4 -4
  24. package/_shared/pr-loop/code-rules-gate.md +3 -5
  25. package/_shared/pr-loop/fix-protocol.md +2 -3
  26. package/_shared/pr-loop/gh-payloads.md +1 -1
  27. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  28. package/_shared/pr-loop/scripts/README.md +1 -1
  29. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  32. package/_shared/pr-loop/scripts/preflight.py +9 -4
  33. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  34. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  35. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  36. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  37. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  38. package/_shared/pr-loop/state-schema.md +5 -14
  39. package/agents/CLAUDE.md +2 -2
  40. package/agents/clean-coder.md +58 -548
  41. package/agents/code-quality-agent.md +10 -2
  42. package/agents/code-verifier.md +1 -1
  43. package/agents/test_agent_frontmatter.py +32 -40
  44. package/audit-rubrics/CLAUDE.md +2 -1
  45. package/audit-rubrics/audit-categories.json +704 -0
  46. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  47. package/bin/CLAUDE.md +16 -5
  48. package/bin/ever-shipped-skills.mjs +2 -0
  49. package/bin/install-plan.mjs +402 -0
  50. package/bin/install-transaction.mjs +455 -0
  51. package/bin/install.mjs +593 -147
  52. package/bin/install.plan.test.mjs +194 -0
  53. package/bin/install.profile-root.test.mjs +154 -0
  54. package/bin/install.profiles.test.mjs +253 -0
  55. package/bin/install.settings-defaults.test.mjs +200 -0
  56. package/bin/install.transaction.test.mjs +400 -0
  57. package/bin/install.uninstall-transaction.test.mjs +418 -0
  58. package/bin/merge_managed_permissions.mjs +130 -0
  59. package/bin/resolve-install-root.mjs +181 -0
  60. package/bin/select-install-targets.mjs +401 -0
  61. package/commands/CLAUDE.md +0 -2
  62. package/docs/references/CLAUDE.md +3 -1
  63. package/docs/references/advisor-tool.md +25 -7
  64. package/docs/references/prose-style-enforcement.md +25 -0
  65. package/docs/references/team-advisor-skill.md +3 -3
  66. package/docs/references/weak-executor-advisor.md +91 -0
  67. package/hooks/blocking/CLAUDE.md +6 -6
  68. package/hooks/blocking/_path_setup.py +9 -5
  69. package/hooks/blocking/code_rules_docstrings.py +124 -30
  70. package/hooks/blocking/code_rules_enforcer.py +161 -16
  71. package/hooks/blocking/code_rules_shared.py +40 -23
  72. package/hooks/blocking/config/CLAUDE.md +3 -5
  73. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  74. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  75. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  76. package/hooks/blocking/hedging_language_blocker.py +103 -20
  77. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  78. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  79. package/hooks/blocking/plain_language_blocker.py +139 -20
  80. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  81. package/hooks/blocking/state_description_blocker.py +7 -1
  82. package/hooks/blocking/tdd_enforcer.py +8 -0
  83. package/hooks/blocking/test__path_setup.py +28 -0
  84. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  85. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  86. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  87. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  88. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  89. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  90. package/hooks/blocking/test_fable_spawn_gate.py +18 -11
  91. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  92. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  93. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  94. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  95. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  96. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  97. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  98. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  99. package/hooks/blocking/test_state_description_blocker.py +45 -2
  100. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  101. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  102. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  103. package/hooks/hooks.json +15 -0
  104. package/hooks/hooks_constants/CLAUDE.md +14 -3
  105. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  106. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  107. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  108. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  109. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  110. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  111. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  112. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  113. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  114. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  115. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  116. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  117. package/hooks/hooks_constants/session_start_injector.py +163 -0
  118. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  119. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  120. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  121. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  122. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  123. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  124. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  125. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  126. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  127. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  128. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  129. package/hooks/observability/CLAUDE.md +2 -0
  130. package/hooks/observability/prose_matcher_advisory.py +237 -0
  131. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  132. package/hooks/session/CLAUDE.md +9 -1
  133. package/hooks/session/_path_setup.py +13 -0
  134. package/hooks/session/issue_tracker_session_starter.py +135 -0
  135. package/hooks/session/orchestrator_auto_starter.py +100 -0
  136. package/hooks/session/test__path_setup.py +28 -0
  137. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  138. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  139. package/hooks/session/test_working_style_prompt.py +47 -0
  140. package/hooks/session/untracked_repo_detector.py +1 -24
  141. package/hooks/session/working_style_prompt.py +36 -0
  142. package/hooks/validators/_path_setup.py +19 -0
  143. package/hooks/validators/run_all_validators.py +8 -13
  144. package/installable-surfaces.manifest.json +21 -0
  145. package/output-styles/CLAUDE.md +1 -3
  146. package/package.json +4 -2
  147. package/rules/CLAUDE.md +1 -0
  148. package/rules/durable-post-artifacts.md +2 -2
  149. package/rules/eli11-replies.md +6 -1
  150. package/rules/hedging-claims.md +4 -2
  151. package/rules/long-horizon-autonomy.md +3 -1
  152. package/rules/opus5-communication-contract.md +45 -0
  153. package/rules/plain-language.md +2 -2
  154. package/rules/research-mode.md +1 -1
  155. package/scripts/CLAUDE.md +11 -0
  156. package/scripts/Sync-RepoMain.ps1 +215 -0
  157. package/scripts/active_capability_references.py +218 -0
  158. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  159. package/scripts/claude_chain_runner.py +394 -6
  160. package/scripts/claude_chain_usage.py +1 -1
  161. package/scripts/codex_compat_materializer.py +105 -85
  162. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
  163. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  164. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  165. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  166. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  167. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  168. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  169. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  170. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  171. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  172. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  173. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  174. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  175. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  176. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  177. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  178. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  179. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  180. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  181. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  182. package/scripts/spawn_grok_batch.py +3 -0
  183. package/scripts/test_active_capability_references.py +108 -0
  184. package/scripts/test_claude_chain_runner.py +414 -82
  185. package/scripts/test_claude_chain_usage.py +12 -12
  186. package/scripts/test_resolve_worker_spawn.py +2 -2
  187. package/scripts/test_verify_installable_package.py +208 -0
  188. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  189. package/scripts/verify_installable_package.py +612 -0
  190. package/settings.json +10 -0
  191. package/skills/CLAUDE.md +2 -0
  192. package/skills/_shared/advisor/CLAUDE.md +1 -1
  193. package/skills/_shared/advisor/scripts/README.md +2 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  195. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  196. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  199. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  200. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  201. package/skills/autoconverge/reference/convergence.md +2 -1
  202. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  203. package/skills/beat-sheet/SKILL.md +54 -0
  204. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  205. package/skills/bugteam/CONSTRAINTS.md +4 -4
  206. package/skills/bugteam/EXAMPLES.md +1 -1
  207. package/skills/bugteam/reference/README.md +1 -1
  208. package/skills/e-code-review/SKILL.md +26 -5
  209. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  210. package/skills/e-code-review/reference/medium.md +15 -4
  211. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  212. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  213. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  214. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  215. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  216. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  217. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  218. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  219. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  220. package/skills/e-code-review/scripts/grok_code_review.py +16 -7
  221. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  222. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  223. package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
  224. package/skills/grokify/SKILL.md +1 -1
  225. package/skills/grokify/templates/handoff-template.md +2 -2
  226. package/skills/orchestrator/SKILL.md +5 -4
  227. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  228. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  229. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  230. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  231. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  232. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  233. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  234. package/skills/pr-converge/scripts/conftest.py +35 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  237. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  238. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  239. package/skills/rebase/SKILL.md +15 -3
  240. package/skills/reviewer-gates/SKILL.md +2 -2
  241. package/skills/show/SKILL.md +51 -0
  242. package/skills/show/references/accessibility.md +7 -0
  243. package/skills/show/references/art.md +3 -0
  244. package/skills/show/references/charts.md +3 -0
  245. package/skills/show/references/core-design.md +14 -0
  246. package/skills/show/references/erds.md +3 -0
  247. package/skills/show/references/flowcharts.md +3 -0
  248. package/skills/show/references/host-and-html.md +3 -0
  249. package/skills/show/references/illustrative-diagrams.md +10 -0
  250. package/skills/show/references/interaction.md +3 -0
  251. package/skills/show/references/mockups.md +3 -0
  252. package/skills/show/references/quality-gates.md +7 -0
  253. package/skills/show/references/structural-diagrams.md +3 -0
  254. package/skills/show/references/subject-inventory.md +21 -0
  255. package/skills/show/references/svg-contract.md +22 -0
  256. package/skills/show/routing.yaml +30 -0
  257. package/skills/show/samples/pr1262-v2.svg +222 -0
  258. package/skills/show/scripts/README.md +6 -0
  259. package/skills/show/scripts/validate-artifact.py +91 -0
  260. package/skills/show/scripts/validate-package.py +18 -0
  261. package/skills/show/templates/html-widget.html +4 -0
  262. package/skills/show/templates/svg-base.svg +19 -0
  263. package/skills/show/tests/fixtures/css-var.svg +6 -0
  264. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  265. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  266. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  267. package/skills/show/tests/fixtures/invalid.svg +1 -0
  268. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  269. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  270. package/skills/show/tests/fixtures/valid.html +1 -0
  271. package/skills/show/tests/test_validate-artifact.py +74 -0
  272. package/skills/show/tests/test_validators.py +59 -0
  273. package/skills/show/workflows/create-visual.md +13 -0
  274. package/skills/show/workflows/review-visual.md +20 -0
  275. package/skills/split-pr/SKILL.md +85 -0
  276. package/skills/split-pr/reference/path-layers.md +16 -0
  277. package/skills/split-pr/reference/proposal-format.md +15 -0
  278. package/skills/split-pr/reference/split-further-loop.md +10 -0
  279. package/skills/split-pr/reference/splitting-principles.md +26 -0
  280. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  281. package/skills/split-pr/scripts/categorize_files.py +106 -0
  282. package/skills/split-pr/scripts/config/__init__.py +1 -0
  283. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  284. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  285. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  286. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  287. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  288. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  289. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  290. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  291. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  292. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  293. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  294. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  295. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  296. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  297. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  298. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  299. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  300. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  301. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  302. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  303. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  304. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  305. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  306. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  307. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  308. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  309. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  310. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  311. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  312. package/skills/split-pr/scripts/verify_plan.py +139 -0
  313. package/skills/team-advisor/SKILL.md +7 -4
  314. package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
  315. package/system-prompts/software-engineer.xml +11 -2
  316. package/commands/initialize.md +0 -90
  317. package/commands/stubcheck.md +0 -88
  318. package/output-styles/caveman-agent.md +0 -37
@@ -13,14 +13,22 @@ equivalence holds across both outcomes.
13
13
  from __future__ import annotations
14
14
 
15
15
  import json
16
+ import os
16
17
  import subprocess
17
18
  import sys
18
19
  from pathlib import Path
20
+ from unittest.mock import patch
21
+
22
+ import pytest
19
23
 
20
24
  _HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
21
25
  if _HOOKS_DIR not in sys.path:
22
26
  sys.path.insert(0, _HOOKS_DIR)
23
27
 
28
+ _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT = {
29
+ "CLAUDE_PROSE_STYLE_ENFORCEMENT": "1",
30
+ }
31
+
24
32
  from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402, I001
25
33
  DENY_DECISION,
26
34
  EDIT_TOOL_NAME,
@@ -43,6 +51,19 @@ _STATE_DESCRIPTION_DENY_CONTENT = "# Guide\n\nPreviously the system used port 80
43
51
  _PLAIN_LANGUAGE_ALLOW_CONTENT = "# Guide\n\nStart the build to make the report.\n"
44
52
  _PLAIN_LANGUAGE_DENY_CONTENT = "# Guide\n\nUtilize this to commence the process.\n"
45
53
 
54
+ _PROSE_STYLE_ENV_VAR = "CLAUDE_PROSE_STYLE_ENFORCEMENT"
55
+ _PROSE_STYLE_ENV_VALUE = "1"
56
+
57
+ @pytest.fixture(autouse=True)
58
+ def enable_prose_style_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
59
+ """Opinionated prose gates are default-off; native-equivalence tests arm them."""
60
+ monkeypatch.setenv(_PROSE_STYLE_ENV_VAR, _PROSE_STYLE_ENV_VALUE)
61
+
62
+ def _subprocess_environment() -> dict[str, str]:
63
+ """Return process env with opinionated prose gates enabled."""
64
+ environment_by_key = os.environ.copy()
65
+ environment_by_key[_PROSE_STYLE_ENV_VAR] = _PROSE_STYLE_ENV_VALUE
66
+ return environment_by_key
46
67
 
47
68
  def _write_payload_dictionary(file_path: str, content: str) -> dict[str, object]:
48
69
  """Build a Write tool payload dict.
@@ -59,7 +80,6 @@ def _write_payload_dictionary(file_path: str, content: str) -> dict[str, object]
59
80
  "tool_input": {"file_path": file_path, "content": content},
60
81
  }
61
82
 
62
-
63
83
  def _edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, object]:
64
84
  """Build an Edit tool payload dict.
65
85
 
@@ -79,7 +99,6 @@ def _edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, objec
79
99
  },
80
100
  }
81
101
 
82
-
83
102
  def _multi_edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, object]:
84
103
  """Build a MultiEdit tool payload dict.
85
104
 
@@ -99,6 +118,14 @@ def _multi_edit_payload_dictionary(file_path: str, new_string: str) -> dict[str,
99
118
  }
100
119
 
101
120
 
121
+ def _evaluate_with_prose_style_opt_in(
122
+ evaluate_callable: object, payload_dictionary: dict[str, object]
123
+ ) -> str | None:
124
+ """Call a native evaluate with opinionated prose enforcement enabled."""
125
+ with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
126
+ return evaluate_callable(payload_dictionary) # type: ignore[operator]
127
+
128
+
102
129
  def _run_script_subprocess(script_path: str, payload_dictionary: dict[str, object]) -> str:
103
130
  """Run a hook script as a subprocess and return its stripped stdout.
104
131
 
@@ -116,10 +143,10 @@ def _run_script_subprocess(script_path: str, payload_dictionary: dict[str, objec
116
143
  capture_output=True,
117
144
  text=True,
118
145
  encoding="utf-8",
146
+ env=_subprocess_environment(),
119
147
  )
120
148
  return completed_process.stdout.strip()
121
149
 
122
-
123
150
  def _deny_reason_from_script_stdout(stdout_text: str) -> str | None:
124
151
  """Parse a script's stdout into its deny-reason text, or None for allow.
125
152
 
@@ -139,7 +166,6 @@ def _deny_reason_from_script_stdout(stdout_text: str) -> str | None:
139
166
  reason_text = hook_specific.get("permissionDecisionReason", "")
140
167
  return reason_text if isinstance(reason_text, str) else None
141
168
 
142
-
143
169
  def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str | None:
144
170
  """Run the dispatcher as a subprocess and return its deny-reason text.
145
171
 
@@ -157,10 +183,10 @@ def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str |
157
183
  capture_output=True,
158
184
  text=True,
159
185
  encoding="utf-8",
186
+ env=_subprocess_environment(),
160
187
  )
161
188
  return _deny_reason_from_script_stdout(completed_process.stdout.strip())
162
189
 
163
-
164
190
  def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict[str, object]:
165
191
  """Run the dispatcher as a subprocess and return its parsed deny payload.
166
192
 
@@ -177,145 +203,157 @@ def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict
177
203
  capture_output=True,
178
204
  text=True,
179
205
  encoding="utf-8",
206
+ env=_subprocess_environment(),
180
207
  )
181
208
  parsed_payload = json.loads(completed_process.stdout.strip())
182
209
  assert isinstance(parsed_payload, dict)
183
210
  return parsed_payload
184
211
 
185
-
186
212
  def test_state_description_native_allows_match_script() -> None:
187
213
  """state_description_blocker native allow matches the script's allow."""
188
214
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_ALLOW_CONTENT)
189
- native_reason = state_description_blocker.evaluate(payload_dictionary)
215
+ native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
190
216
  script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
191
217
  script_reason = _deny_reason_from_script_stdout(script_stdout)
192
218
  assert native_reason is None
193
219
  assert script_reason is None
194
220
 
195
-
196
221
  def test_state_description_native_deny_matches_script_reason() -> None:
197
222
  """state_description_blocker native deny reason matches the script's reason."""
198
223
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
199
- native_reason = state_description_blocker.evaluate(payload_dictionary)
224
+ native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
200
225
  script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
201
226
  script_reason = _deny_reason_from_script_stdout(script_stdout)
202
227
  assert native_reason is not None
203
228
  assert native_reason == script_reason
204
229
 
205
-
206
230
  def test_state_description_native_edit_deny_matches_script_reason() -> None:
207
231
  """state_description_blocker native and script agree on an Edit denial."""
208
232
  payload_dictionary = _edit_payload_dictionary(
209
233
  _MARKDOWN_PATH, "Previously this used the old client.\n"
210
234
  )
211
- native_reason = state_description_blocker.evaluate(payload_dictionary)
235
+ native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
212
236
  script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
213
237
  script_reason = _deny_reason_from_script_stdout(script_stdout)
214
238
  assert native_reason is not None
215
239
  assert native_reason == script_reason
216
240
 
217
-
218
241
  def test_state_description_native_non_target_tool_allows_match_script() -> None:
219
242
  """state_description_blocker native allows MultiEdit, matching the script."""
220
243
  payload_dictionary = _multi_edit_payload_dictionary(
221
244
  _MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT
222
245
  )
223
- native_reason = state_description_blocker.evaluate(payload_dictionary)
246
+ native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
224
247
  script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
225
248
  script_reason = _deny_reason_from_script_stdout(script_stdout)
226
249
  assert native_reason is None
227
250
  assert script_reason is None
228
251
 
229
-
230
252
  def test_plain_language_native_allows_match_script() -> None:
231
253
  """plain_language_blocker native allow matches the script's allow."""
232
254
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_ALLOW_CONTENT)
233
- native_reason = plain_language_blocker.evaluate(payload_dictionary)
255
+ native_reason = _evaluate_with_prose_style_opt_in(plain_language_blocker.evaluate, payload_dictionary)
234
256
  script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
235
257
  script_reason = _deny_reason_from_script_stdout(script_stdout)
236
258
  assert native_reason is None
237
259
  assert script_reason is None
238
260
 
239
261
 
240
- def test_plain_language_native_deny_matches_script_reason() -> None:
241
- """plain_language_blocker native deny reason matches the script's reason."""
262
+ def test_plain_language_native_heavy_word_advisory_matches_script() -> None:
263
+ """plain_language_blocker native and script agree: no deny, allow advisory."""
242
264
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
243
- native_reason = plain_language_blocker.evaluate(payload_dictionary)
265
+ with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
266
+ native_deny, native_advisory = plain_language_blocker.evaluate_with_advisory(
267
+ payload_dictionary
268
+ )
244
269
  script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
245
270
  script_reason = _deny_reason_from_script_stdout(script_stdout)
246
- assert native_reason is not None
247
- assert native_reason == script_reason
271
+ assert native_deny is None
272
+ assert script_reason is None
273
+ assert native_advisory is not None
274
+ assert "utilize" in native_advisory.lower() or "commence" in native_advisory.lower()
275
+ script_payload = json.loads(script_stdout)
276
+ assert script_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
277
+ assert native_advisory in script_payload["systemMessage"]
248
278
 
249
279
 
250
- def test_plain_language_native_multi_edit_deny_matches_script_reason() -> None:
251
- """plain_language_blocker native and script agree on a MultiEdit denial."""
280
+ def test_plain_language_native_multi_edit_heavy_word_advisory_matches_script() -> None:
281
+ """plain_language_blocker native and script agree on MultiEdit heavy-word allow."""
252
282
  payload_dictionary = _multi_edit_payload_dictionary(
253
283
  _MARKDOWN_PATH, "Utilize this to commence the process.\n"
254
284
  )
255
- native_reason = plain_language_blocker.evaluate(payload_dictionary)
285
+ with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
286
+ native_deny, native_advisory = plain_language_blocker.evaluate_with_advisory(
287
+ payload_dictionary
288
+ )
256
289
  script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
257
290
  script_reason = _deny_reason_from_script_stdout(script_stdout)
258
- assert native_reason is not None
259
- assert native_reason == script_reason
260
-
291
+ assert native_deny is None
292
+ assert script_reason is None
293
+ assert native_advisory is not None
294
+ script_payload = json.loads(script_stdout)
295
+ assert script_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
296
+ assert native_advisory in script_payload["systemMessage"]
261
297
 
262
298
  def test_plain_language_native_non_markdown_allows_match_script() -> None:
263
299
  """plain_language_blocker native allows a non-markdown Write, matching the script."""
264
300
  payload_dictionary = _write_payload_dictionary(_PYTHON_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
265
- native_reason = plain_language_blocker.evaluate(payload_dictionary)
301
+ native_reason = _evaluate_with_prose_style_opt_in(plain_language_blocker.evaluate, payload_dictionary)
266
302
  script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
267
303
  script_reason = _deny_reason_from_script_stdout(script_stdout)
268
304
  assert native_reason is None
269
305
  assert script_reason is None
270
306
 
271
-
272
307
  def test_dispatcher_native_path_denies_state_description() -> None:
273
308
  """The dispatcher's native path denies a state_description_blocker violation."""
274
309
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
275
- native_reason = state_description_blocker.evaluate(payload_dictionary)
310
+ native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
276
311
  dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
277
312
  assert native_reason is not None
278
313
  assert dispatcher_reason is not None
279
314
  assert native_reason in dispatcher_reason
280
315
 
281
316
 
282
- def test_dispatcher_native_path_denies_plain_language() -> None:
283
- """The dispatcher's native path denies a plain_language_blocker violation."""
317
+ def test_dispatcher_native_path_allows_plain_language_heavy_word() -> None:
318
+ """The dispatcher's native path allows heavy words and does not deny."""
284
319
  payload_dictionary = _multi_edit_payload_dictionary(
285
320
  _MARKDOWN_PATH, "Utilize this to commence the process.\n"
286
321
  )
287
- native_reason = plain_language_blocker.evaluate(payload_dictionary)
322
+ native_reason = _evaluate_with_prose_style_opt_in(
323
+ plain_language_blocker.evaluate, payload_dictionary
324
+ )
288
325
  dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
289
- assert native_reason is not None
290
- assert dispatcher_reason is not None
291
- assert native_reason in dispatcher_reason
326
+ assert native_reason is None
327
+ assert dispatcher_reason is None
292
328
 
293
329
 
294
- def test_dispatcher_native_plain_language_carries_system_message() -> None:
295
- """The dispatcher's plain-language deny carries the standalone systemMessage."""
330
+ def test_dispatcher_native_plain_language_carries_advisory_system_message() -> None:
331
+ """The dispatcher's plain-language allow carries the advisory systemMessage."""
296
332
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
297
- deny_reason = plain_language_blocker.evaluate(payload_dictionary)
298
- assert deny_reason is not None
299
- standalone_payload = plain_language_blocker.build_deny_payload(deny_reason)
300
- expected_system_message = standalone_payload["systemMessage"]
301
- assert isinstance(expected_system_message, str)
333
+ with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
334
+ deny_reason, advisory_message = plain_language_blocker.evaluate_with_advisory(
335
+ payload_dictionary
336
+ )
337
+ assert deny_reason is None
338
+ assert advisory_message is not None
302
339
  dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
340
+ assert dispatcher_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
303
341
  dispatcher_system_message = dispatcher_payload.get("systemMessage")
304
342
  assert isinstance(dispatcher_system_message, str)
305
- assert expected_system_message in dispatcher_system_message
343
+ assert advisory_message in dispatcher_system_message
306
344
 
307
345
 
308
- def test_dispatcher_native_plain_language_carries_suppress_output() -> None:
309
- """The dispatcher's plain-language deny carries the standalone suppressOutput flag."""
346
+ def test_dispatcher_native_plain_language_allow_does_not_suppress_output() -> None:
347
+ """The dispatcher's plain-language advisory allow leaves suppressOutput off."""
310
348
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
311
349
  dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
312
- assert dispatcher_payload.get("suppressOutput") is True
313
-
350
+ assert dispatcher_payload.get("suppressOutput") is not True
351
+ assert dispatcher_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
314
352
 
315
353
  def test_dispatcher_native_state_description_carries_additional_context() -> None:
316
354
  """The dispatcher's state-description deny carries the standalone additionalContext."""
317
355
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
318
- deny_reason = state_description_blocker.evaluate(payload_dictionary)
356
+ deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
319
357
  assert deny_reason is not None
320
358
  standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
321
359
  standalone_hook_specific = standalone_payload["hookSpecificOutput"]
@@ -326,11 +364,10 @@ def test_dispatcher_native_state_description_carries_additional_context() -> Non
326
364
  assert isinstance(dispatcher_hook_specific, dict)
327
365
  assert dispatcher_hook_specific.get("additionalContext") == expected_additional_context
328
366
 
329
-
330
367
  def test_dispatcher_native_state_description_carries_system_message() -> None:
331
368
  """The dispatcher's state-description deny carries the standalone systemMessage."""
332
369
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
333
- deny_reason = state_description_blocker.evaluate(payload_dictionary)
370
+ deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
334
371
  assert deny_reason is not None
335
372
  standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
336
373
  expected_system_message = standalone_payload["systemMessage"]
@@ -19,8 +19,8 @@ if _HOOKS_ROOT not in sys.path:
19
19
  from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
20
20
  from state_description_blocker import ( # noqa: E402
21
21
  build_deny_payload,
22
- evaluate,
23
22
  )
23
+ from state_description_blocker import evaluate as _evaluate_without_opt_in # noqa: E402
24
24
 
25
25
  HOOK_SCRIPT_PATH = os.path.join(
26
26
  os.path.dirname(__file__), "state_description_blocker.py"
@@ -39,23 +39,66 @@ VIOLATION_MD_PREVIOUSLY = "# Config\n\nPreviously set via env var."
39
39
  VIOLATION_MD_NOW_USES = "# Auth\n\nNow uses OAuth2."
40
40
 
41
41
 
42
+ def evaluate(payload_by_key: dict[str, object]) -> str | None:
43
+ """Call evaluate with opinionated prose enforcement enabled."""
44
+ with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
45
+ return _evaluate_without_opt_in(payload_by_key)
46
+
47
+
42
48
  class _RunHook:
43
49
  """Helper to test the hook via subprocess."""
44
50
 
45
- def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
51
+ def __call__(
52
+ self,
53
+ tool_name: str,
54
+ tool_input: dict,
55
+ *,
56
+ is_prose_style_enabled: bool = True,
57
+ ) -> subprocess.CompletedProcess:
46
58
  payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
59
+ environment_by_key = os.environ.copy()
60
+ if is_prose_style_enabled:
61
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
62
+ else:
63
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
47
64
  return subprocess.run(
48
65
  [sys.executable, HOOK_SCRIPT_PATH],
49
66
  input=payload,
50
67
  capture_output=True,
51
68
  text=True,
52
69
  check=False,
70
+ env=environment_by_key,
53
71
  )
54
72
 
55
73
 
56
74
  _run_hook = _RunHook()
57
75
 
58
76
 
77
+ def test_historical_phrase_scan_is_default_off(
78
+ monkeypatch: pytest.MonkeyPatch,
79
+ ) -> None:
80
+ monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
81
+ result = _run_hook(
82
+ "Write",
83
+ {
84
+ "file_path": "src/main.py",
85
+ "content": VIOLATION_INSTEAD_OF_COMMENT,
86
+ },
87
+ is_prose_style_enabled=False,
88
+ )
89
+ assert result.returncode == 0
90
+ assert result.stdout == ""
91
+ assert _evaluate_without_opt_in(
92
+ {
93
+ "tool_name": "Write",
94
+ "tool_input": {
95
+ "file_path": "src/main.py",
96
+ "content": VIOLATION_INSTEAD_OF_COMMENT,
97
+ },
98
+ }
99
+ ) is None
100
+
101
+
59
102
  def test_block_clean_python_comment_passes():
60
103
  result = _run_hook(
61
104
  "Write",
@@ -156,9 +156,10 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
156
156
  payload_text = json.dumps(
157
157
  {
158
158
  "stop_hook_active": False,
159
- "last_assistant_message": "This is probably correct without a source.",
159
+ "last_assistant_message": "This is probably correct.",
160
160
  }
161
161
  )
162
+ environment_by_key = {**os.environ, "CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}
162
163
  completed = subprocess.run(
163
164
  [sys.executable, _DISPATCHER_SCRIPT],
164
165
  check=False,
@@ -166,6 +167,7 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
166
167
  capture_output=True,
167
168
  text=True,
168
169
  encoding="utf-8",
170
+ env=environment_by_key,
169
171
  )
170
172
  assert completed.returncode == 0
171
173
  parsed = json.loads(completed.stdout)
@@ -173,14 +175,16 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
173
175
  assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
174
176
 
175
177
 
176
- def test_dispatcher_blocks_overlong_reply_matching_standalone() -> None:
177
- """An overlong last_assistant_message blocks through the dispatcher."""
178
- overlong_message = "\n".join(
179
- " ".join(f"finding{each_index}" for each_index in range(10))
180
- for _ in range(30)
178
+ def test_dispatcher_blocks_eli11_shape_violation_matching_standalone() -> None:
179
+ """An ELI11 shape violation (too many bullets) blocks through the dispatcher."""
180
+ too_many_bullets_message = "\n".join(
181
+ f"- finding{each_index} detail for this line" for each_index in range(7)
181
182
  )
182
183
  payload_text = json.dumps(
183
- {"stop_hook_active": False, "last_assistant_message": overlong_message}
184
+ {
185
+ "stop_hook_active": False,
186
+ "last_assistant_message": too_many_bullets_message,
187
+ }
184
188
  )
185
189
  completed = subprocess.run(
186
190
  [sys.executable, _DISPATCHER_SCRIPT],
@@ -31,13 +31,13 @@ def _body_names_volatile_path(tool_name: str, tool_input: dict[str, object]) ->
31
31
 
32
32
 
33
33
  def test_scan_detects_job_scratch_path_backslash() -> None:
34
- text = r"See C:\Users\example\.claude-editor\jobs\95762cea\tmp\staging\contact_sheet.png"
35
- assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
34
+ text = r"See C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"
35
+ assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
36
36
 
37
37
 
38
38
  def test_scan_detects_job_scratch_path_forward_slash() -> None:
39
- text = "artifact at /home/user/.claude-editor/jobs/abc/tmp/out.png"
40
- assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
39
+ text = "artifact at /home/user/.claude-profile-a/jobs/abc/tmp/out.png"
40
+ assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
41
41
 
42
42
 
43
43
  def test_scan_detects_worktree_path() -> None:
@@ -92,12 +92,12 @@ def test_scan_home_anchored_worktree_path_is_detected() -> None:
92
92
 
93
93
 
94
94
  def test_scan_posix_absolute_job_scratch_path_is_detected() -> None:
95
- text = "/home/me/.claude-editor/jobs/j1/log.txt"
96
- assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
95
+ text = "/home/me/.claude-profile-a/jobs/j1/log.txt"
96
+ assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
97
97
 
98
98
 
99
99
  def test_scan_bare_parenthesized_job_scratch_mention_is_allowed() -> None:
100
- text = "(.claude-editor/jobs/) as a directory name in prose"
100
+ text = "(.claude-profile-a/jobs/) as a directory name in prose"
101
101
  assert scan_text_for_volatile_marker(text) is None
102
102
 
103
103
 
@@ -139,8 +139,8 @@ def test_scan_markdown_link_relative_worktree_target_is_detected() -> None:
139
139
 
140
140
 
141
141
  def test_scan_space_prefixed_relative_job_scratch_path_is_detected() -> None:
142
- text = "see .claude-editor/jobs/j1/log.txt"
143
- assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
142
+ text = "see .claude-profile-a/jobs/j1/log.txt"
143
+ assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
144
144
 
145
145
 
146
146
  def test_scan_backslash_relative_worktree_path_is_detected() -> None:
@@ -171,7 +171,7 @@ def test_scan_sentence_final_marker_period_is_allowed() -> None:
171
171
  def test_gh_comment_with_job_scratch_path_is_blocked() -> None:
172
172
  command = (
173
173
  'gh pr comment 669 --body "Contact sheet at '
174
- r'C:\Users\example\.claude-editor\jobs\95762cea\tmp\staging\contact_sheet.png"'
174
+ r'C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"'
175
175
  )
176
176
  assert _body_names_volatile_path("Bash", {"command": command})
177
177
 
@@ -223,7 +223,7 @@ def test_substring_mention_of_gh_post_does_not_classify() -> None:
223
223
 
224
224
 
225
225
  def test_gh_word_not_first_token_does_not_classify() -> None:
226
- command = r'echo gh pr comment 42 --body "see C:\.claude-editor\jobs\x"'
226
+ command = r'echo gh pr comment 42 --body "see C:\.claude-profile-a\jobs\x"'
227
227
  assert not _body_names_volatile_path("Bash", {"command": command})
228
228
 
229
229
 
@@ -279,7 +279,7 @@ def test_gh_issue_edit_quoting_bare_constant_is_allowed(tmp_path: pathlib.Path)
279
279
  body_file.write_text(
280
280
  "The manifest skips these tooling-state prefixes:\n"
281
281
  ' ".claude/worktrees/",\n'
282
- ' ".claude-editor/jobs/",\n'
282
+ ' ".claude-profile-a/jobs/",\n'
283
283
  "Neither is a machine-local path.",
284
284
  encoding="utf-8",
285
285
  )
@@ -21,7 +21,7 @@ another argument never classifies.
21
21
 
22
22
  The marker scan runs in two tiers over the normalized body. Five bare markers
23
23
  — the system temp locations and env tokens — match as a plain substring
24
- anywhere. Two dot-relative directory markers, ``.claude-editor/jobs/`` and
24
+ anywhere. Two dot-relative directory markers, ``.claude-profile-a/jobs/`` and
25
25
  ``.claude/worktrees/``, match when either a slash sits immediately before the
26
26
  marker or a path segment follows it. A machine-local absolute path
27
27
  (``~/.claude/worktrees/wt/f.py``) and a relative path that names a child
@@ -117,7 +117,7 @@ def scan_text_for_volatile_marker(text: str) -> str | None:
117
117
  markers match anywhere::
118
118
 
119
119
  ok: "the `.claude/worktrees/` manifest key" -> None
120
- flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x" -> ".claude-editor/jobs/"
120
+ flag: r"C:\\Users\\me\\.claude-profile-a\\jobs\\x" -> ".claude-profile-a/jobs/"
121
121
  flag: "see .claude/worktrees/wt-1/notes.md" -> ".claude/worktrees/"
122
122
  flag: "saved to %TEMP%\\out.txt" -> "%temp%"
123
123
 
package/hooks/hooks.json CHANGED
@@ -176,6 +176,21 @@
176
176
  "type": "command",
177
177
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/task_list_loop_starter.py",
178
178
  "timeout": 10
179
+ },
180
+ {
181
+ "type": "command",
182
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/working_style_prompt.py",
183
+ "timeout": 10
184
+ },
185
+ {
186
+ "type": "command",
187
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/orchestrator_auto_starter.py",
188
+ "timeout": 10
189
+ },
190
+ {
191
+ "type": "command",
192
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/issue_tracker_session_starter.py",
193
+ "timeout": 10
179
194
  }
180
195
  ]
181
196
  }
@@ -8,6 +8,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
8
8
  |---|---|
9
9
  | `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
10
10
  | `any_type_config.py` | Config for the `Any`-type escape-hatch check |
11
+ | `ask_user_question_shape.py` | Pure AskUserQuestion lean-block shape analyzer: grades question and option-description prose against shared chat-detail markers and sentence/word caps with no hook I/O |
11
12
  | `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
12
13
  | `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
13
14
  | `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
@@ -31,11 +32,12 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
31
32
  | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
32
33
  | `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
33
34
  | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
34
- | `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (word cap, enforced word floor, bullet cap, lead-line window, per-line word cap, over-packed line cap), the `Long form:` escape prefix, the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
35
+ | `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (bullet cap, lead-line window, per-list-line word cap, overpacked list-line cap), the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
35
36
  | `fable_spawn_gate_constants.py` | The spawn tool-name set, the fable model alias and the model-id segment split pattern, the `FABLE-SPAWN-AUTHORIZED` authorization token, the advisor-protocol document path, payload field names, the deny message, and the deny `additionalContext` recovery text for the fable-tier spawn gate |
36
37
  | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
37
38
  | `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
38
39
  | `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
40
+ | `hedging_uncertainty_constants.py` | Sentence-boundary pattern, explicit uncertainty label patterns, hedge-term list separator, and positive corrective block text for OP-07C sentence-scoped hedging |
39
41
  | `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
40
42
  | `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
41
43
  | `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
@@ -54,15 +56,17 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
54
56
  | `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
55
57
  | `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
56
58
  | `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
57
- | `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker |
59
+ | `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker; classification that consumes those program and wrapper constants lives in `pytest_invocation.py` |
60
+ | `pytest_invocation.py` | Pure parsed-segment classifier for pytest runs through interpreters, shims, wrappers, and shell string-exec forms; no pipeline deny decision |
58
61
  | `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
59
62
  | `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
60
63
  | `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
61
64
  | `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
62
65
  | `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
66
+ | `prose_matcher_precision_constants.py` | Sample floor, keep/narrow precision floors, matcher ids, label decisions, advisory log path, emit caps, and context fingerprint length for OP-07B prose-matcher advisory telemetry |
63
67
  | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
64
68
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
65
- | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` the per-user project-path registry |
69
+ | project_paths_reader.py | Loads ~/.claude/project-paths.json and finds a home-bounded git root for registry checks |
66
70
  | `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
67
71
  | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
68
72
  | `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
@@ -72,8 +76,14 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
72
76
  | `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
73
77
  | `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
74
78
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
79
+ | `session_start_injector.py` | Pure SessionStart context injector: normalize `source` (`startup`/`resume`/`clear`/`compact`), honor enable/timeout, return structured injection results for starter hooks |
80
+ | `session_start_injector_constants.py` | Known SessionStart source values, injection status codes, enable env var, default timeout for the shared injector |
81
+ | `test_session_start_injector.py` | Behavioral tests for every known source, disabled, timeout, and unknown-source paths |
82
+ | `orchestrator_auto_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the orchestrator auto-starter |
83
+ | `issue_tracker_session_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the repository-gated issue-tracker starter |
75
84
  | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
76
85
  | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
86
+ | `shell_command_pipeline.py` | Quote-aware shell pipeline parsing: operator-paired segments after line-continuation join, comment strip, heredoc body drop, and parenthesis-group join |
77
87
  | `shell_command_segments.py` | Shell tokenization helpers shared by the segment-walking blockers: segment splitting on control operators, a segment's effective leading program, and a token's basename |
78
88
  | `shell_substitution_blocker_constants.py` | Tool name, payload keys, the four substitution-detection patterns, the deny decision shape, and the corrective message for the shell-substitution blocker |
79
89
  | `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
@@ -91,6 +101,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
91
101
  | `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
92
102
  | `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
93
103
  | `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
104
+ | `working_style_prompt_constants.py` | The fixed multi-paragraph working-style prompt text for the SessionStart working_style_prompt hook |
94
105
 
95
106
  ## Conventions
96
107