claude-dev-env 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (323) hide show
  1. package/CLAUDE.md +8 -2
  2. package/_shared/pr-loop/CLAUDE.md +1 -1
  3. package/_shared/pr-loop/audit-contract.md +17 -6
  4. package/_shared/pr-loop/audit-reply-template.md +4 -4
  5. package/_shared/pr-loop/code-rules-gate.md +3 -5
  6. package/_shared/pr-loop/fix-protocol.md +2 -3
  7. package/_shared/pr-loop/gh-payloads.md +1 -1
  8. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  9. package/_shared/pr-loop/scripts/README.md +1 -1
  10. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
  13. package/_shared/pr-loop/scripts/preflight.py +9 -4
  14. package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
  15. package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
  16. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
  17. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
  18. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  19. package/_shared/pr-loop/state-schema.md +5 -14
  20. package/agents/CLAUDE.md +2 -2
  21. package/agents/clean-coder.md +62 -562
  22. package/agents/code-quality-agent.md +10 -2
  23. package/agents/code-verifier.md +1 -1
  24. package/agents/test_agent_frontmatter.py +58 -40
  25. package/audit-rubrics/CLAUDE.md +2 -1
  26. package/audit-rubrics/audit-categories.json +704 -0
  27. package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
  28. package/bin/CLAUDE.md +16 -5
  29. package/bin/ever-shipped-skills.mjs +2 -0
  30. package/bin/install-plan.mjs +402 -0
  31. package/bin/install-transaction.mjs +455 -0
  32. package/bin/install.mjs +593 -147
  33. package/bin/install.plan.test.mjs +194 -0
  34. package/bin/install.profile-root.test.mjs +154 -0
  35. package/bin/install.profiles.test.mjs +253 -0
  36. package/bin/install.settings-defaults.test.mjs +200 -0
  37. package/bin/install.transaction.test.mjs +400 -0
  38. package/bin/install.uninstall-transaction.test.mjs +418 -0
  39. package/bin/merge_managed_permissions.mjs +130 -0
  40. package/bin/resolve-install-root.mjs +181 -0
  41. package/bin/select-install-targets.mjs +401 -0
  42. package/commands/CLAUDE.md +0 -2
  43. package/docs/CODE_RULES.md +4 -2
  44. package/docs/references/CLAUDE.md +3 -2
  45. package/docs/references/advisor-tool.md +44 -6
  46. package/docs/references/prose-style-enforcement.md +25 -0
  47. package/docs/references/team-advisor-skill.md +14 -8
  48. package/hooks/blocking/CLAUDE.md +6 -6
  49. package/hooks/blocking/_path_setup.py +9 -5
  50. package/hooks/blocking/code_rules_docstrings.py +124 -30
  51. package/hooks/blocking/code_rules_enforcer.py +161 -16
  52. package/hooks/blocking/code_rules_shared.py +40 -23
  53. package/hooks/blocking/config/CLAUDE.md +3 -5
  54. package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
  55. package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
  56. package/hooks/blocking/eli11_reply_enforcer.py +70 -113
  57. package/hooks/blocking/hedging_language_blocker.py +103 -20
  58. package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
  59. package/hooks/blocking/intent_only_ending_blocker.py +6 -0
  60. package/hooks/blocking/plain_language_blocker.py +139 -20
  61. package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
  62. package/hooks/blocking/state_description_blocker.py +7 -1
  63. package/hooks/blocking/tdd_enforcer.py +8 -0
  64. package/hooks/blocking/test__path_setup.py +28 -0
  65. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
  66. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
  67. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
  68. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
  69. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
  70. package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
  71. package/hooks/blocking/test_hedging_language_blocker.py +120 -1
  72. package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
  73. package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
  74. package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
  75. package/hooks/blocking/test_plain_language_blocker.py +129 -19
  76. package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
  77. package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
  78. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
  79. package/hooks/blocking/test_state_description_blocker.py +45 -2
  80. package/hooks/blocking/test_stop_dispatcher.py +11 -7
  81. package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
  82. package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
  83. package/hooks/hooks.json +15 -0
  84. package/hooks/hooks_constants/CLAUDE.md +14 -3
  85. package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
  86. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
  87. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  88. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
  89. package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
  90. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
  91. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
  92. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
  93. package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
  94. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
  95. package/hooks/hooks_constants/project_paths_reader.py +31 -4
  96. package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
  97. package/hooks/hooks_constants/pytest_invocation.py +354 -0
  98. package/hooks/hooks_constants/session_start_injector.py +163 -0
  99. package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
  100. package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
  101. package/hooks/hooks_constants/shell_command_segments.py +5 -0
  102. package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
  103. package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
  104. package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
  105. package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
  106. package/hooks/hooks_constants/test_session_start_injector.py +168 -0
  107. package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
  108. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
  109. package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
  110. package/hooks/observability/CLAUDE.md +2 -0
  111. package/hooks/observability/prose_matcher_advisory.py +237 -0
  112. package/hooks/observability/test_prose_matcher_advisory.py +143 -0
  113. package/hooks/session/CLAUDE.md +9 -1
  114. package/hooks/session/_path_setup.py +13 -0
  115. package/hooks/session/issue_tracker_session_starter.py +135 -0
  116. package/hooks/session/orchestrator_auto_starter.py +100 -0
  117. package/hooks/session/test__path_setup.py +28 -0
  118. package/hooks/session/test_issue_tracker_session_starter.py +104 -0
  119. package/hooks/session/test_orchestrator_auto_starter.py +99 -0
  120. package/hooks/session/test_working_style_prompt.py +47 -0
  121. package/hooks/session/untracked_repo_detector.py +1 -24
  122. package/hooks/session/working_style_prompt.py +36 -0
  123. package/hooks/validators/_path_setup.py +19 -0
  124. package/hooks/validators/run_all_validators.py +8 -13
  125. package/installable-surfaces.manifest.json +21 -0
  126. package/output-styles/CLAUDE.md +15 -0
  127. package/package.json +5 -2
  128. package/rules/CLAUDE.md +1 -0
  129. package/rules/code-standards.md +33 -7
  130. package/rules/durable-post-artifacts.md +2 -2
  131. package/rules/eli11-replies.md +7 -2
  132. package/rules/hedging-claims.md +4 -2
  133. package/rules/long-horizon-autonomy.md +3 -1
  134. package/rules/opus5-communication-contract.md +45 -0
  135. package/rules/plain-language.md +2 -2
  136. package/rules/research-mode.md +1 -1
  137. package/scripts/CLAUDE.md +13 -2
  138. package/scripts/Sync-RepoMain.ps1 +215 -0
  139. package/scripts/active_capability_references.py +218 -0
  140. package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
  141. package/scripts/claude_chain_runner.py +394 -6
  142. package/scripts/claude_chain_usage.py +1 -1
  143. package/scripts/codex_compat_materializer.py +105 -85
  144. package/scripts/dev_env_scripts_constants/CLAUDE.md +3 -1
  145. package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
  146. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
  147. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  148. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
  149. package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
  150. package/scripts/grok_patch_artifacts.py +123 -0
  151. package/scripts/grok_run_ledger.py +318 -0
  152. package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
  153. package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
  154. package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
  155. package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
  156. package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
  157. package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
  158. package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
  159. package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
  160. package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
  161. package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
  162. package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
  163. package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
  164. package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
  165. package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
  166. package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
  167. package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
  168. package/scripts/spawn_grok_batch.py +556 -9
  169. package/scripts/test_active_capability_references.py +108 -0
  170. package/scripts/test_claude_chain_runner.py +414 -82
  171. package/scripts/test_claude_chain_usage.py +12 -12
  172. package/scripts/test_grok_patch_artifacts.py +82 -0
  173. package/scripts/test_grok_run_ledger.py +116 -0
  174. package/scripts/test_resolve_worker_spawn.py +2 -2
  175. package/scripts/test_spawn_grok_batch.py +295 -0
  176. package/scripts/test_verify_installable_package.py +208 -0
  177. package/scripts/tests/test_codex_compat_materializer.py +33 -0
  178. package/scripts/verify_installable_package.py +612 -0
  179. package/settings.json +10 -0
  180. package/skills/CLAUDE.md +6 -2
  181. package/skills/_shared/CLAUDE.md +37 -4
  182. package/skills/_shared/advisor/CLAUDE.md +9 -0
  183. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  184. package/skills/_shared/advisor/scripts/README.md +9 -0
  185. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  186. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  187. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  188. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  189. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  190. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  191. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  192. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  193. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  194. package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
  195. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  196. package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
  197. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  198. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
  199. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
  200. package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
  201. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
  202. package/skills/_shared/pr-loop/state-schema.md +5 -0
  203. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  204. package/skills/autoconverge/reference/convergence.md +2 -1
  205. package/skills/autoconverge/reference/stop-conditions.md +5 -3
  206. package/skills/beat-sheet/SKILL.md +54 -0
  207. package/skills/beat-sheet/reference/visual-beats.md +29 -0
  208. package/skills/bugteam/CONSTRAINTS.md +4 -4
  209. package/skills/bugteam/EXAMPLES.md +1 -1
  210. package/skills/bugteam/reference/README.md +1 -1
  211. package/skills/e-code-review/SKILL.md +29 -3
  212. package/skills/e-code-review/reference/effort-evaluation.md +35 -0
  213. package/skills/e-code-review/reference/medium.md +15 -4
  214. package/skills/e-code-review/reference/runner-selection.md +40 -0
  215. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
  216. package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
  217. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  218. package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
  219. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  220. package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
  221. package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
  222. package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
  223. package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
  224. package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
  225. package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
  226. package/skills/e-code-review/scripts/grok_code_review.py +230 -0
  227. package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
  228. package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
  229. package/skills/e-code-review/scripts/test_grok_code_review.py +289 -0
  230. package/skills/grok-spawn/SKILL.md +5 -0
  231. package/skills/orchestrator/SKILL.md +5 -0
  232. package/skills/plan-to-pr/scripts/create_packet.py +4 -4
  233. package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
  234. package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
  235. package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
  236. package/skills/plan-to-pr/scripts/validate_run.py +4 -1
  237. package/skills/pr-converge/scripts/check_convergence.py +21 -19
  238. package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
  239. package/skills/pr-converge/scripts/conftest.py +35 -0
  240. package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
  241. package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
  242. package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
  243. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  244. package/skills/rebase/SKILL.md +15 -3
  245. package/skills/reviewer-gates/SKILL.md +2 -2
  246. package/skills/show/SKILL.md +51 -0
  247. package/skills/show/references/accessibility.md +7 -0
  248. package/skills/show/references/art.md +3 -0
  249. package/skills/show/references/charts.md +3 -0
  250. package/skills/show/references/core-design.md +14 -0
  251. package/skills/show/references/erds.md +3 -0
  252. package/skills/show/references/flowcharts.md +3 -0
  253. package/skills/show/references/host-and-html.md +3 -0
  254. package/skills/show/references/illustrative-diagrams.md +10 -0
  255. package/skills/show/references/interaction.md +3 -0
  256. package/skills/show/references/mockups.md +3 -0
  257. package/skills/show/references/quality-gates.md +7 -0
  258. package/skills/show/references/structural-diagrams.md +3 -0
  259. package/skills/show/references/subject-inventory.md +21 -0
  260. package/skills/show/references/svg-contract.md +22 -0
  261. package/skills/show/routing.yaml +30 -0
  262. package/skills/show/samples/pr1262-v2.svg +222 -0
  263. package/skills/show/scripts/README.md +6 -0
  264. package/skills/show/scripts/validate-artifact.py +91 -0
  265. package/skills/show/scripts/validate-package.py +18 -0
  266. package/skills/show/templates/html-widget.html +4 -0
  267. package/skills/show/templates/svg-base.svg +19 -0
  268. package/skills/show/tests/fixtures/css-var.svg +6 -0
  269. package/skills/show/tests/fixtures/dead-ref.svg +7 -0
  270. package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
  271. package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
  272. package/skills/show/tests/fixtures/invalid.svg +1 -0
  273. package/skills/show/tests/fixtures/large-canvas.svg +21 -0
  274. package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
  275. package/skills/show/tests/fixtures/valid.html +1 -0
  276. package/skills/show/tests/test_validate-artifact.py +74 -0
  277. package/skills/show/tests/test_validators.py +59 -0
  278. package/skills/show/workflows/create-visual.md +13 -0
  279. package/skills/show/workflows/review-visual.md +20 -0
  280. package/skills/split-pr/SKILL.md +85 -0
  281. package/skills/split-pr/reference/path-layers.md +16 -0
  282. package/skills/split-pr/reference/proposal-format.md +15 -0
  283. package/skills/split-pr/reference/split-further-loop.md +10 -0
  284. package/skills/split-pr/reference/splitting-principles.md +26 -0
  285. package/skills/split-pr/scripts/analyze_pr.py +279 -0
  286. package/skills/split-pr/scripts/categorize_files.py +106 -0
  287. package/skills/split-pr/scripts/config/__init__.py +1 -0
  288. package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
  289. package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
  290. package/skills/split-pr/scripts/config/packing_constants.py +61 -0
  291. package/skills/split-pr/scripts/config/plan_constants.py +49 -0
  292. package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
  293. package/skills/split-pr/scripts/execute_split_slices.py +82 -0
  294. package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
  295. package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
  296. package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
  297. package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
  298. package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
  299. package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
  300. package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
  301. package/skills/split-pr/scripts/split_pr_title.py +41 -0
  302. package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
  303. package/skills/split-pr/scripts/test_categorize_files.py +55 -0
  304. package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
  305. package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
  306. package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
  307. package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
  308. package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
  309. package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
  310. package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
  311. package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
  312. package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
  313. package/skills/split-pr/scripts/test_verify_plan.py +56 -0
  314. package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
  315. package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
  316. package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
  317. package/skills/split-pr/scripts/verify_plan.py +139 -0
  318. package/skills/task-build/reference/tool-routing.md +3 -0
  319. package/skills/team-advisor/SKILL.md +23 -44
  320. package/system-prompts/software-engineer.xml +17 -5
  321. package/commands/initialize.md +0 -90
  322. package/commands/stubcheck.md +0 -88
  323. package/skills/test_markdown_link_integrity.py +0 -107
@@ -3,19 +3,12 @@
3
3
  import ast
4
4
  import difflib
5
5
  import os
6
- import sys
7
6
  import tempfile
8
7
  from collections.abc import Collection, Iterator
9
8
  from pathlib import Path
10
9
 
11
- _blocking_directory = str(Path(__file__).resolve().parent)
12
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
13
- if _blocking_directory not in sys.path:
14
- sys.path.insert(0, _blocking_directory)
15
- if _hooks_directory not in sys.path:
16
- sys.path.insert(0, _hooks_directory)
17
-
18
- from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
10
+ from hooks_constants.code_rules_enforcer_constants import (
11
+ ALL_AGENT_HOME_TOOLING_PATTERNS,
19
12
  ALL_DIFF_CHANGED_OPCODE_TAGS,
20
13
  ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES,
21
14
  ALL_HOOK_INFRASTRUCTURE_PATTERNS,
@@ -30,14 +23,14 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
30
23
  LEADING_DRIVE_LETTER_PATTERN,
31
24
  STRICT_TEST_FILE_BASENAME_PATTERN,
32
25
  )
33
- from hooks_constants.harness_scratchpad_constants import ( # noqa: E402
26
+ from hooks_constants.harness_scratchpad_constants import (
34
27
  CLAUDE_SESSION_ID_ENVIRONMENT_VARIABLE_NAME,
35
28
  HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
36
29
  HARNESS_SCRATCHPAD_USER_DIRECTORY_NAME,
37
30
  HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX,
38
31
  HOOK_PAYLOAD_SESSION_ID_KEY,
39
32
  )
40
- from hooks_constants.unused_module_import_constants import ( # noqa: E402
33
+ from hooks_constants.unused_module_import_constants import (
41
34
  TYPE_CHECKING_IDENTIFIER,
42
35
  )
43
36
 
@@ -56,6 +49,27 @@ def is_hook_infrastructure(file_path: str) -> bool:
56
49
  return any(pattern.replace("\\", "/") in path_lower for pattern in ALL_HOOK_INFRASTRUCTURE_PATTERNS)
57
50
 
58
51
 
52
+ def is_agent_home_tooling(file_path: str) -> bool:
53
+ """Check whether a path sits under a coding agent's home-directory tooling.
54
+
55
+ A coding agent installs helper scripts under a dot-directory in the home
56
+ tree, such as ``~/.grok/runs/``. That tree is vendored tooling rather than
57
+ project code, so the repository code rules do not govern it. The leading dot
58
+ is load-bearing: a project directory named ``grok/`` stays governed.
59
+
60
+ Args:
61
+ file_path: The candidate path to classify.
62
+
63
+ Returns:
64
+ True when the path sits under a recognized agent home directory.
65
+ """
66
+ path_lower = "/" + file_path.lower().replace("\\", "/").lstrip("/")
67
+ return any(
68
+ each_pattern.replace("\\", "/") in path_lower
69
+ for each_pattern in ALL_AGENT_HOME_TOOLING_PATTERNS
70
+ )
71
+
72
+
59
73
  def is_test_file(file_path: str) -> bool:
60
74
  """Check if file is a test file."""
61
75
  path_lower = file_path.lower()
@@ -405,17 +419,18 @@ def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
405
419
 
406
420
 
407
421
  def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
408
- """Return True when file_path is a throwaway scratch path exempt from repo gates.
409
-
410
- Combines the two throwaway-path families a repo gate skips: the root-anchored
411
- ephemeral scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``) and the
412
- harness session scratchpad. The session scratchpad match reads the session id
413
- from the payload when one is supplied, and from the harness environment
414
- variable otherwise, so a caller that holds no payload still gets the match.
415
- The run_all_validators PreToolUse gate calls this predicate to skip a scratch
416
- target before it validates. The code-rules and TDD gates call the two path
417
- predicates ``is_ephemeral_script_path`` and ``is_under_session_scratchpad``
418
- directly.
422
+ """Return True when file_path is exempt from the repository gates.
423
+
424
+ Combines the path families a repo gate skips: the root-anchored ephemeral
425
+ scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``), the harness
426
+ session scratchpad, and a coding agent's own home-directory tooling such as
427
+ ``~/.grok/runs/``. The session scratchpad match reads the session id from the
428
+ payload when one is supplied, and from the harness environment variable
429
+ otherwise, so a caller that holds no payload still gets the match. The
430
+ run_all_validators PreToolUse gate calls this predicate to skip an exempt
431
+ target before it validates. The code-rules and TDD gates call the underlying
432
+ predicates ``is_ephemeral_script_path``, ``is_agent_home_tooling``, and
433
+ ``is_under_session_scratchpad`` directly.
419
434
 
420
435
  Args:
421
436
  file_path: The candidate path to classify.
@@ -423,10 +438,12 @@ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
423
438
  read the session id from the environment alone.
424
439
 
425
440
  Returns:
426
- True when the path is ephemeral scratch or under the session scratchpad.
441
+ True when the path is scratch, agent tooling, or the session scratchpad.
427
442
  """
428
443
  if is_ephemeral_script_path(file_path):
429
444
  return True
445
+ if is_agent_home_tooling(file_path):
446
+ return True
430
447
  return is_under_session_scratchpad(file_path, hook_payload or {})
431
448
 
432
449
 
@@ -1,16 +1,14 @@
1
1
  # hooks/blocking/config
2
2
 
3
- A Python package that holds shared constants for the verified-commit gate family. Three modules in `blocking/` import from here:
4
-
5
- - `verification_verdict_store.py`
6
- - `verified_commit_gate.py`
7
- - `verifier_verdict_minter.py`
3
+ A Python package that holds shared constants for blocking hooks. Verified-commit gate modules and opinionated prose-style gates import from here.
8
4
 
9
5
  ## Key files
10
6
 
11
7
  | File | Contents |
12
8
  |---|---|
13
9
  | `__init__.py` | Declares this as a regular package (not a namespace package) so it resolves first on `sys.path` |
10
+ | `code_review_enforcement_constants.py` | Stamp and effort tunables for the code-review enforcement gates |
11
+ | `prose_style_enforcement_constants.py` | `CLAUDE_PROSE_STYLE_ENFORCEMENT` opt-in (default off) for opinionated prose gates |
14
12
  | `verified_commit_constants.py` | All tunables for the gate: directory names, regex patterns for detecting verdict paths and obfuscation attempts, timeout values, git subcommand sets, bypass marker, and corrective messages |
15
13
  | `verified_commit_context_constants.py` | `VERIFY_SKIP_ADDITIONAL_CONTEXT`, the ``# verify-skip`` usage rule the gate attaches to a deny payload's `additionalContext` |
16
14
  | `verified_commit_gate_output_constants.py` | `PRE_TOOL_USE_HOOK_EVENT_NAME` and `DENY_PERMISSION_DECISION` (the deny payload's event name and decision string), `GATE_HOOK_MODULE_NAME` (the gate's own module name for block logging), and `REGEX_ALTERNATION_SEPARATOR` (the `\|` join for the directory-change verb alternation in `gated_invocations.py`) |
@@ -0,0 +1,38 @@
1
+ """Single source of truth for opinionated prose-style gate opt-in.
2
+
3
+ ::
4
+
5
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=1 -> PROSE_STYLE_ENFORCEMENT_ENABLED True
6
+ (unset) -> PROSE_STYLE_ENFORCEMENT_ENABLED False
7
+
8
+ Structural AskUserQuestion lean-block validation stays always on. The flag
9
+ arms only opinionated prose gates (heavy-word swaps, hedging, historical
10
+ state phrasing, intent-only endings, hook prose-detector consistency).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import os
16
+
17
+ PROSE_STYLE_ENFORCEMENT_ENV_VAR = "CLAUDE_PROSE_STYLE_ENFORCEMENT"
18
+ ALL_ENFORCEMENT_ENABLED_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
19
+
20
+
21
+ def prose_style_enforcement_enabled_in_environment() -> bool:
22
+ """Read whether this machine turns opinionated prose gates on.
23
+
24
+ ::
25
+
26
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=1 -> True
27
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=" On " -> True
28
+ CLAUDE_PROSE_STYLE_ENFORCEMENT=0 -> False
29
+ (variable unset) -> False
30
+
31
+ Returns:
32
+ True when the variable holds an enabled value, False otherwise.
33
+ """
34
+ raw_environment_setting = os.environ.get(PROSE_STYLE_ENFORCEMENT_ENV_VAR, "")
35
+ return raw_environment_setting.strip().lower() in ALL_ENFORCEMENT_ENABLED_ENV_VALUES
36
+
37
+
38
+ PROSE_STYLE_ENFORCEMENT_ENABLED = prose_style_enforcement_enabled_in_environment()
@@ -0,0 +1,45 @@
1
+ """Tests for the prose-style enforcement opt-in flag (default off)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _CONSTANTS_PATH = Path(__file__).resolve().parent / "prose_style_enforcement_constants.py"
11
+
12
+
13
+ def _load_constants_module(monkeypatch: pytest.MonkeyPatch, env_value: str | None):
14
+ module_name = "prose_style_enforcement_constants_under_test"
15
+ if env_value is None:
16
+ monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
17
+ else:
18
+ monkeypatch.setenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", env_value)
19
+ module_spec = importlib.util.spec_from_file_location(module_name, _CONSTANTS_PATH)
20
+ assert module_spec is not None and module_spec.loader is not None
21
+ module = importlib.util.module_from_spec(module_spec)
22
+ module_spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ def test_absent_env_is_default_off(monkeypatch: pytest.MonkeyPatch) -> None:
27
+ module = _load_constants_module(monkeypatch, None)
28
+ assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is False
29
+ assert module.prose_style_enforcement_enabled_in_environment() is False
30
+
31
+
32
+ @pytest.mark.parametrize("enabled_value", ["1", "true", "yes", "on", " True "])
33
+ def test_truthy_env_enables(
34
+ monkeypatch: pytest.MonkeyPatch, enabled_value: str
35
+ ) -> None:
36
+ module = _load_constants_module(monkeypatch, enabled_value)
37
+ assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is True
38
+
39
+
40
+ @pytest.mark.parametrize("disabled_value", ["0", "false", "no", "off", ""])
41
+ def test_falsy_env_stays_off(
42
+ monkeypatch: pytest.MonkeyPatch, disabled_value: str
43
+ ) -> None:
44
+ module = _load_constants_module(monkeypatch, disabled_value)
45
+ assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is False
@@ -1,15 +1,13 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Stop hook that blocks a final reply breaking the short action-first reply shape.
4
-
5
- The `eli11-replies` rule asks every chat reply to lead with the action, keep
6
- findings to a few bullets, and stay short. This hook reads the final assistant
7
- message, strips code fences, inline code, blockquotes, table rows, and link
8
- targets, then judges what a reader actually sees: the word count against a hard
9
- cap, whether numbered steps lead a reply that tells the user to act, the number
10
- of bullet lines, and how many lines pack more than one idea's worth of words.
11
- A reply opening with "Long form:" opts out, so a user-requested full report
12
- still goes through.
3
+ Stop hook that blocks egregious reply-shape failures (action-first, bullets).
4
+
5
+ The `eli11-replies` and `opus5-communication-contract` rules ask every chat
6
+ reply to lead with the action when the user must act, keep findings to a few
7
+ bullets, and stay scannable. This hook reads the final assistant message,
8
+ strips code fences, inline code, blockquotes, table rows, and link targets,
9
+ then judges shape only: multi-line instructions need numbered steps first,
10
+ bullet count stays bounded, and list lines stay one-idea short.
13
11
  """
14
12
 
15
13
  import json
@@ -30,13 +28,10 @@ from hooks_constants.eli11_reply_enforcer_constants import ( # noqa: E402
30
28
  IMPERATIVE_OBJECT_TOKEN_PATTERN,
31
29
  LINK_TARGET_PATTERN,
32
30
  LIST_MARKER_PREFIX_PATTERN,
33
- LONG_FORM_ESCAPE_PREFIX,
34
31
  MARKDOWN_LEAD_MARKER_PATTERN,
35
32
  MAXIMUM_BULLET_LINE_COUNT,
36
- MAXIMUM_OVERPACKED_LINE_COUNT,
37
- MAXIMUM_REPLY_WORD_COUNT,
38
- MAXIMUM_WORDS_PER_LINE,
39
- MINIMUM_ENFORCED_WORD_COUNT,
33
+ MAXIMUM_OVERPACKED_LIST_LINE_COUNT,
34
+ MAXIMUM_WORDS_PER_LIST_LINE,
40
35
  NUMBERED_STEP_PATTERN,
41
36
  TABLE_ROW_PATTERN,
42
37
  TARGET_BULLET_LINE_COUNT,
@@ -69,18 +64,6 @@ def extract_reply_prose(assistant_message: str) -> str:
69
64
  return LINK_TARGET_PATTERN.sub("", prose_text)
70
65
 
71
66
 
72
- def count_reply_words(prose_text: str) -> int:
73
- """Return the number of reader-visible words in the stripped prose.
74
-
75
- Args:
76
- prose_text: Prose already cleaned of code, quotes, tables, and links.
77
-
78
- Returns:
79
- The count of whitespace-separated tokens carrying a letter or digit.
80
- """
81
- return len(COUNTABLE_WORD_PATTERN.findall(prose_text))
82
-
83
-
84
67
  def collect_non_empty_lines(prose_text: str) -> list[str]:
85
68
  """Return every line of the stripped prose that carries visible text.
86
69
 
@@ -249,29 +232,13 @@ def count_words_in_line(prose_line: str) -> int:
249
232
  return len(COUNTABLE_WORD_PATTERN.findall(unmarked_line))
250
233
 
251
234
 
252
- def count_overpacked_lines(all_prose_lines: list[str]) -> int:
253
- """Return how many lines carry more words than one idea needs.
254
-
255
- Args:
256
- all_prose_lines: The non-empty lines of reply prose.
257
-
258
- Returns:
259
- The count of lines over the per-line word cap.
260
- """
261
- return sum(
262
- 1
263
- for each_line in all_prose_lines
264
- if count_words_in_line(each_line) > MAXIMUM_WORDS_PER_LINE
265
- )
266
-
267
-
268
235
  def strip_markdown_lead_markers(prose_line: str) -> str:
269
236
  """Return a line with its opening blockquote, heading, and bold markers off.
270
237
 
271
238
  ::
272
239
 
273
- in: "> **Long form:** the report follows"
274
- out: "Long form:** the report follows"
240
+ in: "> **Bold lead:** the report follows"
241
+ out: "Bold lead:** the report follows"
275
242
 
276
243
  Args:
277
244
  prose_line: One line of raw reply text.
@@ -282,34 +249,37 @@ def strip_markdown_lead_markers(prose_line: str) -> str:
282
249
  return MARKDOWN_LEAD_MARKER_PATTERN.sub("", prose_line.strip(), count=1)
283
250
 
284
251
 
285
- def opens_with_long_form_escape(assistant_message: str) -> bool:
286
- """Return True when the reply opts out through the Long form prefix.
252
+ def is_list_item_line(prose_line: str) -> bool:
253
+ """Return True when a line is a bullet or numbered list item.
254
+
255
+ Args:
256
+ prose_line: One non-empty line of reply prose.
257
+
258
+ Returns:
259
+ True when the line opens with a bullet or numbered-step marker.
260
+ """
261
+ if BULLET_LINE_PATTERN.match(prose_line):
262
+ return True
263
+ return NUMBERED_STEP_PATTERN.match(prose_line) is not None
287
264
 
288
- ::
289
265
 
290
- ok: "**Long form:** the report follows" -> True
291
- flag: "The report follows" -> False
266
+ def count_overpacked_list_lines(all_prose_lines: list[str]) -> int:
267
+ """Return how many list lines carry more words than one idea needs.
292
268
 
293
- A bold, blockquote, or heading wrapper around the prefix still opts out.
269
+ Plain paragraphs are not counted so a requested full report can use long
270
+ prose lines without a word ceiling.
294
271
 
295
272
  Args:
296
- assistant_message: The raw final assistant message.
273
+ all_prose_lines: The non-empty lines of reply prose.
297
274
 
298
275
  Returns:
299
- True when the first line with content starts with the escape prefix.
276
+ The count of bullet/numbered lines over the per-list-line word cap.
300
277
  """
301
- for each_line in assistant_message.splitlines():
302
- if not each_line.strip():
303
- continue
304
- unwrapped_line = strip_markdown_lead_markers(each_line)
305
- return unwrapped_line.lower().startswith(LONG_FORM_ESCAPE_PREFIX)
306
- return False
307
-
308
-
309
- def describe_word_cap_violation(reply_word_count: int) -> str:
310
- """Return the violation text naming the reply length and the cap."""
311
- return (
312
- f"{reply_word_count} words, over the {MAXIMUM_REPLY_WORD_COUNT}-word cap"
278
+ return sum(
279
+ 1
280
+ for each_line in all_prose_lines
281
+ if is_list_item_line(each_line)
282
+ and count_words_in_line(each_line) > MAXIMUM_WORDS_PER_LIST_LINE
313
283
  )
314
284
 
315
285
 
@@ -317,70 +287,68 @@ def describe_bullet_cap_violation(bullet_line_count: int) -> str:
317
287
  """Return the violation text naming the bullet count and the target."""
318
288
  return (
319
289
  f"{bullet_line_count} bullets, over the {MAXIMUM_BULLET_LINE_COUNT}-bullet "
320
- f"cap - cut findings to {TARGET_BULLET_LINE_COUNT} bullets"
290
+ f"cap - put findings in at most {TARGET_BULLET_LINE_COUNT} bullets"
321
291
  )
322
292
 
323
293
 
324
- def describe_overpacked_line_violation(overpacked_line_count: int) -> str:
325
- """Return the violation text naming how many lines pack too much in."""
294
+ def describe_overpacked_list_line_violation(overpacked_line_count: int) -> str:
295
+ """Return the violation text for overpacked list lines."""
326
296
  return (
327
- f"{overpacked_line_count} lines carry too many words - one idea per line, "
328
- f"at most {MAXIMUM_WORDS_PER_LINE} words each"
297
+ f"{overpacked_line_count} list lines carry too many words - one idea per "
298
+ f"bullet or step, at most {MAXIMUM_WORDS_PER_LIST_LINE} words each"
329
299
  )
330
300
 
331
301
 
332
302
  def describe_action_first_violation() -> str:
333
303
  """Return the violation text for instructions buried under prose."""
334
304
  return (
335
- "the reply tells the user to act but leads with prose - put the steps "
336
- "first, as a numbered list"
305
+ "the reply tells the user to act but leads with prose - open with "
306
+ "numbered steps, one short line each"
337
307
  )
338
308
 
339
309
 
340
- def is_action_first_violation(
341
- reply_word_count: int, all_prose_lines: list[str]
342
- ) -> bool:
343
- """Return True when instructions sit below the lead of a long reply.
310
+ def is_action_first_violation(all_prose_lines: list[str]) -> bool:
311
+ """Return True when multi-line instructions lack leading numbered steps.
312
+
313
+ ::
314
+
315
+ ok: single-line "Run the migration." -> False
316
+ ok: numbered steps first, then prose -> False
317
+ flag: prose then "Install the package." -> True
344
318
 
345
319
  Args:
346
- reply_word_count: The reader-visible word count of the reply.
347
320
  all_prose_lines: The non-empty lines of reply prose.
348
321
 
349
322
  Returns:
350
- True when the reply is long, carries instruction lines, and shows no
351
- numbered step among its lead lines.
323
+ True when the reply has more than one line, carries instruction
324
+ lines, and shows no numbered step among its lead lines.
352
325
  """
353
- if reply_word_count < MINIMUM_ENFORCED_WORD_COUNT:
326
+ if len(all_prose_lines) <= 1:
354
327
  return False
355
328
  if not has_imperative_instruction_line(all_prose_lines):
356
329
  return False
357
330
  return not has_leading_numbered_step(all_prose_lines)
358
331
 
359
332
 
360
- def collect_shape_violations(
361
- reply_word_count: int, all_prose_lines: list[str]
362
- ) -> list[str]:
333
+ def collect_shape_violations(all_prose_lines: list[str]) -> list[str]:
363
334
  """Return the violation texts for every reply-shape rule the reply breaks.
364
335
 
365
336
  Args:
366
- reply_word_count: The reader-visible word count of the reply.
367
337
  all_prose_lines: The non-empty lines of reply prose.
368
338
 
369
339
  Returns:
370
340
  One violation text per broken rule, empty when the reply is in shape.
371
341
  """
372
342
  all_violations: list[str] = []
373
- if reply_word_count > MAXIMUM_REPLY_WORD_COUNT:
374
- all_violations.append(describe_word_cap_violation(reply_word_count))
375
- if is_action_first_violation(reply_word_count, all_prose_lines):
343
+ if is_action_first_violation(all_prose_lines):
376
344
  all_violations.append(describe_action_first_violation())
377
345
  bullet_line_count = count_bullet_lines(all_prose_lines)
378
346
  if bullet_line_count > MAXIMUM_BULLET_LINE_COUNT:
379
347
  all_violations.append(describe_bullet_cap_violation(bullet_line_count))
380
- overpacked_line_count = count_overpacked_lines(all_prose_lines)
381
- if overpacked_line_count > MAXIMUM_OVERPACKED_LINE_COUNT:
348
+ overpacked_list_line_count = count_overpacked_list_lines(all_prose_lines)
349
+ if overpacked_list_line_count > MAXIMUM_OVERPACKED_LIST_LINE_COUNT:
382
350
  all_violations.append(
383
- describe_overpacked_line_violation(overpacked_line_count)
351
+ describe_overpacked_list_line_violation(overpacked_list_line_count)
384
352
  )
385
353
  return all_violations
386
354
 
@@ -390,11 +358,9 @@ def find_reply_shape_violations(assistant_message: str) -> list[str]:
390
358
 
391
359
  ::
392
360
 
393
- ok: a 40-word outcome sentence -> []
394
- flag: a 260-word wall -> ["260 words, over the 120-word cap"]
395
-
396
- A reply opening with the escape prefix, and a reply under the enforced word
397
- floor, are both returned as in shape.
361
+ ok: a short outcome sentence -> []
362
+ ok: a long requested audit with few bullets -> []
363
+ flag: prose then buried "Install the package." -> [action-first]
398
364
 
399
365
  Args:
400
366
  assistant_message: The raw final assistant message.
@@ -402,19 +368,12 @@ def find_reply_shape_violations(assistant_message: str) -> list[str]:
402
368
  Returns:
403
369
  One violation text per broken rule, empty when the reply is in shape.
404
370
  """
405
- if opens_with_long_form_escape(assistant_message):
406
- return []
407
371
  prose_text = extract_reply_prose(assistant_message)
408
- reply_word_count = count_reply_words(prose_text)
409
- if reply_word_count < MINIMUM_ENFORCED_WORD_COUNT:
410
- return []
411
- return collect_shape_violations(
412
- reply_word_count, collect_non_empty_lines(prose_text)
413
- )
372
+ return collect_shape_violations(collect_non_empty_lines(prose_text))
414
373
 
415
374
 
416
375
  def build_block_reason(all_violations: list[str]) -> str:
417
- """Return the corrective message naming each violation and the escape hatch.
376
+ """Return the corrective message naming each violation and the rewrite path.
418
377
 
419
378
  Args:
420
379
  all_violations: The violation texts the reply earned.
@@ -424,16 +383,14 @@ def build_block_reason(all_violations: list[str]) -> str:
424
383
  """
425
384
  formatted_violation_list = VIOLATION_SEPARATOR.join(all_violations)
426
385
  return (
427
- f"ELI11 REPLY SHAPE: Your reply breaks the short action-first shape "
386
+ f"ELI11 REPLY SHAPE: Rewrite the reply into a short action-first shape "
428
387
  f"({formatted_violation_list}).\n\n"
429
- f"Rewrite it short. When the user must act, open with numbered "
430
- f"click-by-click steps, one short line each. When nothing is needed, open "
431
- f"with the outcome in one sentence. Keep findings to at most "
432
- f"{TARGET_BULLET_LINE_COUNT} short bullets and stay under "
433
- f"{MAXIMUM_REPLY_WORD_COUNT} words. Code fences, blockquotes, tables, and "
434
- f"links are already exempt from the count.\n\n"
435
- f"When the user asked for a full report, start the reply with "
436
- f'"Long form:" to opt out of this check.\n\n'
388
+ f"When the user must act, open with numbered click-by-click steps, one "
389
+ f"short line each. When nothing is needed, open with the outcome in one "
390
+ f"sentence. Keep findings to at most {TARGET_BULLET_LINE_COUNT} short "
391
+ f"bullets. A requested full report may run long; keep list lines short "
392
+ f"and lead with the outcome. Code fences, blockquotes, tables, and "
393
+ f"links are already exempt from shape counting.\n\n"
437
394
  f"You MUST re-output the complete, revised response with the correction "
438
395
  f"applied."
439
396
  )