claude-dev-env 1.58.0 → 1.60.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 (106) 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-b-selector-engine-compat.md +1 -1
  9. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
  10. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  11. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  12. package/bin/install.mjs +100 -27
  13. package/bin/install.test.mjs +133 -1
  14. package/docs/CODE_RULES.md +3 -3
  15. package/hooks/blocking/code_rules_annotations_length.py +153 -0
  16. package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
  17. package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
  18. package/hooks/blocking/code_rules_duplicate_body.py +439 -0
  19. package/hooks/blocking/code_rules_enforcer.py +190 -21
  20. package/hooks/blocking/code_rules_magic_values.py +98 -0
  21. package/hooks/blocking/code_rules_shared.py +41 -0
  22. package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
  23. package/hooks/blocking/config/__init__.py +5 -0
  24. package/hooks/blocking/config/verified_commit_constants.py +106 -0
  25. package/hooks/blocking/destructive_command_blocker.py +1027 -12
  26. package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
  27. package/hooks/blocking/subprocess_budget_completeness.py +380 -0
  28. package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
  29. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
  30. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
  31. package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
  32. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
  33. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
  34. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
  35. package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
  36. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
  37. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
  38. package/hooks/blocking/test_destructive_command_blocker.py +622 -3
  39. package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
  40. package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
  41. package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
  42. package/hooks/blocking/test_verification_verdict_store.py +278 -0
  43. package/hooks/blocking/test_verified_commit_gate.py +368 -0
  44. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
  45. package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
  46. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
  47. package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
  48. package/hooks/blocking/verification_verdict_store.py +446 -0
  49. package/hooks/blocking/verified_commit_gate.py +523 -0
  50. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
  51. package/hooks/blocking/verifier_verdict_minter.py +299 -0
  52. package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
  53. package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
  54. package/hooks/hooks.json +58 -1
  55. package/hooks/hooks_constants/blocking_check_limits.py +1 -0
  56. package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
  57. package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
  58. package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
  59. package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
  60. package/hooks/hooks_constants/duplicate_function_body_constants.py +34 -0
  61. package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
  62. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
  63. package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
  64. package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
  65. package/package.json +1 -1
  66. package/rules/docstring-prose-matches-implementation.md +43 -0
  67. package/rules/file-global-constants.md +7 -1
  68. package/rules/hook-prose-matches-detector.md +26 -0
  69. package/rules/no-cross-skill-duplicate-helpers.md +29 -0
  70. package/rules/no-inline-destructive-literals.md +11 -0
  71. package/rules/workflow-substitution-slots.md +7 -0
  72. package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
  73. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
  74. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
  75. package/skills/autoconverge/SKILL.md +67 -19
  76. package/skills/autoconverge/reference/closing-report.md +59 -17
  77. package/skills/autoconverge/reference/convergence.md +7 -3
  78. package/skills/autoconverge/reference/stop-conditions.md +7 -2
  79. package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
  80. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
  81. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
  82. package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
  83. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
  84. package/skills/autoconverge/workflow/converge.mjs +234 -42
  85. package/skills/autoconverge/workflow/convergence_summary.py +110 -0
  86. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
  87. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
  88. package/skills/autoconverge/workflow/render_report.py +488 -397
  89. package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
  90. package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
  91. package/skills/autoconverge/workflow/test_render_report.py +488 -259
  92. package/skills/pr-converge/reference/per-tick.md +28 -8
  93. package/skills/pr-converge/scripts/check_convergence.py +195 -64
  94. package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
  95. package/skills/rebase/SKILL.md +2 -4
  96. package/skills/update/SKILL.md +37 -5
  97. package/system-prompts/software-engineer.xml +2 -6
  98. package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
  99. package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
  100. package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
  101. package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
  102. package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
  103. package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
  104. package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
  105. package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
  106. package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
@@ -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 {{{") == ""