claude-dev-env 1.57.2 → 1.59.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 (77) hide show
  1. package/CLAUDE.md +2 -2
  2. package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
  3. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  5. package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
  6. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
  7. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
  8. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  9. package/bin/install.mjs +317 -54
  10. package/bin/install.test.mjs +478 -3
  11. package/docs/CODE_RULES.md +3 -3
  12. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  13. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  14. package/hooks/blocking/code_rules_duplicate_body.py +287 -0
  15. package/hooks/blocking/code_rules_enforcer.py +175 -21
  16. package/hooks/blocking/code_rules_magic_values.py +98 -0
  17. package/hooks/blocking/code_rules_shared.py +41 -0
  18. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  19. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  20. package/hooks/blocking/intent_only_ending_blocker.py +155 -0
  21. package/hooks/blocking/session_handoff_blocker.py +190 -0
  22. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  23. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  24. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  25. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  26. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  27. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  28. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  29. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  30. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  31. package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
  32. package/hooks/blocking/test_session_handoff_blocker.py +312 -0
  33. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  34. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  35. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  36. package/hooks/hooks.json +25 -0
  37. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  38. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  39. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  40. package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
  41. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  42. package/hooks/hooks_constants/messages.py +4 -0
  43. package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
  44. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  45. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  46. package/hooks/workflow/auto_formatter.py +26 -1
  47. package/hooks/workflow/test_auto_formatter.py +134 -0
  48. package/package.json +1 -1
  49. package/rules/conservative-action.md +1 -0
  50. package/rules/docstring-prose-matches-implementation.md +43 -0
  51. package/rules/hook-prose-matches-detector.md +26 -0
  52. package/rules/long-horizon-autonomy.md +43 -0
  53. package/rules/no-inline-destructive-literals.md +11 -0
  54. package/rules/workflow-substitution-slots.md +7 -0
  55. package/skills/autoconverge/SKILL.md +68 -6
  56. package/skills/autoconverge/reference/closing-report.md +44 -0
  57. package/skills/autoconverge/reference/convergence.md +7 -3
  58. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  59. package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  60. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
  61. package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
  62. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  63. package/skills/autoconverge/workflow/converge.mjs +106 -38
  64. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
  65. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
  66. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
  67. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
  68. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
  69. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
  70. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
  71. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
  72. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
  73. package/skills/autoconverge/workflow/render_report.py +903 -0
  74. package/skills/autoconverge/workflow/test_render_report.py +484 -0
  75. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  76. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  77. package/skills/update/SKILL.md +37 -5
@@ -0,0 +1,265 @@
1
+ """Unit tests for hook_prose_detector_consistency PreToolUse hook."""
2
+
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ import pathlib
7
+ import sys
8
+ from unittest import mock
9
+
10
+ _HOOK_DIR = pathlib.Path(__file__).parent
11
+ _HOOKS_ROOT = _HOOK_DIR.parent
12
+ for _each_root in (str(_HOOK_DIR), str(_HOOKS_ROOT)):
13
+ if _each_root not in sys.path:
14
+ sys.path.insert(0, _each_root)
15
+
16
+ hook_spec = importlib.util.spec_from_file_location(
17
+ "hook_prose_detector_consistency",
18
+ _HOOK_DIR / "hook_prose_detector_consistency.py",
19
+ )
20
+ assert hook_spec is not None
21
+ assert hook_spec.loader is not None
22
+ hook_module = importlib.util.module_from_spec(hook_spec)
23
+ hook_spec.loader.exec_module(hook_module)
24
+
25
+ content_has_violation = hook_module.content_has_violation
26
+ claims_output_key_trigger = hook_module.claims_output_key_trigger
27
+ detects_only_path_shape = hook_module.detects_only_path_shape
28
+ is_constants_module = hook_module.is_constants_module
29
+ is_hook_python_module = hook_module.is_hook_python_module
30
+ is_own_detector_family = hook_module.is_own_detector_family
31
+ written_content = hook_module.written_content
32
+
33
+ _BLOCKER_MODULE_PATH = "/repo/hooks/blocking/some_blocker.py"
34
+ _CONSTANTS_MODULE_PATH = "/repo/hooks/hooks_constants/some_blocker_constants.py"
35
+
36
+ _OWN_HOOK_PATH = "/repo/packages/x/hooks/blocking/hook_prose_detector_consistency.py"
37
+ _OWN_CONSTANTS_PATH = (
38
+ "/repo/packages/x/hooks/hooks_constants/hook_prose_detector_consistency_constants.py"
39
+ )
40
+ _OWN_TEST_PATH = "/repo/packages/x/hooks/blocking/test_hook_prose_detector_consistency.py"
41
+
42
+
43
+ _OVERSTATED_MESSAGE_MODULE = (
44
+ 'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
45
+ "CORRECTIVE_MESSAGE = (\n"
46
+ ' "A bare per-iteration index token (for example `cand_i`) appears as a path "\n'
47
+ ' "or output-key segment inside a looping block."\n'
48
+ ")\n"
49
+ )
50
+
51
+ _FIXED_MESSAGE_MODULE = (
52
+ 'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
53
+ "CORRECTIVE_MESSAGE = (\n"
54
+ ' "A bare per-iteration index token (for example `cand_i`) appears as a "\n'
55
+ ' "per-iteration path segment inside a looping block."\n'
56
+ ")\n"
57
+ )
58
+
59
+
60
+ def test_overstated_path_shape_module_is_flagged() -> None:
61
+ assert content_has_violation(_OVERSTATED_MESSAGE_MODULE, _BLOCKER_MODULE_PATH) is True
62
+
63
+
64
+ def test_fixed_path_shape_module_passes() -> None:
65
+ assert content_has_violation(_FIXED_MESSAGE_MODULE, _BLOCKER_MODULE_PATH) is False
66
+
67
+
68
+ def test_output_key_claim_without_path_detector_in_blocker_passes() -> None:
69
+ no_path_detector = (
70
+ 'pattern = re.compile(r"[A-Za-z]+")\n'
71
+ 'CORRECTIVE_MESSAGE = "blocks an output-key segment"\n'
72
+ )
73
+ assert content_has_violation(no_path_detector, _BLOCKER_MODULE_PATH) is False
74
+
75
+
76
+ def test_output_key_claim_alone_in_constants_module_is_flagged() -> None:
77
+ constants_only = 'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n'
78
+ assert content_has_violation(constants_only, _CONSTANTS_MODULE_PATH) is True
79
+
80
+
81
+ def test_constants_module_without_output_key_claim_passes() -> None:
82
+ clean_constants = 'CORRECTIVE_MESSAGE = "appears as a per-iteration path segment"\n'
83
+ assert content_has_violation(clean_constants, _CONSTANTS_MODULE_PATH) is False
84
+
85
+
86
+ def test_path_detector_without_output_key_claim_passes() -> None:
87
+ path_only = 'path_context = re.compile(r"(?:[\\\\/]\\s*([A-Za-z][\\w]*?_[ijk])")\n'
88
+ assert content_has_violation(path_only, _BLOCKER_MODULE_PATH) is False
89
+
90
+
91
+ def test_space_separated_output_key_phrase_is_flagged() -> None:
92
+ space_variant = (
93
+ 'path_context = re.compile(r"(?:[\\\\/]\\s*(token))")\n'
94
+ 'message = "appears as a path or output key segment"\n'
95
+ )
96
+ assert content_has_violation(space_variant, _BLOCKER_MODULE_PATH) is True
97
+
98
+
99
+ def test_own_hook_module_is_exempt_from_self_lockout() -> None:
100
+ own_hook_content = pathlib.Path(hook_module.__file__).read_text(encoding="utf-8")
101
+ assert content_has_violation(own_hook_content, _OWN_HOOK_PATH) is False
102
+
103
+
104
+ def test_own_constants_module_is_exempt_from_self_lockout() -> None:
105
+ own_constants_path = (
106
+ _HOOKS_ROOT
107
+ / "hooks_constants"
108
+ / "hook_prose_detector_consistency_constants.py"
109
+ )
110
+ own_constants_content = own_constants_path.read_text(encoding="utf-8")
111
+ assert content_has_violation(own_constants_content, _OWN_CONSTANTS_PATH) is False
112
+
113
+
114
+ def test_own_test_module_is_exempt_from_self_lockout() -> None:
115
+ own_test_content = pathlib.Path(__file__).read_text(encoding="utf-8")
116
+ assert content_has_violation(own_test_content, _OWN_TEST_PATH) is False
117
+
118
+
119
+ def test_slot_blocker_constants_companion_passes_at_its_real_path() -> None:
120
+ slot_constants_path = (
121
+ _HOOKS_ROOT
122
+ / "hooks_constants"
123
+ / "workflow_substitution_slot_blocker_constants.py"
124
+ )
125
+ slot_constants_content = slot_constants_path.read_text(encoding="utf-8")
126
+ assert content_has_violation(slot_constants_content, str(slot_constants_path)) is False
127
+
128
+
129
+ def test_is_own_detector_family_recognizes_hook_module() -> None:
130
+ assert is_own_detector_family(_OWN_HOOK_PATH) is True
131
+
132
+
133
+ def test_is_own_detector_family_recognizes_constants_companion() -> None:
134
+ assert is_own_detector_family(_OWN_CONSTANTS_PATH) is True
135
+
136
+
137
+ def test_is_own_detector_family_recognizes_test_module() -> None:
138
+ assert is_own_detector_family(_OWN_TEST_PATH) is True
139
+
140
+
141
+ def test_is_own_detector_family_rejects_unrelated_blocker() -> None:
142
+ assert is_own_detector_family(_BLOCKER_MODULE_PATH) is False
143
+
144
+
145
+ def test_unrelated_constants_module_still_flagged_after_exemption() -> None:
146
+ constants_only = 'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n'
147
+ assert content_has_violation(constants_only, _CONSTANTS_MODULE_PATH) is True
148
+
149
+
150
+ def test_is_constants_module_accepts_constants_suffix() -> None:
151
+ assert is_constants_module(_CONSTANTS_MODULE_PATH) is True
152
+
153
+
154
+ def test_is_constants_module_rejects_blocker_module() -> None:
155
+ assert is_constants_module(_BLOCKER_MODULE_PATH) is False
156
+
157
+
158
+ def test_claims_output_key_trigger_matches_hyphen_form() -> None:
159
+ assert claims_output_key_trigger("a path or output-key segment here") is True
160
+
161
+
162
+ def test_claims_output_key_trigger_ignores_unrelated_output_word() -> None:
163
+ assert claims_output_key_trigger("the output is written to disk") is False
164
+
165
+
166
+ def test_detects_only_path_shape_finds_separator_class() -> None:
167
+ assert detects_only_path_shape('re.compile(r"[\\\\/]token")') is True
168
+
169
+
170
+ def test_detects_only_path_shape_finds_backslash_only_class() -> None:
171
+ assert detects_only_path_shape(r'pat = re.compile(r"[\\]token")') is True
172
+
173
+
174
+ def test_detects_only_path_shape_false_without_separator_class() -> None:
175
+ assert detects_only_path_shape('re.compile(r"[A-Za-z]+")') is False
176
+
177
+
178
+ def test_is_hook_python_module_accepts_hooks_path() -> None:
179
+ assert is_hook_python_module("/repo/packages/x/hooks/blocking/some_blocker.py") is True
180
+
181
+
182
+ def test_is_hook_python_module_rejects_non_hook_path() -> None:
183
+ assert is_hook_python_module("/repo/src/blocking/some_blocker.py") is False
184
+
185
+
186
+ def test_is_hook_python_module_rejects_non_python_file() -> None:
187
+ assert is_hook_python_module("/repo/hooks/blocking/notes.md") is False
188
+
189
+
190
+ def test_written_content_reads_edit_new_string() -> None:
191
+ edit_input = {"new_string": "edited body"}
192
+ assert written_content("Edit", edit_input) == "edited body"
193
+
194
+
195
+ def _run_main_with_io(input_text: str) -> str:
196
+ with mock.patch("sys.stdin", io.StringIO(input_text)):
197
+ with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
198
+ try:
199
+ hook_module.main()
200
+ except SystemExit:
201
+ pass
202
+ return mock_stdout.getvalue()
203
+
204
+
205
+ def test_main_blocks_overstated_hook_module_write() -> None:
206
+ hook_input = {
207
+ "tool_name": "Write",
208
+ "tool_input": {
209
+ "file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
210
+ "content": _OVERSTATED_MESSAGE_MODULE,
211
+ },
212
+ }
213
+ output_text = _run_main_with_io(json.dumps(hook_input))
214
+ payload = json.loads(output_text)
215
+ assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
216
+
217
+
218
+ def test_main_blocks_overstated_hook_module_edit() -> None:
219
+ hook_input = {
220
+ "tool_name": "Edit",
221
+ "tool_input": {
222
+ "file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
223
+ "new_string": _OVERSTATED_MESSAGE_MODULE,
224
+ },
225
+ }
226
+ output_text = _run_main_with_io(json.dumps(hook_input))
227
+ payload = json.loads(output_text)
228
+ assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
229
+
230
+
231
+ def test_main_passes_fixed_hook_module_write() -> None:
232
+ hook_input = {
233
+ "tool_name": "Write",
234
+ "tool_input": {
235
+ "file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
236
+ "content": _FIXED_MESSAGE_MODULE,
237
+ },
238
+ }
239
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
240
+
241
+
242
+ def test_main_passes_non_hook_path() -> None:
243
+ hook_input = {
244
+ "tool_name": "Write",
245
+ "tool_input": {
246
+ "file_path": "/repo/src/some_blocker.py",
247
+ "content": _OVERSTATED_MESSAGE_MODULE,
248
+ },
249
+ }
250
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
251
+
252
+
253
+ def test_main_passes_wrong_tool_name() -> None:
254
+ hook_input = {
255
+ "tool_name": "Bash",
256
+ "tool_input": {
257
+ "file_path": "/repo/hooks/blocking/x.py",
258
+ "command": "echo output-key segment",
259
+ },
260
+ }
261
+ assert _run_main_with_io(json.dumps(hook_input)) == ""
262
+
263
+
264
+ def test_main_passes_malformed_json() -> None:
265
+ assert _run_main_with_io("not valid json {{{") == ""
@@ -0,0 +1,175 @@
1
+ """Tests for intent_only_ending_blocker hook response shape."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+
9
+ HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "intent_only_ending_blocker.py")
10
+ _HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
11
+ _HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
12
+ _HOOK_CONFIG_DIR = os.path.join(_HOOKS_ROOT, "hooks_constants")
13
+ if _HOOKS_DIR not in sys.path:
14
+ sys.path.insert(0, _HOOKS_DIR)
15
+ if _HOOKS_ROOT not in sys.path:
16
+ sys.path.insert(0, _HOOKS_ROOT)
17
+ import intent_only_ending_blocker
18
+ from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE
19
+
20
+ INTENT_ENDING_MESSAGE = "I'll now run the test suite and fix any failures that come up."
21
+ NEXT_STEPS_MESSAGE = "Next steps:"
22
+ COMPLETED_WORK_MESSAGE = "Done - all tests pass. The fix is in place."
23
+ BENIGN_PAST_TENSE_MESSAGE = "I implemented the parser and verified it against the fixtures."
24
+ CLEAN_MESSAGE = "The function returns the parsed payload to its caller."
25
+ COMMITMENT_PHRASING_MESSAGE = "I'll commit to keeping the API stable across the next release."
26
+ NAMING_COMMITMENT_MESSAGE = "Let me commit to a clear naming convention for these helpers."
27
+ GIT_COMMIT_INTENT_MESSAGE = "I'll commit the changes once the tests pass."
28
+ DEFERRED_TO_USER_CI_RUNS_MESSAGE = (
29
+ "Let me know if this looks right. The CI will run automatically on push."
30
+ )
31
+ DEFERRED_TO_USER_EITHER_WAY_MESSAGE = (
32
+ "Let me know which option you want. I can check either way."
33
+ )
34
+ USER_DIRECTED_NEXT_STEPS_MESSAGE = (
35
+ "All done. The PR is merged.\n\nNext steps: you should deploy when ready."
36
+ )
37
+ EMPTY_MESSAGE = ""
38
+
39
+
40
+ def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
41
+ hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
42
+ return subprocess.run(
43
+ [sys.executable, HOOK_SCRIPT_PATH],
44
+ input=hook_input_payload,
45
+ capture_output=True,
46
+ text=True,
47
+ check=False,
48
+ )
49
+
50
+
51
+ def run_hook_with_payload(hook_input_payload: dict) -> subprocess.CompletedProcess:
52
+ return subprocess.run(
53
+ [sys.executable, HOOK_SCRIPT_PATH],
54
+ input=json.dumps(hook_input_payload),
55
+ capture_output=True,
56
+ text=True,
57
+ check=False,
58
+ )
59
+
60
+
61
+ def test_user_facing_notice_matches_config_messages_module():
62
+ config_messages_path = os.path.join(_HOOK_CONFIG_DIR, "messages.py")
63
+ specification = importlib.util.spec_from_file_location("messages", config_messages_path)
64
+ module = importlib.util.module_from_spec(specification)
65
+ specification.loader.exec_module(module)
66
+
67
+ assert module.USER_FACING_INTENT_ENDING_NOTICE == USER_FACING_INTENT_ENDING_NOTICE
68
+ assert (
69
+ intent_only_ending_blocker.USER_FACING_INTENT_ENDING_NOTICE
70
+ == module.USER_FACING_INTENT_ENDING_NOTICE
71
+ )
72
+
73
+
74
+ def test_intent_ending_message_emits_block_with_short_user_notice():
75
+ completed_process = run_hook_with_message(INTENT_ENDING_MESSAGE)
76
+
77
+ assert completed_process.returncode == 0
78
+ parsed_response = json.loads(completed_process.stdout)
79
+
80
+ assert parsed_response["decision"] == "block"
81
+ assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
82
+ assert parsed_response["suppressOutput"] is True
83
+ assert "long-horizon-autonomy" in parsed_response["reason"]
84
+
85
+
86
+ def test_next_steps_lead_in_emits_block():
87
+ completed_process = run_hook_with_message(NEXT_STEPS_MESSAGE)
88
+
89
+ assert completed_process.returncode == 0
90
+ parsed_response = json.loads(completed_process.stdout)
91
+
92
+ assert parsed_response["decision"] == "block"
93
+ assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
94
+
95
+
96
+ def test_completed_work_summary_passes_through_with_no_output():
97
+ completed_process = run_hook_with_message(COMPLETED_WORK_MESSAGE)
98
+
99
+ assert completed_process.returncode == 0
100
+ assert completed_process.stdout == ""
101
+
102
+
103
+ def test_past_tense_summary_passes_through_with_no_output():
104
+ completed_process = run_hook_with_message(BENIGN_PAST_TENSE_MESSAGE)
105
+
106
+ assert completed_process.returncode == 0
107
+ assert completed_process.stdout == ""
108
+
109
+
110
+ def test_commitment_phrasing_passes_through_with_no_output():
111
+ completed_process = run_hook_with_message(COMMITMENT_PHRASING_MESSAGE)
112
+
113
+ assert completed_process.returncode == 0
114
+ assert completed_process.stdout == ""
115
+
116
+
117
+ def test_naming_commitment_passes_through_with_no_output():
118
+ completed_process = run_hook_with_message(NAMING_COMMITMENT_MESSAGE)
119
+
120
+ assert completed_process.returncode == 0
121
+ assert completed_process.stdout == ""
122
+
123
+
124
+ def test_git_commit_intent_emits_block():
125
+ completed_process = run_hook_with_message(GIT_COMMIT_INTENT_MESSAGE)
126
+
127
+ assert completed_process.returncode == 0
128
+ parsed_response = json.loads(completed_process.stdout)
129
+
130
+ assert parsed_response["decision"] == "block"
131
+ assert parsed_response["systemMessage"] == USER_FACING_INTENT_ENDING_NOTICE
132
+
133
+
134
+ def test_benign_opener_with_unrelated_work_verb_passes_through_with_no_output():
135
+ completed_process = run_hook_with_message(DEFERRED_TO_USER_CI_RUNS_MESSAGE)
136
+
137
+ assert completed_process.returncode == 0
138
+ assert completed_process.stdout == ""
139
+
140
+
141
+ def test_deferral_to_user_with_later_verb_passes_through_with_no_output():
142
+ completed_process = run_hook_with_message(DEFERRED_TO_USER_EITHER_WAY_MESSAGE)
143
+
144
+ assert completed_process.returncode == 0
145
+ assert completed_process.stdout == ""
146
+
147
+
148
+ def test_user_directed_next_steps_passes_through_with_no_output():
149
+ completed_process = run_hook_with_message(USER_DIRECTED_NEXT_STEPS_MESSAGE)
150
+
151
+ assert completed_process.returncode == 0
152
+ assert completed_process.stdout == ""
153
+
154
+
155
+ def test_clean_message_passes_through_with_no_output():
156
+ completed_process = run_hook_with_message(CLEAN_MESSAGE)
157
+
158
+ assert completed_process.returncode == 0
159
+ assert completed_process.stdout == ""
160
+
161
+
162
+ def test_empty_message_passes_through_with_no_output():
163
+ completed_process = run_hook_with_message(EMPTY_MESSAGE)
164
+
165
+ assert completed_process.returncode == 0
166
+ assert completed_process.stdout == ""
167
+
168
+
169
+ def test_stop_hook_active_short_circuits_with_no_output():
170
+ completed_process = run_hook_with_payload(
171
+ {"last_assistant_message": INTENT_ENDING_MESSAGE, "stop_hook_active": True}
172
+ )
173
+
174
+ assert completed_process.returncode == 0
175
+ assert completed_process.stdout == ""