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
@@ -1,12 +1,15 @@
1
1
  """Tests for hedging_language_blocker hook response shape."""
2
2
 
3
3
  import importlib.util
4
+ import io
4
5
  import json
5
6
  import os
6
7
  import subprocess
7
8
  import sys
8
9
  import tempfile
9
10
 
11
+ import pytest
12
+
10
13
  HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
11
14
  _HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
12
15
  _HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
@@ -30,14 +33,22 @@ CLEAN_MESSAGE = "This is verified by the source document."
30
33
  EMPTY_MESSAGE = ""
31
34
 
32
35
 
33
- def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
36
+ def run_hook_with_message(
37
+ assistant_message: str, *, is_prose_style_enabled: bool = True
38
+ ) -> subprocess.CompletedProcess:
34
39
  hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
40
+ environment_by_key = os.environ.copy()
41
+ if is_prose_style_enabled:
42
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
43
+ else:
44
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
35
45
  return subprocess.run(
36
46
  [sys.executable, HOOK_SCRIPT_PATH],
37
47
  input=hook_input_payload,
38
48
  capture_output=True,
39
49
  text=True,
40
50
  check=False,
51
+ env=environment_by_key,
41
52
  )
42
53
 
43
54
 
@@ -58,6 +69,8 @@ def run_hook_with_patched_search_paths(
58
69
  wrapper_file_path = wrapper_file.name
59
70
 
60
71
  hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
72
+ environment_by_key = os.environ.copy()
73
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
61
74
  try:
62
75
  completed_process = subprocess.run(
63
76
  [sys.executable, wrapper_file_path],
@@ -65,6 +78,7 @@ def run_hook_with_patched_search_paths(
65
78
  capture_output=True,
66
79
  text=True,
67
80
  check=False,
81
+ env=environment_by_key,
68
82
  )
69
83
  finally:
70
84
  os.unlink(wrapper_file_path)
@@ -80,6 +94,49 @@ def test_user_facing_notice_matches_config_messages_module():
80
94
  assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
81
95
 
82
96
 
97
+ def test_hedging_scan_is_default_off() -> None:
98
+ completed_process = run_hook_with_message(
99
+ HEDGING_MESSAGE, is_prose_style_enabled=False
100
+ )
101
+
102
+ assert completed_process.returncode == 0
103
+ assert completed_process.stdout == ""
104
+
105
+
106
+ def test_default_off_emits_privacy_safe_advisory_candidates(
107
+ monkeypatch: pytest.MonkeyPatch,
108
+ ) -> None:
109
+ all_emitted: list[tuple[str, str, str]] = []
110
+
111
+ def _record_advisory(
112
+ matcher_id: str, surface: str, context_text: str, **_kwargs: object
113
+ ) -> dict[str, object]:
114
+ all_emitted.append((matcher_id, surface, context_text))
115
+ return {}
116
+
117
+ monkeypatch.setattr(
118
+ hedging_language_blocker, "emit_advisory_candidate", _record_advisory
119
+ )
120
+ monkeypatch.setattr(
121
+ hedging_language_blocker,
122
+ "prose_style_enforcement_enabled_in_environment",
123
+ lambda: False,
124
+ )
125
+ monkeypatch.setattr(
126
+ hedging_language_blocker.sys,
127
+ "stdin",
128
+ io.StringIO(json.dumps({"last_assistant_message": HEDGING_MESSAGE})),
129
+ )
130
+ with pytest.raises(SystemExit) as exit_info:
131
+ hedging_language_blocker.main()
132
+ assert exit_info.value.code == 0
133
+ assert all_emitted
134
+ matcher_id, surface, context_text = all_emitted[0]
135
+ assert matcher_id == "hedging_word"
136
+ assert surface == "Stop"
137
+ assert "likely" in context_text
138
+
139
+
83
140
  def test_hedging_message_emits_block_with_short_user_notice():
84
141
  completed_process = run_hook_with_message(HEDGING_MESSAGE)
85
142
 
@@ -140,3 +197,65 @@ def test_empty_message_passes_through_with_no_output():
140
197
 
141
198
  assert completed_process.returncode == 0
142
199
  assert completed_process.stdout == ""
200
+
201
+
202
+ def test_explicit_unverified_label_in_same_sentence_passes() -> None:
203
+ completed_process = run_hook_with_message(
204
+ "This claim is unverified; the deploy is probably blocked."
205
+ )
206
+
207
+ assert completed_process.returncode == 0
208
+ assert completed_process.stdout == ""
209
+
210
+
211
+ def test_i_dont_know_label_in_same_sentence_passes() -> None:
212
+ completed_process = run_hook_with_message(
213
+ "I don't know whether the port is probably open."
214
+ )
215
+
216
+ assert completed_process.returncode == 0
217
+ assert completed_process.stdout == ""
218
+
219
+
220
+ def test_supported_probability_without_hedge_word_passes() -> None:
221
+ completed_process = run_hook_with_message(
222
+ "The suite reports 0.92 precision on the labeled fixture set in "
223
+ "test_prose_matcher_advisory.py."
224
+ )
225
+
226
+ assert completed_process.returncode == 0
227
+ assert completed_process.stdout == ""
228
+
229
+
230
+ def test_label_in_one_sentence_does_not_exempt_bare_hedge_in_another() -> None:
231
+ completed_process = run_hook_with_message(
232
+ "This claim is unverified. The deploy is probably blocked."
233
+ )
234
+
235
+ assert completed_process.returncode == 0
236
+ parsed_response = json.loads(completed_process.stdout)
237
+ assert parsed_response["decision"] == "block"
238
+ assert "probably" in parsed_response["reason"]
239
+ assert "explicit uncertainty label" in parsed_response["reason"]
240
+
241
+
242
+ def test_bare_probably_still_blocks_with_positive_corrective() -> None:
243
+ completed_process = run_hook_with_message("The deploy is probably blocked.")
244
+
245
+ assert completed_process.returncode == 0
246
+ parsed_response = json.loads(completed_process.stdout)
247
+ assert parsed_response["decision"] == "block"
248
+ assert "probably" in parsed_response["reason"]
249
+ assert "label that claim unverified" in parsed_response["reason"]
250
+ assert "AskUserQuestion" in parsed_response["reason"]
251
+
252
+
253
+ def test_find_blocking_hedging_terms_is_sentence_scoped() -> None:
254
+ bare = hedging_language_blocker.find_blocking_hedging_terms(
255
+ "This claim is unverified. The deploy is probably blocked."
256
+ )
257
+ labeled = hedging_language_blocker.find_blocking_hedging_terms(
258
+ "This claim is unverified; the deploy is probably blocked."
259
+ )
260
+ assert bare == ["probably"]
261
+ assert labeled == []
@@ -3,6 +3,7 @@
3
3
  import importlib.util
4
4
  import io
5
5
  import json
6
+ import os
6
7
  import pathlib
7
8
  import sys
8
9
  from unittest import mock
@@ -192,14 +193,33 @@ def test_written_content_reads_edit_new_string() -> None:
192
193
  assert written_content("Edit", edit_input) == "edited body"
193
194
 
194
195
 
195
- def _run_main_with_io(input_text: str) -> str:
196
- with mock.patch("sys.stdin", io.StringIO(input_text)):
197
- with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
198
- try:
199
- hook_module.main()
200
- except SystemExit:
201
- pass
202
- return mock_stdout.getvalue()
196
+ def _run_main_with_io(
197
+ input_text: str, *, is_prose_style_enabled: bool = True
198
+ ) -> str:
199
+ environment_by_key = os.environ.copy()
200
+ if is_prose_style_enabled:
201
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
202
+ else:
203
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
204
+ with mock.patch.dict(os.environ, environment_by_key, clear=True):
205
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
206
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
207
+ try:
208
+ hook_module.main()
209
+ except SystemExit:
210
+ pass
211
+ return mock_stdout.getvalue()
212
+
213
+
214
+ def test_main_is_default_off_for_overstated_hook_module_write() -> None:
215
+ hook_input = {
216
+ "tool_name": "Write",
217
+ "tool_input": {
218
+ "file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
219
+ "content": _OVERSTATED_MESSAGE_MODULE,
220
+ },
221
+ }
222
+ assert _run_main_with_io(json.dumps(hook_input), is_prose_style_enabled=False) == ""
203
223
 
204
224
 
205
225
  def test_main_blocks_overstated_hook_module_write() -> None:
@@ -42,24 +42,40 @@ USER_DIRECTED_NEXT_STEPS_MESSAGE = (
42
42
  EMPTY_MESSAGE = ""
43
43
 
44
44
 
45
- def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
45
+ def run_hook_with_message(
46
+ assistant_message: str, *, is_prose_style_enabled: bool = True
47
+ ) -> subprocess.CompletedProcess:
46
48
  hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
49
+ environment_by_key = os.environ.copy()
50
+ if is_prose_style_enabled:
51
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
52
+ else:
53
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
47
54
  return subprocess.run(
48
55
  [sys.executable, HOOK_SCRIPT_PATH],
49
56
  input=hook_input_payload,
50
57
  capture_output=True,
51
58
  text=True,
52
59
  check=False,
60
+ env=environment_by_key,
53
61
  )
54
62
 
55
63
 
56
- def run_hook_with_payload(hook_input_payload: dict) -> subprocess.CompletedProcess:
64
+ def run_hook_with_payload(
65
+ hook_input_payload: dict, *, is_prose_style_enabled: bool = True
66
+ ) -> subprocess.CompletedProcess:
67
+ environment_by_key = os.environ.copy()
68
+ if is_prose_style_enabled:
69
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
70
+ else:
71
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
57
72
  return subprocess.run(
58
73
  [sys.executable, HOOK_SCRIPT_PATH],
59
74
  input=json.dumps(hook_input_payload),
60
75
  capture_output=True,
61
76
  text=True,
62
77
  check=False,
78
+ env=environment_by_key,
63
79
  )
64
80
 
65
81
 
@@ -76,6 +92,15 @@ def test_user_facing_notice_matches_config_messages_module():
76
92
  )
77
93
 
78
94
 
95
+ def test_intent_scan_is_default_off() -> None:
96
+ completed_process = run_hook_with_message(
97
+ INTENT_ENDING_MESSAGE, is_prose_style_enabled=False
98
+ )
99
+
100
+ assert completed_process.returncode == 0
101
+ assert completed_process.stdout == ""
102
+
103
+
79
104
  def test_intent_ending_message_emits_block_with_short_user_notice():
80
105
  completed_process = run_hook_with_message(INTENT_ENDING_MESSAGE)
81
106
 
@@ -367,9 +367,16 @@ def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
367
367
  assert survey.named_basenames == {"dialer_compose.py", "compose_dialer_cli.py"}
368
368
 
369
369
 
370
- def test_find_stale_inventory_skips_prose_only_directory():
371
- audit_rubrics_directory = (
372
- Path(__file__).resolve().parent.parent.parent / "audit-rubrics"
370
+ def test_find_stale_inventory_skips_directory_below_minimum_named_entries(
371
+ tmp_path: Path,
372
+ ) -> None:
373
+ """Skip a package inventory that names fewer than the minimum on-disk siblings."""
374
+ package_directory = tmp_path / "prose_package"
375
+ package_directory.mkdir()
376
+ (package_directory / "CLAUDE.md").write_text(
377
+ "| File | Role |\n|---|---|\n| `only_one.py` | sole entry |\n",
378
+ encoding="utf-8",
373
379
  )
374
- new_file_path = audit_rubrics_directory / "new_helper.py"
380
+ (package_directory / "only_one.py").write_text("FLAG = 1\n", encoding="utf-8")
381
+ new_file_path = package_directory / "new_helper.py"
375
382
  assert find_stale_inventory(str(new_file_path)) is None
@@ -42,18 +42,31 @@ strip_non_prose_regions = hook_module.strip_non_prose_regions
42
42
  build_block_reason = hook_module.build_block_reason
43
43
  find_question_block_violations = hook_module.find_question_block_violations
44
44
  build_lean_block_reason = hook_module.build_lean_block_reason
45
- evaluate = hook_module.evaluate
45
+
46
+
47
+ def evaluate(payload_by_key: dict[str, object]) -> str | None:
48
+ """Call evaluate with opinionated prose enforcement enabled."""
49
+ with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
50
+ return hook_module.evaluate(payload_by_key)
46
51
 
47
52
  from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
48
53
 
49
54
 
50
- def _run_hook_with_payload(payload: dict) -> subprocess.CompletedProcess[str]:
55
+ def _run_hook_with_payload(
56
+ payload: dict, *, is_prose_style_enabled: bool = True
57
+ ) -> subprocess.CompletedProcess[str]:
58
+ environment_by_key = os.environ.copy()
59
+ if is_prose_style_enabled:
60
+ environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
61
+ else:
62
+ environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
51
63
  return subprocess.run(
52
64
  [sys.executable, str(HOOK_SCRIPT_PATH)],
53
65
  input=json.dumps(payload),
54
66
  capture_output=True,
55
67
  text=True,
56
68
  check=False,
69
+ env=environment_by_key,
57
70
  )
58
71
 
59
72
 
@@ -64,10 +77,81 @@ def _decision_from(completed: subprocess.CompletedProcess[str]) -> str | None:
64
77
  return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
65
78
 
66
79
 
80
+ def _system_message_from(completed: subprocess.CompletedProcess[str]) -> str | None:
81
+ if not completed.stdout:
82
+ return None
83
+ parsed = json.loads(completed.stdout)
84
+ system_message = parsed.get("systemMessage")
85
+ return system_message if isinstance(system_message, str) else None
86
+
87
+
67
88
  def test_canonical_hook_script_exists_at_expected_path() -> None:
68
89
  assert HOOK_SCRIPT_PATH.is_file()
69
90
 
70
91
 
92
+ def test_heavy_word_scan_is_default_off(monkeypatch: pytest.MonkeyPatch) -> None:
93
+ monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
94
+ payload = {
95
+ "tool_name": "Write",
96
+ "tool_input": {
97
+ "file_path": "notes.md",
98
+ "content": "We initiate the worker pool at boot.",
99
+ },
100
+ }
101
+ assert hook_module.evaluate(payload) is None
102
+ completed = _run_hook_with_payload(payload, is_prose_style_enabled=False)
103
+ assert _decision_from(completed) is None
104
+
105
+
106
+ def test_default_off_emits_privacy_safe_advisory_candidates(
107
+ monkeypatch: pytest.MonkeyPatch,
108
+ ) -> None:
109
+ monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
110
+ all_emitted: list[tuple[str, str, str]] = []
111
+
112
+ def _record_advisory(
113
+ matcher_id: str, surface: str, context_text: str, **_kwargs: object
114
+ ) -> dict[str, object]:
115
+ all_emitted.append((matcher_id, surface, context_text))
116
+ return {}
117
+
118
+ monkeypatch.setattr(hook_module, "emit_advisory_candidate", _record_advisory)
119
+ payload = {
120
+ "tool_name": "Write",
121
+ "tool_input": {
122
+ "file_path": "notes.md",
123
+ "content": "We initiate the worker pool at boot.",
124
+ },
125
+ }
126
+ assert hook_module.evaluate(payload) is None
127
+ assert all_emitted
128
+ matcher_id, surface, context_text = all_emitted[0]
129
+ assert matcher_id == "plain_language_heavy_word"
130
+ assert surface == "Write"
131
+ assert "initiate" in context_text
132
+
133
+
134
+ def test_heavy_word_scan_opt_in_allows_with_system_message_swaps() -> None:
135
+ payload = {
136
+ "tool_name": "Write",
137
+ "tool_input": {
138
+ "file_path": "notes.md",
139
+ "content": "We initiate the worker pool at boot.",
140
+ },
141
+ }
142
+ assert evaluate(payload) is None
143
+ with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
144
+ deny_reason, advisory_message = hook_module.evaluate_with_advisory(payload)
145
+ assert deny_reason is None
146
+ assert advisory_message is not None
147
+ assert 'use "start" instead of "initiate"' in advisory_message
148
+ completed = _run_hook_with_payload(payload, is_prose_style_enabled=True)
149
+ assert _decision_from(completed) == "allow"
150
+ system_message = _system_message_from(completed)
151
+ assert system_message is not None
152
+ assert 'use "start" instead of "initiate"' in system_message
153
+
154
+
71
155
  def test_bare_prose_banned_term_is_detected() -> None:
72
156
  matched = find_banned_terms("We initiate the worker pool at boot.")
73
157
  assert any(each_term == "initiate" for each_term, _replacement in matched)
@@ -127,7 +211,7 @@ def test_block_reason_names_term_and_replacement() -> None:
127
211
  assert "start" in reason
128
212
 
129
213
 
130
- def test_ask_user_question_with_banned_term_is_denied() -> None:
214
+ def test_ask_user_question_with_banned_term_is_advisory_allow() -> None:
131
215
  payload = {
132
216
  "tool_name": "AskUserQuestion",
133
217
  "tool_input": {
@@ -141,10 +225,13 @@ def test_ask_user_question_with_banned_term_is_denied() -> None:
141
225
  },
142
226
  }
143
227
  completed = _run_hook_with_payload(payload)
144
- assert _decision_from(completed) == "deny"
228
+ assert _decision_from(completed) == "allow"
229
+ system_message = _system_message_from(completed)
230
+ assert system_message is not None
231
+ assert "utilize" in system_message
145
232
 
146
233
 
147
- def test_ask_user_question_banned_term_in_option_label_is_denied() -> None:
234
+ def test_ask_user_question_banned_term_in_option_label_is_advisory_allow() -> None:
148
235
  payload = {
149
236
  "tool_name": "AskUserQuestion",
150
237
  "tool_input": {
@@ -158,7 +245,10 @@ def test_ask_user_question_banned_term_in_option_label_is_denied() -> None:
158
245
  },
159
246
  }
160
247
  completed = _run_hook_with_payload(payload)
161
- assert _decision_from(completed) == "deny"
248
+ assert _decision_from(completed) == "allow"
249
+ system_message = _system_message_from(completed)
250
+ assert system_message is not None
251
+ assert "utilize" in system_message
162
252
 
163
253
 
164
254
  def test_clean_ask_user_question_passes_through() -> None:
@@ -178,7 +268,7 @@ def test_clean_ask_user_question_passes_through() -> None:
178
268
  assert _decision_from(completed) is None
179
269
 
180
270
 
181
- def test_write_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
271
+ def test_write_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
182
272
  target = tmp_path / "notes.md"
183
273
  payload = {
184
274
  "tool_name": "Write",
@@ -188,7 +278,10 @@ def test_write_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
188
278
  },
189
279
  }
190
280
  completed = _run_hook_with_payload(payload)
191
- assert _decision_from(completed) == "deny"
281
+ assert _decision_from(completed) == "allow"
282
+ system_message = _system_message_from(completed)
283
+ assert system_message is not None
284
+ assert "utilize" in system_message
192
285
 
193
286
 
194
287
  def test_write_markdown_at_ephemeral_path_is_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -230,7 +323,7 @@ def test_edit_markdown_clean_content_passes_through(tmp_path: Path) -> None:
230
323
  assert _decision_from(completed) is None
231
324
 
232
325
 
233
- def test_multiedit_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
326
+ def test_multiedit_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
234
327
  target = tmp_path / "notes.md"
235
328
  payload = {
236
329
  "tool_name": "MultiEdit",
@@ -243,7 +336,10 @@ def test_multiedit_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
243
336
  },
244
337
  }
245
338
  completed = _run_hook_with_payload(payload)
246
- assert _decision_from(completed) == "deny"
339
+ assert _decision_from(completed) == "allow"
340
+ system_message = _system_message_from(completed)
341
+ assert system_message is not None
342
+ assert "utilize" in system_message
247
343
 
248
344
 
249
345
  def test_other_tool_is_ignored() -> None:
@@ -270,19 +366,24 @@ def test_real_file_path_is_still_stripped() -> None:
270
366
 
271
367
 
272
368
  def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
273
- """A deny routed through the dispatcher's native path logs one record.
369
+ """A lean-block deny routed through the dispatcher's native path logs once.
274
370
 
275
371
  On the Write|Edit|MultiEdit surface this hook runs only through
276
372
  pre_tool_use_dispatcher's native path, which calls evaluate() and
277
- build_deny_payload() — never _emit_deny() or main(). The block must still
278
- land in the hook-blocks log, so the log call lives on build_deny_payload,
279
- the function the native path executes.
373
+ build_deny_payload() — never _emit_deny() or main(). Heavy words no longer
374
+ deny (OP-07D), so this probe uses AskUserQuestion lean-block chat detail.
375
+ The log call still lives on build_deny_payload.
280
376
  """
281
377
  deny_payload = {
282
- "tool_name": "Edit",
378
+ "tool_name": "AskUserQuestion",
283
379
  "tool_input": {
284
- "file_path": str(tmp_path / "notes.md"),
285
- "new_string": "This guide explains how to utilize the new cache layer.",
380
+ "questions": [
381
+ {
382
+ "question": "Which gate should run first?\n\nPlan the whole stack.",
383
+ "header": "Gate",
384
+ "options": [{"label": "Write", "description": "Runs on write."}],
385
+ }
386
+ ]
286
387
  },
287
388
  }
288
389
  native_hook = NativeHook(
@@ -291,7 +392,9 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
291
392
  )
292
393
 
293
394
  with patch.object(Path, "home", return_value=tmp_path):
294
- hosted_result = run_native_hook(native_hook, deny_payload, is_blocking=True)
395
+ hosted_result = run_native_hook(
396
+ native_hook, deny_payload, is_blocking=True
397
+ )
295
398
 
296
399
  assert hosted_result.captured_stdout
297
400
  log_path = tmp_path / ".claude" / "logs" / "hook-blocks.log"
@@ -527,7 +630,14 @@ def test_markdown_write_keeps_its_bullets_and_tables(tmp_path: Path) -> None:
527
630
  "tool_input": {"file_path": target_path, "content": bulleted_prose},
528
631
  }
529
632
 
530
- assert evaluate(heavy_payload) is not None
633
+ assert evaluate(heavy_payload) is None
634
+ with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
635
+ _deny_reason, advisory_message = hook_module.evaluate_with_advisory(
636
+ heavy_payload
637
+ )
638
+ assert _deny_reason is None
639
+ assert advisory_message is not None
640
+ assert "utilize" in advisory_message
531
641
  assert evaluate(clean_payload) is None
532
642
 
533
643