claude-dev-env 2.19.0 → 2.21.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 (53) hide show
  1. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +31 -1
  2. package/.agents/skills/e-code-review/SKILL.md +12 -1
  3. package/.agents/skills/e-code-review/reference/fix.md +5 -1
  4. package/.agents/skills/e-code-review/reference/loop.md +4 -0
  5. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
  6. package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
  7. package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
  8. package/.agents/skills/pr-cleanup/SKILL.md +109 -11
  9. package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
  11. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
  13. package/docs/CODE_RULES.md +2 -0
  14. package/hooks/advisory/conftest.py +10 -0
  15. package/hooks/advisory/refactor_guard.py +250 -144
  16. package/hooks/advisory/refactor_guard_test_support.py +46 -0
  17. package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
  18. package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
  19. package/hooks/blocking/block_main_commit.py +66 -33
  20. package/hooks/blocking/code_rules_blast_radius.py +194 -0
  21. package/hooks/blocking/code_rules_enforcer.py +95 -0
  22. package/hooks/blocking/codex_apply_patch.py +238 -0
  23. package/hooks/blocking/test_block_main_commit.py +145 -0
  24. package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
  25. package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +148 -0
  26. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +1 -0
  27. package/hooks/blocking/test_destructive_command_blocker.py +154 -138
  28. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +52 -9
  29. package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
  30. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +71 -5
  31. package/hooks/git-hooks/AGENTS.md +1 -1
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
  33. package/hooks/git-hooks/post_commit.py +160 -51
  34. package/hooks/git-hooks/pre_commit.py +3 -3
  35. package/hooks/git-hooks/test_post_commit.py +203 -0
  36. package/hooks/git-hooks/test_pre_commit.py +2 -2
  37. package/hooks/hooks_constants/blast_radius_constants.py +14 -0
  38. package/hooks/hooks_constants/code_rules_enforcer_constants.py +1 -0
  39. package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
  40. package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
  41. package/hooks/observability/test_instructions_loaded_logger.py +54 -0
  42. package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
  43. package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
  44. package/hooks/validation/mypy_validator.py +213 -80
  45. package/hooks/validation/test_mypy_validator.py +288 -13
  46. package/hooks/workflow/auto_formatter.py +225 -93
  47. package/hooks/workflow/investigation_tracker_reset.py +2 -0
  48. package/hooks/workflow/test_auto_formatter.py +261 -12
  49. package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
  50. package/package.json +1 -1
  51. package/rules/failure-blast-radius.md +126 -0
  52. package/scripts/codex_compat_materializer.py +395 -17
  53. package/scripts/tests/test_codex_compat_materializer.py +17 -3
@@ -1,11 +1,16 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
+ """Format eligible newly written source files without blocking the write."""
4
+
5
+ from __future__ import annotations
6
+
3
7
  import importlib
4
8
  import json
5
9
  import os
6
10
  import platform
7
11
  import subprocess
8
12
  import sys
13
+ from collections.abc import Mapping
9
14
  from pathlib import Path
10
15
  from types import ModuleType
11
16
 
@@ -14,6 +19,37 @@ NOTIFICATION_UTILS_DIRECTORY = os.path.join(
14
19
  )
15
20
  sys.path.insert(0, NOTIFICATION_UTILS_DIRECTORY)
16
21
 
22
+ PYTHON_EXTENSIONS = frozenset({".py"})
23
+ JS_EXTENSIONS = frozenset({".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"})
24
+ JSON_EXTENSIONS = frozenset({".json"})
25
+ PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
26
+ HOOKS_DIR = os.path.join(PLUGIN_ROOT, "hooks") + os.sep
27
+ PYTHON_FORMAT_TIMEOUT_SECONDS = 12
28
+ JS_FORMAT_TIMEOUT_SECONDS = 30
29
+ GIT_LS_FILES_TIMEOUT_SECONDS = 5
30
+ WRITE_TOOL_NAME = "Write"
31
+ PYTHON_FORMATTER_NAME = "python"
32
+ PRETTIER_FORMATTER_NAME = "prettier"
33
+ NPX_EXECUTABLE = "npx.cmd" if os.name == "nt" else "npx"
34
+ FORMATTER_DIAGNOSTIC_TEMPLATE = "auto-formatter: %s: %s\n"
35
+ FORMATTER_DIAGNOSTIC_SEPARATOR = "\n"
36
+ FORMATTER_TIMEOUT_DIAGNOSTIC_TEMPLATE = "auto-formatter: %s timed out after %d seconds\n"
37
+ PRETTIER_CONFIG_NAMES = frozenset(
38
+ {
39
+ ".prettierrc",
40
+ ".prettierrc.json",
41
+ ".prettierrc.yml",
42
+ ".prettierrc.yaml",
43
+ ".prettierrc.js",
44
+ ".prettierrc.cjs",
45
+ ".prettierrc.mjs",
46
+ ".prettierrc.toml",
47
+ "prettier.config.js",
48
+ "prettier.config.cjs",
49
+ "prettier.config.mjs",
50
+ }
51
+ )
52
+
17
53
 
18
54
  def load_notification_utils() -> ModuleType | None:
19
55
  try:
@@ -41,40 +77,18 @@ def send_format_notification(file_path: str, formatter_name: str) -> None:
41
77
  pass
42
78
 
43
79
 
44
- PYTHON_EXTENSIONS = {".py"}
45
- JS_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs"}
46
- JSON_EXTENSIONS = {".json"}
47
- PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
48
- HOOKS_DIR = os.path.join(PLUGIN_ROOT, "hooks") + os.sep
49
- PYTHON_FORMAT_TIMEOUT_SECONDS = 12
50
- JS_FORMAT_TIMEOUT_SECONDS = 30
51
- GIT_LS_FILES_TIMEOUT_SECONDS = 5
52
- PRETTIER_CONFIG_NAMES = {
53
- ".prettierrc",
54
- ".prettierrc.json",
55
- ".prettierrc.yml",
56
- ".prettierrc.yaml",
57
- ".prettierrc.js",
58
- ".prettierrc.cjs",
59
- ".prettierrc.mjs",
60
- ".prettierrc.toml",
61
- "prettier.config.js",
62
- "prettier.config.cjs",
63
- "prettier.config.mjs",
64
- }
65
-
66
-
67
80
  def has_prettier_config(file_path: str) -> bool:
68
81
  each_ancestor = Path(file_path).resolve().parent
69
82
  while True:
70
- for config_name in PRETTIER_CONFIG_NAMES:
71
- if (each_ancestor / config_name).exists():
72
- return True
73
- parent = each_ancestor.parent
74
- if parent == each_ancestor:
75
- break
76
- each_ancestor = parent
77
- return False
83
+ if any(
84
+ (each_ancestor / each_config_name).exists()
85
+ for each_config_name in PRETTIER_CONFIG_NAMES
86
+ ):
87
+ return True
88
+ parent_directory = each_ancestor.parent
89
+ if parent_directory == each_ancestor:
90
+ return False
91
+ each_ancestor = parent_directory
78
92
 
79
93
 
80
94
  def budgeted_python_format_seconds() -> int:
@@ -89,9 +103,7 @@ def budgeted_python_format_seconds() -> int:
89
103
  bound: when commands are missing or time out the loops can spend more than
90
104
  this budget.
91
105
  """
92
- fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
93
- format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS
94
- return fix_phase_seconds + format_phase_seconds
106
+ return PYTHON_FORMAT_TIMEOUT_SECONDS * 2
95
107
 
96
108
 
97
109
  def is_untracked_in_git(file_path: str) -> bool:
@@ -100,82 +112,202 @@ def is_untracked_in_git(file_path: str) -> bool:
100
112
  try:
101
113
  git_check = subprocess.run(
102
114
  ["git", "ls-files", "--error-unmatch", file_path],
103
- check=False, capture_output=True,
115
+ check=False,
116
+ capture_output=True,
104
117
  text=True,
105
118
  cwd=containing_directory,
106
119
  timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
120
+ env=_build_git_command_environment(),
107
121
  )
108
122
  return git_check.returncode != 0
109
123
  except (FileNotFoundError, subprocess.TimeoutExpired):
110
124
  return False
111
125
 
112
126
 
113
- def main() -> None:
127
+ def _build_git_command_environment() -> dict[str, str]:
128
+ return {
129
+ each_name: each_value
130
+ for each_name, each_value in os.environ.items()
131
+ if not each_name.upper().startswith("GIT_")
132
+ }
133
+
134
+
135
+ def _is_path_under_directory(candidate_path: str, directory_path: str) -> bool:
136
+ try:
137
+ return os.path.commonpath((candidate_path, directory_path)) == directory_path
138
+ except ValueError:
139
+ return False
140
+
141
+
142
+ def is_protected_path(file_path: str) -> bool:
143
+ lexical_hooks_directory = os.path.normcase(os.path.abspath(HOOKS_DIR))
144
+ lexical_candidate_path = os.path.normcase(os.path.abspath(file_path))
145
+ resolved_hooks_directory = os.path.normcase(os.path.realpath(HOOKS_DIR))
146
+ resolved_candidate_path = os.path.normcase(os.path.realpath(file_path))
147
+ return _is_path_under_directory(
148
+ lexical_candidate_path, lexical_hooks_directory
149
+ ) or _is_path_under_directory(resolved_candidate_path, resolved_hooks_directory)
150
+
151
+
152
+ def formatter_name_for_path(file_path: str) -> str | None:
153
+ suffix = Path(file_path).suffix.lower()
154
+ if suffix in PYTHON_EXTENSIONS:
155
+ return PYTHON_FORMATTER_NAME
156
+ if suffix in JS_EXTENSIONS or suffix in JSON_EXTENSIONS:
157
+ if has_prettier_config(file_path):
158
+ return PRETTIER_FORMATTER_NAME
159
+ return None
160
+
161
+
162
+ def is_formatter_eligible(tool_name: str, file_path: str) -> bool:
163
+ if tool_name != WRITE_TOOL_NAME or not file_path:
164
+ return False
165
+ if is_protected_path(file_path) or not is_untracked_in_git(file_path):
166
+ return False
167
+ return formatter_name_for_path(file_path) is not None
168
+
169
+
170
+ def _write_command_diagnostic(file_path: str, command: list[str], diagnostic_text: str) -> None:
171
+ normalized_diagnostic = diagnostic_text.strip()
172
+ if not normalized_diagnostic:
173
+ return
174
+ command_text = " ".join(command)
175
+ sys.stderr.write(
176
+ FORMATTER_DIAGNOSTIC_TEMPLATE % (file_path, f"{command_text}: {normalized_diagnostic}")
177
+ )
178
+
179
+
180
+ def _combine_command_diagnostics(
181
+ completed_process: subprocess.CompletedProcess[str],
182
+ ) -> str:
183
+ all_diagnostics = [
184
+ each_stream.strip()
185
+ for each_stream in (completed_process.stdout, completed_process.stderr)
186
+ if each_stream
187
+ ]
188
+ return FORMATTER_DIAGNOSTIC_SEPARATOR.join(all_diagnostics)
189
+
190
+
191
+ def _run_command(
192
+ command: list[str], file_path: str, timeout_seconds: int
193
+ ) -> tuple[subprocess.CompletedProcess[str] | None, bool]:
114
194
  try:
115
- hook_input = json.load(sys.stdin)
195
+ completed_process = subprocess.run(
196
+ command,
197
+ check=False,
198
+ capture_output=True,
199
+ text=True,
200
+ timeout=timeout_seconds,
201
+ )
202
+ except FileNotFoundError:
203
+ return None, False
204
+ except subprocess.TimeoutExpired:
205
+ sys.stderr.write(FORMATTER_TIMEOUT_DIAGNOSTIC_TEMPLATE % (file_path, timeout_seconds))
206
+ return None, True
207
+
208
+ if completed_process.returncode != 0:
209
+ _write_command_diagnostic(
210
+ file_path,
211
+ command,
212
+ _combine_command_diagnostics(completed_process),
213
+ )
214
+ return completed_process, False
215
+
216
+
217
+ def _run_python_fix_commands(file_path: str) -> None:
218
+ all_fix_commands = [
219
+ ["ruff", "check", "--fix", file_path],
220
+ [sys.executable, "-m", "ruff", "check", "--fix", file_path],
221
+ ]
222
+ for each_command in all_fix_commands:
223
+ completed_process, did_timeout = _run_command(
224
+ each_command, file_path, PYTHON_FORMAT_TIMEOUT_SECONDS
225
+ )
226
+ if did_timeout or completed_process is not None:
227
+ return
228
+
229
+
230
+ def _run_python_format_commands(file_path: str) -> None:
231
+ all_format_commands = [
232
+ ["ruff", "format", file_path],
233
+ [sys.executable, "-m", "ruff", "format", file_path],
234
+ ["black", file_path],
235
+ [sys.executable, "-m", "black", file_path],
236
+ ]
237
+ for each_command in all_format_commands:
238
+ completed_process, did_timeout = _run_command(
239
+ each_command, file_path, PYTHON_FORMAT_TIMEOUT_SECONDS
240
+ )
241
+ if did_timeout:
242
+ return
243
+ if completed_process is not None and completed_process.returncode == 0:
244
+ send_format_notification(file_path, formatter_name_for_command(each_command))
245
+ return
246
+
247
+
248
+ def formatter_name_for_command(command: list[str]) -> str:
249
+ if command[0] == sys.executable:
250
+ return command[2]
251
+ return command[0]
252
+
253
+
254
+ def _run_prettier(file_path: str) -> None:
255
+ prettier_command = [
256
+ NPX_EXECUTABLE,
257
+ "--yes",
258
+ "prettier",
259
+ "--write",
260
+ os.path.realpath(file_path),
261
+ ]
262
+ completed_process, _did_timeout = _run_command(
263
+ prettier_command, file_path, JS_FORMAT_TIMEOUT_SECONDS
264
+ )
265
+ if completed_process is not None and completed_process.returncode == 0:
266
+ send_format_notification(file_path, PRETTIER_FORMATTER_NAME)
267
+
268
+
269
+ def run_eligible_formatter(file_path: str) -> None:
270
+ formatter_name = formatter_name_for_path(file_path)
271
+ if formatter_name == PYTHON_FORMATTER_NAME:
272
+ _run_python_fix_commands(file_path)
273
+ _run_python_format_commands(file_path)
274
+ return
275
+ if formatter_name == PRETTIER_FORMATTER_NAME:
276
+ _run_prettier(file_path)
277
+
278
+
279
+ def _read_hook_input() -> dict[str, object] | None:
280
+ try:
281
+ parsed_input = json.load(sys.stdin)
116
282
  except (json.JSONDecodeError, ValueError):
117
- sys.exit(0)
283
+ return None
284
+ if not isinstance(parsed_input, dict):
285
+ return None
286
+ return parsed_input
118
287
 
119
- tool_name = hook_input.get("tool_name", "")
120
- file_path = hook_input.get("tool_input", {}).get("file_path", "")
121
- if not file_path:
122
- sys.exit(0)
123
288
 
124
- if tool_name == "Edit":
125
- sys.exit(0)
289
+ def _read_string_field(field_source: Mapping[str, object], field_name: str) -> str:
290
+ field_value = field_source.get(field_name)
291
+ return field_value if isinstance(field_value, str) else ""
126
292
 
127
- if tool_name == "Write" and not is_untracked_in_git(file_path):
128
- sys.exit(0)
129
293
 
130
- if file_path.startswith(HOOKS_DIR):
131
- sys.exit(0)
294
+ def _read_hook_file_path(hook_input: Mapping[str, object]) -> str:
295
+ tool_input = hook_input.get("tool_input")
296
+ if not isinstance(tool_input, dict):
297
+ return ""
298
+ return _read_string_field(tool_input, "file_path")
132
299
 
133
- suffix = Path(file_path).suffix.lower()
134
300
 
135
- if suffix in PYTHON_EXTENSIONS:
136
- for each_fix_command in [
137
- ["ruff", "check", "--fix", file_path],
138
- [sys.executable, "-m", "ruff", "check", "--fix", file_path],
139
- ]:
140
- try:
141
- subprocess.run(each_fix_command, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS, check=False)
142
- break
143
- except FileNotFoundError:
144
- continue
145
- except subprocess.TimeoutExpired:
146
- break
147
- for each_formatter_command in [
148
- ["ruff", "format", file_path],
149
- [sys.executable, "-m", "ruff", "format", file_path],
150
- ["black", file_path],
151
- [sys.executable, "-m", "black", file_path],
152
- ]:
153
- try:
154
- format_run = subprocess.run(each_formatter_command, check=False, capture_output=True, text=True, timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)
155
- if format_run.returncode == 0:
156
- formatter_name = each_formatter_command[0] if each_formatter_command[0] != sys.executable else each_formatter_command[2]
157
- send_format_notification(file_path, formatter_name)
158
- break
159
- except FileNotFoundError:
160
- continue
161
- except subprocess.TimeoutExpired:
162
- break
163
- elif suffix in JS_EXTENSIONS or suffix in JSON_EXTENSIONS:
164
- if not has_prettier_config(file_path):
165
- sys.exit(0)
166
- try:
167
- prettier_run = subprocess.run(
168
- ["npx", "--yes", "prettier", "--write", file_path],
169
- check=False, capture_output=True,
170
- text=True,
171
- timeout=JS_FORMAT_TIMEOUT_SECONDS,
172
- )
173
- if prettier_run.returncode == 0:
174
- send_format_notification(file_path, "prettier")
175
- except (FileNotFoundError, subprocess.TimeoutExpired):
176
- pass
177
-
178
- sys.exit(0)
301
+ def main() -> None:
302
+ hook_input = _read_hook_input()
303
+ if hook_input is None:
304
+ return
305
+
306
+ tool_name = _read_string_field(hook_input, "tool_name")
307
+ file_path = _read_hook_file_path(hook_input)
308
+ if not is_formatter_eligible(tool_name, file_path):
309
+ return
310
+ run_eligible_formatter(file_path)
179
311
 
180
312
 
181
313
  if __name__ == "__main__":
@@ -33,6 +33,8 @@ def main() -> None:
33
33
  hook_input = json.load(sys.stdin)
34
34
  except json.JSONDecodeError:
35
35
  sys.exit(0)
36
+ if not isinstance(hook_input, dict):
37
+ sys.exit(0)
36
38
 
37
39
  tool_name = hook_input.get("tool_name", "")
38
40
  if tool_name not in DELEGATION_TOOLS: