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
@@ -15,16 +15,16 @@ if _HOOKS_ROOT not in sys.path:
15
15
  import eli11_reply_enforcer
16
16
  from hooks_constants.eli11_reply_enforcer_constants import (
17
17
  MAXIMUM_BULLET_LINE_COUNT,
18
- MAXIMUM_OVERPACKED_LINE_COUNT,
19
- MAXIMUM_REPLY_WORD_COUNT,
20
- MAXIMUM_WORDS_PER_LINE,
21
- MINIMUM_ENFORCED_WORD_COUNT,
18
+ MAXIMUM_OVERPACKED_LIST_LINE_COUNT,
19
+ MAXIMUM_WORDS_PER_LIST_LINE,
22
20
  TARGET_BULLET_LINE_COUNT,
23
21
  )
24
22
  from hooks_constants.text_stripping import strip_code_and_quotes
25
23
 
26
- SAFE_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LINE // 2
27
- OVERPACKED_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LINE + 5
24
+ SAFE_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LIST_LINE // 2
25
+ OVERPACKED_LIST_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LIST_LINE + 5
26
+ LONG_REPORT_WORD_COUNT = 240
27
+ SHORT_REPLY_WORD_COUNT = 30
28
28
 
29
29
 
30
30
  def build_filler_prose(word_count: int) -> str:
@@ -45,17 +45,13 @@ def build_prose_block(line_count: int, words_per_line: int) -> str:
45
45
 
46
46
 
47
47
  def build_reply_of_exactly(total_word_count: int) -> str:
48
- """Return a filler reply holding exactly the requested words, no line overpacked.
49
-
50
- ::
51
-
52
- in: 22 -> three lines of 10, 10, and 2 filler words
48
+ """Return a filler reply holding exactly the requested words, no list line overpacked.
53
49
 
54
50
  Args:
55
51
  total_word_count: How many countable words the whole reply carries.
56
52
 
57
53
  Returns:
58
- A newline-joined reply whose every line stays under the per-line cap.
54
+ A newline-joined reply whose every line stays under the list-line word cap.
59
55
  """
60
56
  all_lines = []
61
57
  remaining_word_count = total_word_count
@@ -66,63 +62,34 @@ def build_reply_of_exactly(total_word_count: int) -> str:
66
62
  return "\n".join(all_lines)
67
63
 
68
64
 
69
- SHORT_REPLY_WORD_COUNT = MINIMUM_ENFORCED_WORD_COUNT // 2
70
- OVERLONG_REPLY_WORD_COUNT = MAXIMUM_REPLY_WORD_COUNT * 2
71
- JUST_OVER_WORD_CAP_WORD_COUNT = MAXIMUM_REPLY_WORD_COUNT + 1
72
- UNDER_FLOOR_WORD_COUNT = MINIMUM_ENFORCED_WORD_COUNT - 1
73
-
74
65
  SHORT_REPLY = build_reply_of_exactly(SHORT_REPLY_WORD_COUNT)
75
- OVERLONG_REPLY = build_reply_of_exactly(OVERLONG_REPLY_WORD_COUNT)
76
- UNDER_FLOOR_REPLY = build_reply_of_exactly(UNDER_FLOOR_WORD_COUNT)
77
- JUST_OVER_WORD_CAP_REPLY = build_reply_of_exactly(JUST_OVER_WORD_CAP_WORD_COUNT)
78
- THREE_OVERPACKED_LINE_REPLY = build_prose_block(
79
- MAXIMUM_OVERPACKED_LINE_COUNT + 1, OVERPACKED_LINE_WORD_COUNT
66
+ LONG_REPORT_REPLY = build_reply_of_exactly(LONG_REPORT_WORD_COUNT)
67
+ REQUESTED_FULL_REPORT = (
68
+ "Outcome: the audit is complete.\n\n"
69
+ + build_reply_of_exactly(LONG_REPORT_WORD_COUNT)
80
70
  )
81
- TWO_OVERPACKED_LINES = build_prose_block(
82
- MAXIMUM_OVERPACKED_LINE_COUNT, OVERPACKED_LINE_WORD_COUNT
71
+ THREE_OVERPACKED_LIST_LINE_REPLY = build_bullet_block(
72
+ MAXIMUM_OVERPACKED_LIST_LINE_COUNT + 1, OVERPACKED_LIST_LINE_WORD_COUNT
83
73
  )
84
- TWO_OVERPACKED_LINE_REPLY = (
85
- f"{TWO_OVERPACKED_LINES}\n{build_filler_prose(SAFE_LINE_WORD_COUNT)}"
74
+ TWO_OVERPACKED_LIST_LINE_REPLY = build_bullet_block(
75
+ MAXIMUM_OVERPACKED_LIST_LINE_COUNT, OVERPACKED_LIST_LINE_WORD_COUNT
86
76
  )
87
- SEVEN_BULLET_JUST_OVER_FLOOR_REPLY = build_bullet_block(
77
+ SEVEN_BULLET_REPLY = build_bullet_block(
88
78
  MAXIMUM_BULLET_LINE_COUNT + 1, SAFE_LINE_WORD_COUNT
89
79
  )
90
- LONG_FORM_OVERLONG_REPLY = f"Long form: the audit report follows.\n\n{OVERLONG_REPLY}"
91
- BOLD_LONG_FORM_OVERLONG_REPLY = f"**Long form:** the audit report follows.\n\n{OVERLONG_REPLY}"
92
- QUOTED_LONG_FORM_OVERLONG_REPLY = f"> Long form: the audit report follows.\n\n{OVERLONG_REPLY}"
93
- HEADING_LONG_FORM_OVERLONG_REPLY = f"# Long form: the audit report follows.\n\n{OVERLONG_REPLY}"
94
-
80
+ SIX_BULLET_REPLY = build_bullet_block(
81
+ MAXIMUM_BULLET_LINE_COUNT, SAFE_LINE_WORD_COUNT
82
+ )
95
83
  INSTRUCTION_LINE = "Run the migration script."
96
- INSTRUCTION_LINE_WORD_COUNT = len(INSTRUCTION_LINE.split())
97
-
98
84
  ACTION_WITHOUT_STEPS_FIRST_REPLY = (
99
- f"{build_reply_of_exactly(MAXIMUM_REPLY_WORD_COUNT // 2)}\n\n"
100
- f"{INSTRUCTION_LINE}\n\nMerge the branch."
85
+ f"{build_reply_of_exactly(40)}\n\n{INSTRUCTION_LINE}\n\nMerge the branch."
101
86
  )
102
87
  ACTION_WITH_STEPS_FIRST_REPLY = (
103
88
  "1. **Run** the migration script.\n"
104
89
  "2. **Merge** the branch.\n\n"
105
- f"{build_reply_of_exactly(MAXIMUM_REPLY_WORD_COUNT // 2)}"
106
- )
107
- AT_FLOOR_ACTION_REPLY = (
108
- f"{build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT - INSTRUCTION_LINE_WORD_COUNT)}"
109
- f"\n\n{INSTRUCTION_LINE}"
110
- )
111
- SEVEN_BULLET_REPLY = build_bullet_block(
112
- MAXIMUM_BULLET_LINE_COUNT + 1, SAFE_LINE_WORD_COUNT + 3
113
- )
114
- SIX_BULLET_REPLY = build_bullet_block(
115
- MAXIMUM_BULLET_LINE_COUNT, SAFE_LINE_WORD_COUNT + 5
116
- )
117
- FENCED_CODE_REPLY = f"{SHORT_REPLY}\n\n```python\n{OVERLONG_REPLY}\n```\n"
118
- BLOCKQUOTE_REPLY = f"{SHORT_REPLY}\n\n> {build_filler_prose(OVERLONG_REPLY_WORD_COUNT)}\n"
119
- TABLE_REPLY = "{}\n\n{}\n".format(
120
- SHORT_REPLY,
121
- "\n".join(
122
- f"| {build_filler_prose(MAXIMUM_WORDS_PER_LINE)} | cell |" for _ in range(15)
123
- ),
90
+ f"{build_reply_of_exactly(40)}"
124
91
  )
125
-
92
+ SINGLE_LINE_INSTRUCTION = INSTRUCTION_LINE
126
93
  ALL_NARRATIVE_OPENER_LINES = (
127
94
  "Open questions remain about the stripper edge cases.",
128
95
  "Run time stays under one second on the package suite.",
@@ -130,10 +97,10 @@ ALL_NARRATIVE_OPENER_LINES = (
130
97
  )
131
98
  NARRATIVE_OPENER_REPLY = "{}\n\n{}".format(
132
99
  "\n".join(ALL_NARRATIVE_OPENER_LINES),
133
- build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT),
100
+ build_reply_of_exactly(40),
134
101
  )
135
102
  INSTALL_WITHOUT_STEPS_FIRST_REPLY = (
136
- f"{build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT)}\n\n"
103
+ f"{build_reply_of_exactly(40)}\n\n"
137
104
  "Install the package from the registry."
138
105
  )
139
106
  DO_THINGS_WITH_STEPS_FIRST_REPLY = (
@@ -141,12 +108,24 @@ DO_THINGS_WITH_STEPS_FIRST_REPLY = (
141
108
  "1. Install the package.\n"
142
109
  "2. Restart the daemon.\n"
143
110
  "3. Save the file.\n\n"
144
- f"{build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT)}"
111
+ f"{build_reply_of_exactly(40)}"
145
112
  )
146
113
  DO_THINGS_WITHOUT_STEPS_FIRST_REPLY = (
147
- f"{build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT)}\n\n"
114
+ f"{build_reply_of_exactly(40)}\n\n"
148
115
  "Do 3 things: install, restart, save."
149
116
  )
117
+ FENCED_CODE_REPLY = f"{SHORT_REPLY}\n\n```python\n{LONG_REPORT_REPLY}\n```\n"
118
+ BLOCKQUOTE_REPLY = f"{SHORT_REPLY}\n\n> {build_filler_prose(LONG_REPORT_WORD_COUNT)}\n"
119
+ TABLE_REPLY = "{}\n\n{}\n".format(
120
+ SHORT_REPLY,
121
+ "\n".join(
122
+ f"| {build_filler_prose(MAXIMUM_WORDS_PER_LIST_LINE)} | cell |"
123
+ for _ in range(15)
124
+ ),
125
+ )
126
+ LONG_PROSE_PARAGRAPHS_REPLY = "\n\n".join(
127
+ build_filler_prose(MAXIMUM_WORDS_PER_LIST_LINE + 10) for _ in range(4)
128
+ )
150
129
 
151
130
 
152
131
  def run_hook_with_payload(payload: dict) -> subprocess.CompletedProcess:
@@ -171,8 +150,8 @@ def test_blocker_uses_shared_strip_code_and_quotes() -> None:
171
150
  assert eli11_reply_enforcer.strip_code_and_quotes is strip_code_and_quotes
172
151
 
173
152
 
174
- def test_short_reply_passes_through() -> None:
175
- """A reply under the enforced word floor is never judged."""
153
+ def test_short_correct_reply_passes_through() -> None:
154
+ """A concise outcome reply passes without padding."""
176
155
  completed_process = run_hook_with_message(SHORT_REPLY)
177
156
  assert completed_process.returncode == 0
178
157
  assert completed_process.stdout == ""
@@ -188,84 +167,56 @@ def test_empty_message_passes_through() -> None:
188
167
  def test_stop_hook_active_flag_passes_through() -> None:
189
168
  """A re-entrant Stop invocation never blocks again."""
190
169
  completed_process = run_hook_with_payload(
191
- {"last_assistant_message": OVERLONG_REPLY, "stop_hook_active": True}
170
+ {
171
+ "last_assistant_message": ACTION_WITHOUT_STEPS_FIRST_REPLY,
172
+ "stop_hook_active": True,
173
+ }
192
174
  )
193
175
  assert completed_process.returncode == 0
194
176
  assert completed_process.stdout == ""
195
177
 
196
178
 
197
- def test_reply_over_word_cap_emits_block() -> None:
198
- """A reply past the word cap blocks with the count and the cap named."""
199
- completed_process = run_hook_with_message(OVERLONG_REPLY)
200
- assert completed_process.returncode == 0
201
- parsed_response = json.loads(completed_process.stdout)
202
- assert parsed_response["decision"] == "block"
203
- assert str(OVERLONG_REPLY_WORD_COUNT) in parsed_response["reason"]
204
- assert str(MAXIMUM_REPLY_WORD_COUNT) in parsed_response["reason"]
205
-
206
-
207
- def test_reply_just_over_word_cap_emits_block() -> None:
208
- """A reply one word past the cap blocks on length alone."""
209
- completed_process = run_hook_with_message(JUST_OVER_WORD_CAP_REPLY)
210
- assert completed_process.returncode == 0
211
- parsed_response = json.loads(completed_process.stdout)
212
- assert parsed_response["decision"] == "block"
213
- assert (
214
- f"{JUST_OVER_WORD_CAP_WORD_COUNT} words, over the "
215
- f"{MAXIMUM_REPLY_WORD_COUNT}-word cap"
216
- ) in parsed_response["reason"]
217
-
218
-
219
- def test_reply_under_word_floor_passes_through() -> None:
220
- """A reply one word under the floor is never judged."""
221
- completed_process = run_hook_with_message(UNDER_FLOOR_REPLY)
179
+ def test_long_requested_report_passes_without_magic_prefix() -> None:
180
+ """A thorough report passes with no Long form: prefix and no word ceiling."""
181
+ completed_process = run_hook_with_message(REQUESTED_FULL_REPORT)
222
182
  assert completed_process.returncode == 0
223
183
  assert completed_process.stdout == ""
224
184
 
225
185
 
226
- def test_reply_at_the_word_floor_is_judged_for_action_first() -> None:
227
- """A reply of exactly the floor word count earns its action-first violation."""
228
- completed_process = run_hook_with_message(AT_FLOOR_ACTION_REPLY)
186
+ def test_long_prose_paragraphs_are_not_overpacked() -> None:
187
+ """Plain paragraphs over the list-line word cap do not block."""
188
+ completed_process = run_hook_with_message(LONG_PROSE_PARAGRAPHS_REPLY)
229
189
  assert completed_process.returncode == 0
230
- parsed_response = json.loads(completed_process.stdout)
231
- assert parsed_response["decision"] == "block"
232
- assert "put the steps first" in parsed_response["reason"]
190
+ assert completed_process.stdout == ""
233
191
 
234
192
 
235
- def test_reply_just_over_word_floor_is_judged() -> None:
236
- """A bullet-heavy reply just over the floor earns its bullet violation."""
237
- completed_process = run_hook_with_message(SEVEN_BULLET_JUST_OVER_FLOOR_REPLY)
193
+ def test_single_line_instruction_passes_through() -> None:
194
+ """A one-line imperative does not require a numbered list."""
195
+ completed_process = run_hook_with_message(SINGLE_LINE_INSTRUCTION)
238
196
  assert completed_process.returncode == 0
239
- parsed_response = json.loads(completed_process.stdout)
240
- assert parsed_response["decision"] == "block"
241
- assert (
242
- f"cut findings to {TARGET_BULLET_LINE_COUNT} bullets"
243
- in parsed_response["reason"]
244
- )
197
+ assert completed_process.stdout == ""
245
198
 
246
199
 
247
- def test_three_overpacked_lines_emit_block() -> None:
248
- """One line past the overpacked-line cap blocks on one idea per line."""
249
- completed_process = run_hook_with_message(THREE_OVERPACKED_LINE_REPLY)
200
+ def test_three_overpacked_list_lines_emit_block() -> None:
201
+ """List lines past the overpacked-list cap block on one idea per bullet."""
202
+ completed_process = run_hook_with_message(THREE_OVERPACKED_LIST_LINE_REPLY)
250
203
  assert completed_process.returncode == 0
251
204
  parsed_response = json.loads(completed_process.stdout)
252
205
  assert parsed_response["decision"] == "block"
253
- assert "lines carry too many words - one idea per line" in (
254
- parsed_response["reason"]
255
- )
206
+ assert "list lines carry too many words" in parsed_response["reason"]
256
207
 
257
208
 
258
- def test_two_overpacked_lines_pass_through() -> None:
259
- """Over-packed lines at the cap pass."""
260
- completed_process = run_hook_with_message(TWO_OVERPACKED_LINE_REPLY)
209
+ def test_two_overpacked_list_lines_pass_through() -> None:
210
+ """Over-packed list lines at the cap pass."""
211
+ completed_process = run_hook_with_message(TWO_OVERPACKED_LIST_LINE_REPLY)
261
212
  assert completed_process.returncode == 0
262
213
  assert completed_process.stdout == ""
263
214
 
264
215
 
265
216
  def test_bullet_marker_is_not_counted_as_a_line_word() -> None:
266
- """A bullet at the per-line cap stays under it once its marker comes off."""
217
+ """A bullet at the per-list-line cap stays under it once its marker comes off."""
267
218
  capped_bullet_lines = build_bullet_block(
268
- MAXIMUM_BULLET_LINE_COUNT - 2, MAXIMUM_WORDS_PER_LINE
219
+ MAXIMUM_BULLET_LINE_COUNT - 2, MAXIMUM_WORDS_PER_LIST_LINE
269
220
  )
270
221
  all_violations = eli11_reply_enforcer.find_reply_shape_violations(
271
222
  capped_bullet_lines
@@ -273,41 +224,14 @@ def test_bullet_marker_is_not_counted_as_a_line_word() -> None:
273
224
  assert all_violations == []
274
225
 
275
226
 
276
- def test_long_form_prefix_exempts_an_overlong_reply() -> None:
277
- """The Long form escape hatch clears every reply-shape check."""
278
- completed_process = run_hook_with_message(LONG_FORM_OVERLONG_REPLY)
279
- assert completed_process.returncode == 0
280
- assert completed_process.stdout == ""
281
-
282
-
283
- def test_bold_long_form_prefix_exempts_an_overlong_reply() -> None:
284
- """A bold-wrapped Long form prefix still opts the reply out."""
285
- completed_process = run_hook_with_message(BOLD_LONG_FORM_OVERLONG_REPLY)
286
- assert completed_process.returncode == 0
287
- assert completed_process.stdout == ""
288
-
289
-
290
- def test_quoted_long_form_prefix_exempts_an_overlong_reply() -> None:
291
- """A blockquoted Long form prefix still opts the reply out."""
292
- completed_process = run_hook_with_message(QUOTED_LONG_FORM_OVERLONG_REPLY)
293
- assert completed_process.returncode == 0
294
- assert completed_process.stdout == ""
295
-
296
-
297
- def test_heading_long_form_prefix_exempts_an_overlong_reply() -> None:
298
- """A heading-wrapped Long form prefix still opts the reply out."""
299
- completed_process = run_hook_with_message(HEADING_LONG_FORM_OVERLONG_REPLY)
300
- assert completed_process.returncode == 0
301
- assert completed_process.stdout == ""
302
-
303
-
304
227
  def test_instruction_lines_without_leading_steps_emit_block() -> None:
305
228
  """Imperative instructions buried under prose block with a steps-first message."""
306
229
  completed_process = run_hook_with_message(ACTION_WITHOUT_STEPS_FIRST_REPLY)
307
230
  assert completed_process.returncode == 0
308
231
  parsed_response = json.loads(completed_process.stdout)
309
232
  assert parsed_response["decision"] == "block"
310
- assert "put the steps first" in parsed_response["reason"]
233
+ assert "numbered steps" in parsed_response["reason"]
234
+ assert "Rewrite the reply" in parsed_response["reason"]
311
235
 
312
236
 
313
237
  def test_numbered_steps_in_lead_lines_pass_through() -> None:
@@ -350,11 +274,11 @@ def test_names_imperative_object_accepts_determiners_counts_and_paths() -> None:
350
274
  def test_strip_markdown_lead_markers_removes_every_wrapper() -> None:
351
275
  """Blockquote, heading, and bold wrappers come off the front of a line."""
352
276
  assert eli11_reply_enforcer.strip_markdown_lead_markers(
353
- "> **Long form:** the report follows"
354
- ) == "Long form:** the report follows"
277
+ "> **Bold lead:** the report follows"
278
+ ) == "Bold lead:** the report follows"
355
279
  assert eli11_reply_enforcer.strip_markdown_lead_markers(
356
- "# Long form: the report follows"
357
- ) == "Long form: the report follows"
280
+ "# Heading lead: the report follows"
281
+ ) == "Heading lead: the report follows"
358
282
 
359
283
 
360
284
  def test_install_instruction_without_leading_steps_emits_block() -> None:
@@ -363,7 +287,7 @@ def test_install_instruction_without_leading_steps_emits_block() -> None:
363
287
  assert completed_process.returncode == 0
364
288
  parsed_response = json.loads(completed_process.stdout)
365
289
  assert parsed_response["decision"] == "block"
366
- assert "put the steps first" in parsed_response["reason"]
290
+ assert "numbered steps" in parsed_response["reason"]
367
291
 
368
292
 
369
293
  def test_do_three_things_without_leading_steps_emits_block() -> None:
@@ -372,7 +296,7 @@ def test_do_three_things_without_leading_steps_emits_block() -> None:
372
296
  assert completed_process.returncode == 0
373
297
  parsed_response = json.loads(completed_process.stdout)
374
298
  assert parsed_response["decision"] == "block"
375
- assert "put the steps first" in parsed_response["reason"]
299
+ assert "numbered steps" in parsed_response["reason"]
376
300
 
377
301
 
378
302
  def test_do_three_things_with_numbered_steps_passes_through() -> None:
@@ -398,13 +322,13 @@ def test_numbered_step_opener_is_a_numbered_step() -> None:
398
322
 
399
323
 
400
324
  def test_more_than_six_bullets_emits_block() -> None:
401
- """One bullet past the cap blocks with the cut-to-target-bullets message."""
325
+ """One bullet past the cap blocks with the put-findings message."""
402
326
  completed_process = run_hook_with_message(SEVEN_BULLET_REPLY)
403
327
  assert completed_process.returncode == 0
404
328
  parsed_response = json.loads(completed_process.stdout)
405
329
  assert parsed_response["decision"] == "block"
406
330
  assert (
407
- f"cut findings to {TARGET_BULLET_LINE_COUNT} bullets"
331
+ f"put findings in at most {TARGET_BULLET_LINE_COUNT} bullets"
408
332
  in parsed_response["reason"]
409
333
  )
410
334
 
@@ -416,29 +340,29 @@ def test_six_bullets_pass_through() -> None:
416
340
  assert completed_process.stdout == ""
417
341
 
418
342
 
419
- def test_fenced_code_words_are_not_counted() -> None:
420
- """Words inside a fenced code block never push a reply past the cap."""
343
+ def test_fenced_code_does_not_force_shape_block() -> None:
344
+ """Words inside a fenced code block do not create shape violations alone."""
421
345
  completed_process = run_hook_with_message(FENCED_CODE_REPLY)
422
346
  assert completed_process.returncode == 0
423
347
  assert completed_process.stdout == ""
424
348
 
425
349
 
426
- def test_blockquote_words_are_not_counted() -> None:
427
- """Quoted lines are the user's words and never push a reply past the cap."""
350
+ def test_blockquote_does_not_force_shape_block() -> None:
351
+ """Quoted lines are exempt from shape counting."""
428
352
  completed_process = run_hook_with_message(BLOCKQUOTE_REPLY)
429
353
  assert completed_process.returncode == 0
430
354
  assert completed_process.stdout == ""
431
355
 
432
356
 
433
357
  def test_table_rows_are_not_counted() -> None:
434
- """Table rows carry reference data and never push a reply past the cap."""
358
+ """Table rows carry reference data and do not create shape violations alone."""
435
359
  completed_process = run_hook_with_message(TABLE_REPLY)
436
360
  assert completed_process.returncode == 0
437
361
  assert completed_process.stdout == ""
438
362
 
439
363
 
440
364
  def test_urls_are_removed_from_counted_prose() -> None:
441
- """A link target is stripped before the words are counted."""
365
+ """A link target is stripped before prose is judged."""
442
366
  prose_text = eli11_reply_enforcer.extract_reply_prose(
443
367
  "The draft is at https://github.com/owner/repo/pull/704 now"
444
368
  )
@@ -446,12 +370,21 @@ def test_urls_are_removed_from_counted_prose() -> None:
446
370
  assert "draft" in prose_text
447
371
 
448
372
 
449
- def test_block_response_json_shape() -> None:
450
- """The block payload carries the Stop-hook keys and names the escape hatch."""
451
- completed_process = run_hook_with_message(OVERLONG_REPLY)
373
+ def test_block_reason_uses_positive_rewrite_language() -> None:
374
+ """Corrective output tells the model what to write, not only what failed."""
375
+ reason = eli11_reply_enforcer.build_block_reason(
376
+ [eli11_reply_enforcer.describe_action_first_violation()]
377
+ )
378
+ assert "Rewrite the reply" in reason
379
+ assert "120-word" not in reason
380
+ assert "Long form:" not in reason
381
+ assert "minimum" not in reason.lower()
382
+
383
+
384
+ def test_hook_never_forces_padding_on_short_replies() -> None:
385
+ """A correct short reply is not blocked for being under a word floor."""
386
+ tiny_reply = "Done."
387
+ completed_process = run_hook_with_message(tiny_reply)
452
388
  assert completed_process.returncode == 0
453
- parsed_response = json.loads(completed_process.stdout)
454
- assert parsed_response["decision"] == "block"
455
- assert parsed_response["suppressOutput"] is True
456
- assert parsed_response["systemMessage"]
457
- assert "Long form:" in parsed_response["reason"]
389
+ assert completed_process.stdout == ""
390
+ assert eli11_reply_enforcer.find_reply_shape_violations(tiny_reply) == []
@@ -11,8 +11,9 @@ field, pass whether or not the marker is present. One test reads
11
11
  spawn tool names.
12
12
 
13
13
  Token pins read the warm-up section and each consuming skill. Two doc-gate
14
- agreement tests assemble a spawn prompt out of the advisor-protocol wording a
15
- warm-up bind and a drift re-spawn follow, and run each through the gate, so
14
+ agreement tests assemble a spawn prompt out of the advisor reference wording a
15
+ warm-up bind (``reference/warm-up.md``) and a drift re-spawn
16
+ (``reference/lifecycle.md``) follow, and run each through the gate, so
16
17
  wording that stops naming the marker fails here. Two deny-path tests read the
17
18
  preview the gate hands the block logger and hold it bounded and scoped to the
18
19
  model field.
@@ -64,6 +65,13 @@ _FULL_SONNET_MODEL_ID = "claude-sonnet-4-5"
64
65
  _PACKAGE_ROOT = _HOOKS_TREE.parent
65
66
  _ADVISOR_PROTOCOL_PATH = _PACKAGE_ROOT / "_shared" / "advisor" / "advisor-protocol.md"
66
67
  _ADVISOR_PROTOCOL_TEXT = _ADVISOR_PROTOCOL_PATH.read_text(encoding="utf-8")
68
+ _ADVISOR_REFERENCE_DIR = _PACKAGE_ROOT / "_shared" / "advisor" / "reference"
69
+ _ADVISOR_LIFECYCLE_TEXT = (_ADVISOR_REFERENCE_DIR / "lifecycle.md").read_text(
70
+ encoding="utf-8"
71
+ )
72
+ _ADVISOR_WARM_UP_TEXT = (_ADVISOR_REFERENCE_DIR / "warm-up.md").read_text(
73
+ encoding="utf-8"
74
+ )
67
75
  _ALL_CONSUMING_SKILL_NAMES = ("team-advisor", "orchestrator", "orchestrator-refresh")
68
76
  _ALL_CONSUMING_SKILL_PATHS = tuple(
69
77
  _PACKAGE_ROOT / "skills" / each_skill_name / "SKILL.md"
@@ -321,19 +329,18 @@ def test_consuming_skill_names_the_marker_token(skill_path: pathlib.Path) -> Non
321
329
  assert FABLE_SPAWN_AUTHORIZATION_MARKER in skill_path.read_text(encoding="utf-8")
322
330
 
323
331
 
324
- def _paragraph_starting_at(paragraph_marker: str) -> str:
325
- """Return the advisor-protocol paragraph that opens with a marker.
332
+ def _paragraph_starting_at(source_text: str, paragraph_marker: str) -> str:
333
+ """Return the advisor-doc paragraph that opens with a marker.
326
334
 
327
335
  Args:
336
+ source_text: The advisor document text holding the paragraph.
328
337
  paragraph_marker: The literal text opening the paragraph.
329
338
 
330
339
  Returns:
331
340
  The paragraph text, running from that marker to the blank line that
332
341
  closes it.
333
342
  """
334
- paragraph_body = _ADVISOR_PROTOCOL_TEXT[
335
- _ADVISOR_PROTOCOL_TEXT.index(paragraph_marker) :
336
- ]
343
+ paragraph_body = source_text[source_text.index(paragraph_marker) :]
337
344
  paragraph_end = paragraph_body.find(_PARAGRAPH_SEPARATOR)
338
345
  if paragraph_end < 0:
339
346
  return paragraph_body
@@ -341,7 +348,7 @@ def _paragraph_starting_at(paragraph_marker: str) -> str:
341
348
 
342
349
 
343
350
  def _respawn_spawn_prompt() -> str:
344
- """Assemble the spawn prompt a drift re-spawn writes from the protocol.
351
+ """Assemble the spawn prompt a drift re-spawn writes from the lifecycle doc.
345
352
 
346
353
  The prompt comes from the re-spawn paragraph alone, so the gate reads
347
354
  what that one paragraph tells a session to send.
@@ -349,7 +356,7 @@ def _respawn_spawn_prompt() -> str:
349
356
  Returns:
350
357
  The spawn prompt text a session following that paragraph sends.
351
358
  """
352
- return _paragraph_starting_at(_RESPAWN_PARAGRAPH_MARKER)
359
+ return _paragraph_starting_at(_ADVISOR_LIFECYCLE_TEXT, _RESPAWN_PARAGRAPH_MARKER)
353
360
 
354
361
 
355
362
  def test_respawn_paragraph_prompt_passes_the_gate_at_the_fable_tier() -> None:
@@ -358,7 +365,7 @@ def test_respawn_paragraph_prompt_passes_the_gate_at_the_fable_tier() -> None:
358
365
 
359
366
 
360
367
  def _warm_up_spawn_prompt() -> str:
361
- """Assemble the spawn prompt a warm-up bind writes from the protocol.
368
+ """Assemble the spawn prompt a warm-up bind writes from the warm-up doc.
362
369
 
363
370
  The prompt comes from the spawn-field prompt bullet alone, so the gate
364
371
  reads what that one bullet tells a session to send.
@@ -366,7 +373,7 @@ def _warm_up_spawn_prompt() -> str:
366
373
  Returns:
367
374
  The spawn prompt text a session following that bullet sends.
368
375
  """
369
- return _paragraph_starting_at(_WARM_UP_PROMPT_BULLET_MARKER)
376
+ return _paragraph_starting_at(_ADVISOR_WARM_UP_TEXT, _WARM_UP_PROMPT_BULLET_MARKER)
370
377
 
371
378
 
372
379
  def test_warm_up_prompt_bullet_passes_the_gate_at_the_fable_tier() -> None: