claude-dev-env 2.25.0 → 2.27.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 (45) hide show
  1. package/.agents/agents/clean-coder.md +95 -33
  2. package/.agents/agents/code-quality-agent.md +85 -24
  3. package/.agents/agents/pr-description-writer.md +2 -2
  4. package/.agents/agents/test_agent_frontmatter.py +626 -0
  5. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +47 -1
  6. package/.agents/skills/pr-cleanup/SKILL.md +1 -0
  7. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +93 -0
  8. package/.agents/skills/pr-title-description/SKILL.md +92 -0
  9. package/.agents/skills/source-command-sr-loop/SKILL.md +12 -3
  10. package/bin/ever-shipped-skills.mjs +2 -0
  11. package/bin/install.agents-home.test.mjs +199 -8
  12. package/bin/install.mjs +15 -8
  13. package/bin/install.test.mjs +82 -2
  14. package/commands/sr-loop.md +10 -1
  15. package/docs/codex-compatibility.md +19 -0
  16. package/hooks/blocking/luna_fast_mode_gate.py +160 -0
  17. package/hooks/blocking/test_luna_fast_mode_gate.py +211 -0
  18. package/hooks/hooks.json +10 -5
  19. package/hooks/hooks_constants/AGENTS.md +1 -1
  20. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +45 -0
  21. package/hooks/hooks_constants/mypy_integration_constants.py +14 -3
  22. package/hooks/hooks_constants/session_start_injector.py +11 -4
  23. package/hooks/hooks_constants/test_session_start_injector.py +4 -2
  24. package/hooks/session/AGENTS.md +2 -2
  25. package/hooks/session/issue_tracker_session_starter.py +3 -2
  26. package/hooks/session/orchestrator_auto_starter.py +3 -2
  27. package/hooks/session/task_list_loop_starter.py +10 -1
  28. package/hooks/session/test_issue_tracker_session_starter.py +3 -1
  29. package/hooks/session/test_orchestrator_auto_starter.py +5 -3
  30. package/hooks/session/test_task_list_loop_starter.py +10 -8
  31. package/hooks/session/test_untracked_repo_detector.py +7 -5
  32. package/hooks/session/test_working_style_prompt.py +5 -3
  33. package/hooks/session/untracked_repo_detector.py +10 -1
  34. package/hooks/session/working_style_prompt.py +10 -1
  35. package/hooks/validators/AGENTS.md +2 -1
  36. package/hooks/validators/conftest.py +21 -4
  37. package/hooks/validators/mypy_integration.py +77 -16
  38. package/hooks/validators/run_all_validators.py +2 -35
  39. package/hooks/validators/system_temporary_roots.py +90 -0
  40. package/hooks/validators/test_directory_exemption_constants.py +1 -1
  41. package/hooks/validators/test_mypy_integration.py +169 -0
  42. package/hooks/validators/test_system_temporary_roots.py +93 -0
  43. package/package.json +1 -1
  44. package/scripts/codex_compat_materializer.py +4 -2
  45. package/scripts/tests/test_codex_compat_materializer.py +2 -2
@@ -0,0 +1,90 @@
1
+ """Resolve the system temp root that contains a path, if any.
2
+
3
+ Staging copies and pytest basetemp live under ``tempfile.gettempdir()`` or
4
+ under ``TEMP`` / ``TMP`` / ``TMPDIR`` / ``RUNNER_TEMP``. A walk that needs to
5
+ stop at that boundary asks here instead of re-coding membership in each caller.
6
+
7
+ ::
8
+
9
+ %TEMP%/pytest-123/x.py -> %TEMP%
10
+ RUNNER_TEMP/detached/x.py (GHA) -> RUNNER_TEMP
11
+ C:/repo/pkg/x.py -> None
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import sys
18
+ import tempfile
19
+ from pathlib import Path
20
+
21
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
22
+ if _hooks_directory not in sys.path:
23
+ sys.path.insert(0, _hooks_directory)
24
+
25
+ from validators.config.directory_exemption_constants import ( # noqa: E402
26
+ ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES,
27
+ )
28
+
29
+
30
+ def all_system_temporary_roots() -> tuple[Path, ...]:
31
+ """Return resolved roots that count as system temporary directories.
32
+
33
+ ::
34
+
35
+ gettempdir() plus TEMP / TMP / TMPDIR / RUNNER_TEMP when set.
36
+
37
+ GitHub Actions puts pytest basetemp under ``RUNNER_TEMP``
38
+ (``/home/runner/work/_temp``) while ``tempfile.gettempdir()`` is ``/tmp``.
39
+ Both count so a staging walk and a path-exemption check agree.
40
+
41
+ Returns:
42
+ Unique resolved directory Paths. Unresolvable candidates are dropped.
43
+ """
44
+ all_candidate_roots: list[str] = [tempfile.gettempdir()]
45
+ for each_environment_name in ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES:
46
+ environment_value = os.environ.get(each_environment_name)
47
+ if environment_value:
48
+ all_candidate_roots.append(environment_value)
49
+ all_resolved_roots: list[Path] = []
50
+ all_seen_roots: set[Path] = set()
51
+ for each_candidate in all_candidate_roots:
52
+ try:
53
+ resolved_root = Path(each_candidate).resolve()
54
+ except OSError:
55
+ continue
56
+ if resolved_root in all_seen_roots:
57
+ continue
58
+ all_seen_roots.add(resolved_root)
59
+ all_resolved_roots.append(resolved_root)
60
+ return tuple(all_resolved_roots)
61
+
62
+
63
+ def enclosing_system_temporary_root(starting_file: Path) -> Path | None:
64
+ """Return the innermost system temp root that contains *starting_file*.
65
+
66
+ ::
67
+
68
+ %TEMP%/pytest-123/x.py -> %TEMP%
69
+ C:/repo/pkg/x.py -> None
70
+
71
+ When more than one listed root contains the file, the first ancestor that
72
+ is already in the root set wins, so a nested ``RUNNER_TEMP`` stops the walk
73
+ before a broader ``gettempdir()`` ancestor.
74
+
75
+ Args:
76
+ starting_file: The file or directory whose ancestors are bounded.
77
+
78
+ Returns:
79
+ The innermost matching root Path, or ``None`` when no listed root
80
+ contains *starting_file*.
81
+ """
82
+ try:
83
+ resolved_start = starting_file.resolve()
84
+ except OSError:
85
+ return None
86
+ all_root_set = set(all_system_temporary_roots())
87
+ for each_candidate_directory in (resolved_start, *resolved_start.parents):
88
+ if each_candidate_directory in all_root_set:
89
+ return each_candidate_directory
90
+ return None
@@ -147,7 +147,7 @@ def test_runner_temp_pytest_shaped_path_stages_flat_basename_when_gettempdir_dif
147
147
  os_gettemp.mkdir()
148
148
  monkeypatch.setenv("RUNNER_TEMP", str(runner_temp_root))
149
149
  monkeypatch.setattr(
150
- "validators.run_all_validators.tempfile.gettempdir",
150
+ "validators.system_temporary_roots.tempfile.gettempdir",
151
151
  lambda: str(os_gettemp),
152
152
  )
153
153
  pytest_shaped_target = (
@@ -1,10 +1,13 @@
1
1
  """Tests for mypy integration module."""
2
2
 
3
+ import logging
4
+ import subprocess
3
5
  from pathlib import Path
4
6
  from unittest.mock import patch
5
7
 
6
8
  import pytest
7
9
 
10
+ from . import mypy_integration as mypy_integration_module
8
11
  from .mypy_integration import (
9
12
  MypyResult,
10
13
  check_mypy_available,
@@ -30,6 +33,63 @@ def _write_config_importer(script_path: Path) -> None:
30
33
  )
31
34
 
32
35
 
36
+ def test_find_module_resolution_root_ignores_git_above_system_temp(
37
+ tmp_path: Path,
38
+ ) -> None:
39
+ """A temp staging file does not inherit ``.git`` from a parent of ``%TEMP%``.
40
+
41
+ ::
42
+
43
+ %TEMP%/pytest-.../detached/module.py
44
+ C:/Users/<you>/.git exists above %TEMP%
45
+ flag: walk returns the home repo -> mypy cwd=home, torch stubs, 30s timeout
46
+ ok: walk stops at the temp root -> None
47
+ """
48
+ nested_file = tmp_path / "detached" / "module.py"
49
+ nested_file.parent.mkdir()
50
+ nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
51
+ assert find_module_resolution_root(nested_file) is None
52
+
53
+
54
+ def test_find_module_resolution_root_still_sees_git_inside_temp(tmp_path: Path) -> None:
55
+ """A repo created inside the temp tree is still a project root."""
56
+ nested_repo = tmp_path / "inner_repo"
57
+ nested_file = nested_repo / "pkg" / "module.py"
58
+ nested_file.parent.mkdir(parents=True)
59
+ (nested_repo / ".git").mkdir()
60
+ nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
61
+ assert find_module_resolution_root(nested_file) == nested_repo
62
+
63
+
64
+ def test_find_module_resolution_root_stops_at_runner_temp_when_gettempdir_differs(
65
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
66
+ ) -> None:
67
+ """A RUNNER_TEMP staging file does not inherit ``.git`` above that root.
68
+
69
+ ::
70
+
71
+ RUNNER_TEMP/detached/module.py, gettempdir -> sibling os_gettemp
72
+ tmp_path/.git sits above RUNNER_TEMP
73
+ flag: gettempdir-only walk returns tmp_path
74
+ ok: walk stops at RUNNER_TEMP -> None
75
+ """
76
+ runner_temp_root = tmp_path / "runner_temp"
77
+ os_gettemp = tmp_path / "os_gettemp"
78
+ runner_temp_root.mkdir()
79
+ os_gettemp.mkdir()
80
+ (tmp_path / ".git").mkdir()
81
+ monkeypatch.setenv("RUNNER_TEMP", str(runner_temp_root))
82
+ monkeypatch.setattr(
83
+ "validators.system_temporary_roots.tempfile.gettempdir",
84
+ lambda: str(os_gettemp),
85
+ )
86
+ nested_file = runner_temp_root / "detached" / "module.py"
87
+ nested_file.parent.mkdir()
88
+ nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
89
+
90
+ assert find_module_resolution_root(nested_file) is None
91
+
92
+
33
93
  def test_find_module_resolution_root_returns_git_marked_ancestor(tmp_path: Path) -> None:
34
94
  target_repo = tmp_path / "git_repo"
35
95
  nested_directory = target_repo / "src" / "deep"
@@ -165,6 +225,115 @@ def test_run_mypy_check_accepts_relative_path_under_nested_root(
165
225
  assert mypy_result.passed, mypy_result.output
166
226
 
167
227
 
228
+ def test_run_mypy_check_does_not_report_imported_sibling_on_detached_file(
229
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
230
+ ) -> None:
231
+ """A detached gate file does not inherit type errors from an imported sibling.
232
+
233
+ ::
234
+
235
+ detached/importer.py -> from sibling import broken_value
236
+ detached/sibling.py -> broken_value: int = "not an integer"
237
+ flag: follow-imports=normal reports sibling.py and fails the gate file
238
+ ok: follow-imports=skip grades importer.py only -> passed
239
+
240
+ The PreToolUse gate stages one file under a temp directory with no project
241
+ root. Following imports there loads site-packages (torch stubs) and sibling
242
+ modules, and the 30s hook budget expires.
243
+ """
244
+ if not check_mypy_available():
245
+ pytest.skip("mypy is not installed")
246
+
247
+ detached_directory = tmp_path / "detached_gate_dir"
248
+ detached_directory.mkdir()
249
+ (detached_directory / "sibling.py").write_text(
250
+ "broken_value: int = 'not an integer'\n",
251
+ encoding="utf-8",
252
+ )
253
+ importer_file = detached_directory / "importer.py"
254
+ importer_file.write_text(
255
+ "from sibling import broken_value\n\n"
256
+ "copied_value: int = broken_value\n",
257
+ encoding="utf-8",
258
+ )
259
+
260
+ monkeypatch.chdir(tmp_path)
261
+ mypy_result = run_mypy_check([importer_file])
262
+
263
+ assert mypy_result.passed, mypy_result.output
264
+ assert "sibling.py" not in mypy_result.output
265
+
266
+
267
+ def test_run_mypy_check_reports_imported_sibling_on_rooted_file(
268
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
269
+ ) -> None:
270
+ """A rooted file still follows first-party imports and reports sibling errors.
271
+
272
+ ::
273
+
274
+ rooted_repo/.git + importer.py -> from sibling import broken_value
275
+ rooted_repo/sibling.py -> broken_value: int = "not an integer"
276
+ ok: follow-imports=normal reports sibling.py -> failed
277
+ flag: --follow-imports=skip on rooted files hides sibling.py -> passed
278
+
279
+ Skip applies only to detached staging copies. Applying it to a repo file
280
+ would drop first-party type errors the in-repo path must still see.
281
+ """
282
+ if not check_mypy_available():
283
+ pytest.skip("mypy is not installed")
284
+
285
+ rooted_repo = tmp_path / "rooted_repo"
286
+ rooted_repo.mkdir()
287
+ (rooted_repo / ".git").mkdir()
288
+ (rooted_repo / "sibling.py").write_text(
289
+ "broken_value: int = 'not an integer'\n",
290
+ encoding="utf-8",
291
+ )
292
+ importer_file = rooted_repo / "importer.py"
293
+ importer_file.write_text(
294
+ "from sibling import broken_value\n\n"
295
+ "copied_value: int = broken_value\n",
296
+ encoding="utf-8",
297
+ )
298
+
299
+ monkeypatch.chdir(tmp_path)
300
+ mypy_result = run_mypy_check([importer_file])
301
+
302
+ assert not mypy_result.passed
303
+ assert "sibling.py" in mypy_result.output
304
+
305
+
306
+ def test_run_mypy_check_skips_when_detached_mypy_times_out(
307
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
308
+ ) -> None:
309
+ """A detached mypy that exceeds its timeout does not fail the file.
310
+
311
+ ::
312
+
313
+ detached/module.py, subprocess.run raises TimeoutExpired
314
+ ok: passed=True and the skip message is in the output
315
+ flag: TimeoutExpired propagates and the PreToolUse hook dies
316
+ """
317
+ detached_file = tmp_path / "detached" / "module.py"
318
+ detached_file.parent.mkdir()
319
+ detached_file.write_text("sample_number: int = 1\n", encoding="utf-8")
320
+
321
+ monkeypatch.setattr(
322
+ mypy_integration_module, "check_mypy_available", lambda: True
323
+ )
324
+
325
+ def raise_timeout(*_args: object, **_kwargs: object) -> None:
326
+ raise subprocess.TimeoutExpired(cmd="mypy", timeout=1)
327
+
328
+ monkeypatch.setattr(mypy_integration_module.subprocess, "run", raise_timeout)
329
+ with caplog.at_level(logging.WARNING):
330
+ mypy_result = run_mypy_check([detached_file])
331
+
332
+ assert mypy_result.passed
333
+ assert "timed out on a detached file" in mypy_result.output
334
+ assert "timed out on a detached file" in caplog.text
335
+
336
+
168
337
  def test_run_mypy_check_applies_config_resolved_from_config_source_path(
169
338
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
170
339
  ) -> None:
@@ -0,0 +1,93 @@
1
+ """Tests for the shared system-temp root membership helper."""
2
+
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+ from .system_temporary_roots import (
8
+ all_system_temporary_roots,
9
+ enclosing_system_temporary_root,
10
+ )
11
+
12
+
13
+ def test_all_system_temporary_roots_includes_runner_temp_when_set(
14
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
15
+ ) -> None:
16
+ """RUNNER_TEMP joins gettempdir so GHA basetemp counts as system temp."""
17
+ runner_temp_root = tmp_path / "runner_temp"
18
+ os_gettemp = tmp_path / "os_gettemp"
19
+ runner_temp_root.mkdir()
20
+ os_gettemp.mkdir()
21
+ monkeypatch.setenv("RUNNER_TEMP", str(runner_temp_root))
22
+ monkeypatch.setattr(
23
+ "validators.system_temporary_roots.tempfile.gettempdir",
24
+ lambda: str(os_gettemp),
25
+ )
26
+
27
+ all_roots = all_system_temporary_roots()
28
+
29
+ assert os_gettemp.resolve() in all_roots
30
+ assert runner_temp_root.resolve() in all_roots
31
+
32
+
33
+ def test_enclosing_system_temporary_root_returns_gettempdir_for_file_under_it(
34
+ tmp_path: Path,
35
+ ) -> None:
36
+ """A pytest tmp_path file sits under the process temp root."""
37
+ nested_file = tmp_path / "detached" / "module.py"
38
+ nested_file.parent.mkdir()
39
+ nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
40
+
41
+ enclosing_root = enclosing_system_temporary_root(nested_file)
42
+
43
+ assert enclosing_root is not None
44
+ assert nested_file.resolve().is_relative_to(enclosing_root)
45
+
46
+
47
+ def test_enclosing_system_temporary_root_uses_runner_temp_when_gettempdir_differs(
48
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
49
+ ) -> None:
50
+ """GHA basetemp lives under RUNNER_TEMP while gettempdir is often another root.
51
+
52
+ ::
53
+
54
+ RUNNER_TEMP/detached/module.py, gettempdir -> sibling os_gettemp
55
+ ok: enclosing root is RUNNER_TEMP
56
+ flag: gettempdir-only helper returns None and the mypy walk climbs out
57
+ """
58
+ runner_temp_root = tmp_path / "runner_temp"
59
+ os_gettemp = tmp_path / "os_gettemp"
60
+ runner_temp_root.mkdir()
61
+ os_gettemp.mkdir()
62
+ monkeypatch.setenv("RUNNER_TEMP", str(runner_temp_root))
63
+ monkeypatch.setattr(
64
+ "validators.system_temporary_roots.tempfile.gettempdir",
65
+ lambda: str(os_gettemp),
66
+ )
67
+ nested_file = runner_temp_root / "detached" / "module.py"
68
+ nested_file.parent.mkdir()
69
+ nested_file.write_text("sample_number: int = 1\n", encoding="utf-8")
70
+
71
+ assert enclosing_system_temporary_root(nested_file) == runner_temp_root.resolve()
72
+
73
+
74
+ def test_enclosing_system_temporary_root_returns_none_outside_every_temp_root(
75
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
76
+ ) -> None:
77
+ """A path outside gettempdir and the temp env vars is not a staging copy."""
78
+ os_gettemp = tmp_path / "os_gettemp"
79
+ project_root = tmp_path / "project"
80
+ os_gettemp.mkdir()
81
+ project_root.mkdir()
82
+ monkeypatch.delenv("RUNNER_TEMP", raising=False)
83
+ monkeypatch.delenv("TEMP", raising=False)
84
+ monkeypatch.delenv("TMP", raising=False)
85
+ monkeypatch.delenv("TMPDIR", raising=False)
86
+ monkeypatch.setattr(
87
+ "validators.system_temporary_roots.tempfile.gettempdir",
88
+ lambda: str(os_gettemp),
89
+ )
90
+ project_file = project_root / "module.py"
91
+ project_file.write_text("sample_number: int = 1\n", encoding="utf-8")
92
+
93
+ assert enclosing_system_temporary_root(project_file) is None
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.25.0",
3
+ "version": "2.27.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,7 +25,7 @@ reparse_point_attribute_name = "FILE_ATTRIBUTE_REPARSE_POINT"
25
25
  manifest_indentation_width = 2
26
26
  publish_plan_max_positional_arguments = 3
27
27
  publish_plan_failure_injector_position = 2
28
- frontmatter_unsupported_fields = ("tools", "model", "color")
28
+ frontmatter_unsupported_fields = ("tools", "model", "color", "disable-model-invocation")
29
29
  instruction_alias_filenames = frozenset({"AGENTS.md", "CLAUDE.md"})
30
30
  failure_blast_radius_rule_relative_path = "rules/failure-blast-radius.md"
31
31
  codex_instruction_target_path = "AGENTS.md"
@@ -152,7 +152,9 @@ report_categories = (
152
152
  "stale_managed", "deleted", "unsupported", "conflicted", "errors",
153
153
  )
154
154
  report_categories_public_name = "REPORT_CATEGORIES"
155
- frontmatter_allowed_fields = {"name", "description", "tools", "model", "color"}
155
+ frontmatter_allowed_fields = {
156
+ "name", "description", "tools", "model", "color", "disable-model-invocation"
157
+ }
156
158
  line_separator = "\n"
157
159
  comma_separator = ", "
158
160
 
@@ -40,8 +40,8 @@ def test_conversion_escapes_toml_content() -> None:
40
40
  def test_malformed_and_unsupported_frontmatter() -> None:
41
41
  with pytest.raises(MaterializerError):
42
42
  parse_frontmatter(Path("bad.md"), "---\nname: x\n", "bad.md")
43
- agent = parse_frontmatter(Path("ok.md"), "---\nname: x\ndescription: y\nmodel: sonnet\ncolor: blue\n---\n", "ok.md")
44
- assert agent.unsupported == ("color", "model")
43
+ agent = parse_frontmatter(Path("ok.md"), "---\nname: x\ndescription: y\nmodel: sonnet\ncolor: blue\ndisable-model-invocation: true\n---\n", "ok.md")
44
+ assert agent.unsupported == ("color", "disable-model-invocation", "model")
45
45
 
46
46
 
47
47
  @pytest.mark.parametrize(