claude-dev-env 7.4.1 → 7.6.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 (178) hide show
  1. package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
  2. package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
  3. package/.agents/skills/fresh-branch/SKILL.md +1 -0
  4. package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
  5. package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
  6. package/.agents/skills/syncing-submodules/SKILL.md +1 -0
  7. package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
  8. package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
  9. package/.agents/skills/test-runner/SKILL.md +1 -0
  10. package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
  11. package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
  12. package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
  13. package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
  14. package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
  15. package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
  16. package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
  23. package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
  24. package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
  25. package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
  26. package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
  27. package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
  28. package/_shared/pr-loop/scripts/preflight.py +7 -0
  29. package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
  30. package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
  31. package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
  32. package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
  33. package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
  34. package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
  35. package/bin/install-constants.mjs +4 -2
  36. package/bin/install.cursor-rules.test.mjs +57 -15
  37. package/bin/install.mjs +10 -16
  38. package/bin/install.policy-lint.test.mjs +1 -2
  39. package/bin/install.test.mjs +0 -1
  40. package/bin/pstack-model-preferences.claude.json +23 -0
  41. package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
  42. package/hooks/advisory/pr_done_reminder.py +14 -13
  43. package/hooks/advisory/refactor_guard.py +8 -0
  44. package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +22 -0
  46. package/hooks/advisory/test_refactor_guard.py +40 -0
  47. package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
  48. package/hooks/blocking/block_main_commit.py +4 -0
  49. package/hooks/blocking/code_rules_comments.py +39 -5
  50. package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
  51. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
  52. package/hooks/blocking/precommit_code_rules_gate.py +3 -0
  53. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
  54. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
  55. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
  56. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
  57. package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
  58. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
  59. package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
  60. package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
  61. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
  62. package/hooks/git-hooks/pull_request_handoff.py +3 -0
  63. package/hooks/git-hooks/subprocess_window_access.py +41 -0
  64. package/hooks/git-hooks/test_verification_start.py +4 -0
  65. package/hooks/git-hooks/verification_notice_context.py +3 -0
  66. package/hooks/git-hooks/verification_start.py +7 -8
  67. package/hooks/hooks.json +10 -0
  68. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
  69. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
  70. package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
  71. package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
  72. package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
  73. package/hooks/hooks_constants/post_tool_use_context.py +34 -0
  74. package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
  75. package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
  76. package/hooks/hooks_constants/subprocess_window.py +141 -0
  77. package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
  78. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
  79. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
  80. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
  81. package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
  82. package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
  83. package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
  84. package/hooks/hooks_constants/test_subprocess_window.py +128 -0
  85. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
  86. package/hooks/observability/test_failure_recorder.py +3 -1
  87. package/hooks/validation/mypy_validator.py +4 -0
  88. package/hooks/validators/README.md +1 -0
  89. package/hooks/validators/git_checks.py +4 -0
  90. package/hooks/validators/health_check.py +3 -0
  91. package/hooks/validators/hook_timing_harness.py +3 -0
  92. package/hooks/validators/mypy_integration.py +4 -0
  93. package/hooks/validators/ruff_integration.py +6 -0
  94. package/hooks/validators/run_all_validators.py +7 -0
  95. package/hooks/validators/subprocess_window_access.py +41 -0
  96. package/hooks/workflow/auto_formatter.py +8 -0
  97. package/hooks/workflow/test_auto_formatter.py +39 -0
  98. package/package.json +1 -1
  99. package/rules/ask-user-question-required.md +3 -3
  100. package/rules/code-standards.md +3 -2
  101. package/rules/confirm-implementation-forks.md +2 -0
  102. package/rules/destructive-commands.md +19 -19
  103. package/rules/doc-inventory-integrity.md +4 -4
  104. package/rules/docstring-prose-matches-implementation.md +2 -2
  105. package/rules/failure-blast-radius.md +4 -2
  106. package/rules/filesystem-search.md +1 -1
  107. package/rules/git-workflow.md +34 -0
  108. package/rules/long-horizon-autonomy.md +2 -0
  109. package/rules/nas-ssh-invocation.md +3 -3
  110. package/rules/no-cross-skill-duplicate-helpers.md +6 -6
  111. package/rules/orphan-css-class.md +5 -5
  112. package/rules/paired-test-coverage.md +6 -6
  113. package/rules/plain-illustrative-docstrings.md +3 -3
  114. package/rules/pstack-models.md +4 -3
  115. package/rules/re-stage-before-commit.md +7 -7
  116. package/rules/retired-hook-prose.md +16 -0
  117. package/rules/shell-invocation.md +17 -1
  118. package/rules/testing.md +3 -5
  119. package/rules/verify-runtime-state.md +1 -1
  120. package/rules/windows-filesystem-safe.md +2 -2
  121. package/rules/workflow-substitution-slots.md +1 -1
  122. package/scripts/automatic_advisory/checkout.py +8 -1
  123. package/scripts/automatic_advisory/cli.py +9 -3
  124. package/scripts/automatic_advisory/config/constants.py +0 -2
  125. package/scripts/automatic_advisory/execution.py +7 -1
  126. package/scripts/automatic_advisory/git.py +9 -1
  127. package/scripts/automatic_advisory/process_host.py +3 -0
  128. package/scripts/claude_chain_runner.py +3 -0
  129. package/scripts/durable_post_lint_config/config/constants.py +0 -3
  130. package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
  131. package/scripts/gh_artifact_upload.py +3 -0
  132. package/scripts/grok_headless_runner.py +3 -0
  133. package/scripts/grok_patch_artifacts.py +4 -0
  134. package/scripts/grok_worker_preflight.py +3 -0
  135. package/scripts/invoke_code_review.py +3 -0
  136. package/scripts/local_verification/command_runner.py +3 -0
  137. package/scripts/local_verification/git_state.py +3 -0
  138. package/scripts/policy_lint/adapter_configuration.py +24 -0
  139. package/scripts/policy_lint/adapter_detectors.py +48 -2
  140. package/scripts/policy_lint/adapter_pairing.py +150 -2
  141. package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
  142. package/scripts/policy_lint/adapters.py +82 -5
  143. package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
  144. package/scripts/policy_lint/config/constants.py +77 -0
  145. package/scripts/policy_lint/registry.py +6 -1
  146. package/scripts/policy_lint/selection.py +37 -18
  147. package/scripts/policy_lint/selection_git.py +17 -2
  148. package/scripts/repository_checks/config/constants.py +5 -0
  149. package/scripts/repository_checks/config/policy_document.py +19 -0
  150. package/scripts/repository_checks/email_exemptions.py +14 -71
  151. package/scripts/repository_checks/path_exemptions.py +35 -0
  152. package/scripts/repository_checks/policy_document.py +120 -0
  153. package/scripts/repository_checks/tracked_secrets.py +27 -4
  154. package/scripts/setup_project_paths.py +3 -0
  155. package/scripts/spawn_grok_batch.py +3 -0
  156. package/scripts/subprocess_window_access.py +41 -0
  157. package/scripts/test_dispatcher_profile_import.py +4 -0
  158. package/scripts/test_durable_post_lint.py +42 -9
  159. package/scripts/tests/test_adapter_configuration.py +8 -0
  160. package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
  161. package/scripts/tests/test_email_exemptions.py +37 -0
  162. package/scripts/tests/test_path_exemptions.py +164 -0
  163. package/scripts/tests/test_policy_document.py +104 -0
  164. package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
  165. package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
  166. package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
  167. package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
  168. package/scripts/tests/test_policy_lint_selection.py +141 -0
  169. package/scripts/tests/test_subprocess_window_access.py +118 -0
  170. package/scripts/tests/test_tracked_secrets.py +79 -0
  171. package/scripts/validate_instruction_pairs.py +3 -0
  172. package/scripts/verify_installable_package.py +5 -0
  173. package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
  174. package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
  175. package/hooks/hooks_constants/test_text_stripping.py +0 -39
  176. package/hooks/hooks_constants/text_stripping.py +0 -36
  177. package/scripts/automatic_advisory/window_flags.py +0 -41
  178. package/scripts/repository_checks/config/email_exemptions.py +0 -10
@@ -0,0 +1,27 @@
1
+ """Tests for the shared PreToolUse allow-with-rewrite stdout emitter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from io import StringIO
7
+ from unittest.mock import patch
8
+
9
+ from hooks_constants.pre_tool_use_allow_output import write_pre_tool_use_allow_to_stdout
10
+
11
+
12
+ def _emitted_payload(updated_tool_input: dict[str, object]) -> dict[str, object]:
13
+ """Return the parsed payload the emitter wrote for one updated tool input."""
14
+ captured_stdout = StringIO()
15
+ with patch("sys.stdout", captured_stdout):
16
+ write_pre_tool_use_allow_to_stdout(updated_tool_input)
17
+ return json.loads(captured_stdout.getvalue())
18
+
19
+
20
+ def test_emitter_writes_an_allow_decision_carrying_the_updated_input() -> None:
21
+ assert _emitted_payload({"command": "git status", "description": "check"}) == {
22
+ "hookSpecificOutput": {
23
+ "hookEventName": "PreToolUse",
24
+ "permissionDecision": "allow",
25
+ "updatedInput": {"command": "git status", "description": "check"},
26
+ }
27
+ }
@@ -0,0 +1,128 @@
1
+ """Tests for the shared hidden-console creation flags."""
2
+
3
+ import pathlib
4
+ import subprocess
5
+ import sys
6
+ from typing import Any, Mapping
7
+
8
+ import pytest
9
+
10
+ _HOOKS_ROOT = pathlib.Path(__file__).resolve().parent.parent
11
+ if str(_HOOKS_ROOT) not in sys.path:
12
+ sys.path.insert(0, str(_HOOKS_ROOT))
13
+
14
+ from hooks_constants.subprocess_window import (
15
+ detached_hidden_window_creation_flags,
16
+ hidden_window_creation_flags,
17
+ inherited_stream_startup_info,
18
+ )
19
+
20
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
21
+ WINDOWS_NEW_PROCESS_GROUP_FLAG = 0x00000200
22
+ WINDOWS_ONLY = pytest.mark.skipif(
23
+ sys.platform != "win32", reason="subprocess.STARTUPINFO exists only on Windows"
24
+ )
25
+
26
+
27
+ def test_windows_reports_the_hidden_console_flag(monkeypatch: pytest.MonkeyPatch) -> None:
28
+ monkeypatch.setattr(sys, "platform", "win32")
29
+ monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", WINDOWS_NO_WINDOW_FLAG, raising=False)
30
+ assert hidden_window_creation_flags() == WINDOWS_NO_WINDOW_FLAG
31
+
32
+
33
+ def test_other_platforms_report_no_creation_flags(monkeypatch: pytest.MonkeyPatch) -> None:
34
+ monkeypatch.setattr(sys, "platform", "linux")
35
+ assert hidden_window_creation_flags() == 0
36
+
37
+
38
+ def test_windows_without_the_flag_attribute_reports_no_creation_flags(
39
+ monkeypatch: pytest.MonkeyPatch,
40
+ ) -> None:
41
+ monkeypatch.setattr(sys, "platform", "win32")
42
+ monkeypatch.delattr(subprocess, "CREATE_NO_WINDOW", raising=False)
43
+ assert hidden_window_creation_flags() == 0
44
+
45
+
46
+ def test_windows_detached_flags_add_a_new_process_group(
47
+ monkeypatch: pytest.MonkeyPatch,
48
+ ) -> None:
49
+ monkeypatch.setattr(sys, "platform", "win32")
50
+ monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", WINDOWS_NO_WINDOW_FLAG, raising=False)
51
+ monkeypatch.setattr(
52
+ subprocess,
53
+ "CREATE_NEW_PROCESS_GROUP",
54
+ WINDOWS_NEW_PROCESS_GROUP_FLAG,
55
+ raising=False,
56
+ )
57
+ assert detached_hidden_window_creation_flags() == (
58
+ WINDOWS_NO_WINDOW_FLAG | WINDOWS_NEW_PROCESS_GROUP_FLAG
59
+ )
60
+
61
+
62
+ def test_other_platforms_report_no_detached_creation_flags(
63
+ monkeypatch: pytest.MonkeyPatch,
64
+ ) -> None:
65
+ monkeypatch.setattr(sys, "platform", "linux")
66
+ assert detached_hidden_window_creation_flags() == 0
67
+
68
+
69
+ def _captured_run_options(monkeypatch: pytest.MonkeyPatch) -> Mapping[str, Any]:
70
+ """Run one command through a patched subprocess.run and return its options."""
71
+ all_captured_options: dict[str, Any] = {}
72
+
73
+ def fake_run(*_all_arguments: Any, **all_options: Any) -> None:
74
+ all_captured_options.update(all_options)
75
+
76
+ monkeypatch.setattr(subprocess, "run", fake_run)
77
+ subprocess.run(
78
+ ["git", "status"],
79
+ creationflags=hidden_window_creation_flags(),
80
+ )
81
+ return all_captured_options
82
+
83
+
84
+ def test_a_call_site_passes_the_hidden_flag_on_windows(
85
+ monkeypatch: pytest.MonkeyPatch,
86
+ ) -> None:
87
+ monkeypatch.setattr(sys, "platform", "win32")
88
+ monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", WINDOWS_NO_WINDOW_FLAG, raising=False)
89
+ assert _captured_run_options(monkeypatch)["creationflags"] == WINDOWS_NO_WINDOW_FLAG
90
+
91
+
92
+ def test_a_call_site_passes_the_default_flags_off_windows(
93
+ monkeypatch: pytest.MonkeyPatch,
94
+ ) -> None:
95
+ monkeypatch.setattr(sys, "platform", "linux")
96
+ assert _captured_run_options(monkeypatch)["creationflags"] == 0
97
+
98
+
99
+ @WINDOWS_ONLY
100
+ def test_windows_startup_info_asks_for_a_hidden_window(
101
+ monkeypatch: pytest.MonkeyPatch,
102
+ ) -> None:
103
+ monkeypatch.setattr(sys, "platform", "win32")
104
+ startup_information = inherited_stream_startup_info()
105
+ assert startup_information is not None
106
+ assert startup_information.dwFlags & subprocess.STARTF_USESHOWWINDOW
107
+ assert startup_information.wShowWindow == subprocess.SW_HIDE
108
+
109
+
110
+ def test_other_platforms_report_no_startup_info(monkeypatch: pytest.MonkeyPatch) -> None:
111
+ monkeypatch.setattr(sys, "platform", "linux")
112
+ assert inherited_stream_startup_info() is None
113
+
114
+
115
+ @WINDOWS_ONLY
116
+ def test_a_stream_inheriting_call_site_passes_no_creation_flags(
117
+ monkeypatch: pytest.MonkeyPatch,
118
+ ) -> None:
119
+ all_captured_options: dict[str, Any] = {}
120
+
121
+ def fake_run(*_all_arguments: Any, **all_options: Any) -> None:
122
+ all_captured_options.update(all_options)
123
+
124
+ monkeypatch.setattr(sys, "platform", "win32")
125
+ monkeypatch.setattr(subprocess, "run", fake_run)
126
+ subprocess.run(["python", "-m", "pytest"], startupinfo=inherited_stream_startup_info())
127
+ assert "creationflags" not in all_captured_options
128
+ assert all_captured_options["startupinfo"] is not None
@@ -44,6 +44,8 @@ from hooks_constants.enter_worktree_prefetch_constants import ( # noqa: E402
44
44
  )
45
45
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
46
46
 
47
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
48
+
47
49
 
48
50
  def is_enter_worktree_creation(payload_by_field: dict[str, object]) -> bool:
49
51
  """Return True when this hook invocation is an EnterWorktree creation call.
@@ -91,6 +93,7 @@ def _run_git_quietly(
91
93
  timeout=timeout_seconds,
92
94
  cwd=repo_directory,
93
95
  check=False,
96
+ creationflags=hidden_window_creation_flags(),
94
97
  )
95
98
  except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
96
99
  return None
@@ -101,6 +101,9 @@ try:
101
101
  for each_bootstrap_directory in (_hooks_root_directory, _blocking_directory):
102
102
  if each_bootstrap_directory not in sys.path:
103
103
  sys.path.insert(0, each_bootstrap_directory)
104
+ from hooks_constants.bash_post_call_dispatcher_constants import (
105
+ EXIT_CODE_ERROR_PREFIX,
106
+ )
104
107
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
105
108
  from hooks_constants.test_failure_recorder_constants import (
106
109
  ALL_CHAINING_OPERATOR_SUBSTRINGS,
@@ -108,7 +111,6 @@ try:
108
111
  ALL_PYTEST_PROGRAM_BASENAMES,
109
112
  ALL_REDIRECTION_TOKENS,
110
113
  BASH_TOOL_NAME,
111
- EXIT_CODE_ERROR_PREFIX,
112
114
  MODULE_RUN_FLAG,
113
115
  NODE_ID_FILE_PATH_SEPARATOR,
114
116
  OPTION_TOKEN_PREFIX,
@@ -57,6 +57,8 @@ from hooks_constants.mypy_validator_cache_constants import ( # noqa: E402
57
57
  )
58
58
  from json_file_reader import read_json_object # noqa: E402
59
59
 
60
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
61
+
60
62
 
61
63
  def load_notification_utils() -> ModuleType | None:
62
64
  try:
@@ -88,6 +90,7 @@ def discover_project_root(target_file: str) -> Path | None:
88
90
  text=True,
89
91
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
90
92
  cwd=str(Path(target_file).parent),
93
+ creationflags=hidden_window_creation_flags(),
91
94
  )
92
95
  if completed_process.returncode != 0:
93
96
  return None
@@ -493,6 +496,7 @@ def run_mypy(target_file: str, project_root: str) -> tuple[int, str]:
493
496
  env=os.environ.copy(),
494
497
  timeout=MYPY_TIMEOUT_SECONDS,
495
498
  cwd=project_root,
499
+ creationflags=hidden_window_creation_flags(),
496
500
  )
497
501
 
498
502
  stdout_output = completed_process.stdout.strip()
@@ -31,6 +31,7 @@ The checks live in `python_style_checks.py`. Shared source-line splitting, funct
31
31
  | `ruff_integration.py` | Ruff lint integration |
32
32
  | `run_all_validators.py` | Pre-push validator orchestration and report |
33
33
  | `security_checks.py` | Hardcoded-secret, SQL injection, and XSS checks |
34
+ | `subprocess_window_access.py` | Deployed-path shim re-exporting the hidden-window helpers |
34
35
  | `system_temporary_roots.py` | System temp-root membership for staged copies |
35
36
  | `todo_checks.py` | TODO/FIXME tracking with issue-reference requirement |
36
37
  | `type_safety_checks.py` | Missing type hints and Any usage checks |
@@ -6,6 +6,8 @@ import sys
6
6
  from dataclasses import dataclass
7
7
  from typing import List
8
8
 
9
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
10
+
9
11
  SUBPROCESS_TIMEOUT_SECONDS = 30
10
12
 
11
13
 
@@ -26,6 +28,7 @@ def get_current_branch() -> str:
26
28
  text=True,
27
29
  check=True,
28
30
  timeout=SUBPROCESS_TIMEOUT_SECONDS,
31
+ creationflags=hidden_window_creation_flags(),
29
32
  )
30
33
  return result.stdout.strip()
31
34
  except (FileNotFoundError, subprocess.CalledProcessError, subprocess.TimeoutExpired):
@@ -55,6 +58,7 @@ def check_draft_pr_state() -> List[Violation]:
55
58
  text=True,
56
59
  check=True,
57
60
  timeout=SUBPROCESS_TIMEOUT_SECONDS,
61
+ creationflags=hidden_window_creation_flags(),
58
62
  )
59
63
  except FileNotFoundError:
60
64
  return []
@@ -16,6 +16,8 @@ from enum import StrEnum
16
16
  from pathlib import Path
17
17
  from typing import Dict, Optional
18
18
 
19
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
20
+
19
21
  VALIDATOR_FILES = [
20
22
  "python_style_checks.py",
21
23
  "test_safety_checks.py",
@@ -144,6 +146,7 @@ def check_optional_tool(tool_name: str) -> bool:
144
146
  check=False,
145
147
  capture_output=True,
146
148
  text=True,
149
+ creationflags=hidden_window_creation_flags(),
147
150
  )
148
151
  return result.returncode == 0
149
152
  except FileNotFoundError:
@@ -41,6 +41,8 @@ from pathlib import Path
41
41
  from types import ModuleType
42
42
  from typing import TextIO
43
43
 
44
+ from subprocess_window_access import hidden_window_creation_flags
45
+
44
46
 
45
47
  def _load_module_from_path(module_name: str, module_path: Path) -> ModuleType:
46
48
  """Load and execute the module found at module_path, under module_name.
@@ -197,6 +199,7 @@ def run_hosted_command_once_milliseconds(
197
199
  text=True,
198
200
  timeout=_harness_constants.SUBPROCESS_TIMEOUT_SECONDS,
199
201
  check=False,
202
+ creationflags=hidden_window_creation_flags(),
200
203
  )
201
204
  finished_at = time.perf_counter()
202
205
  return (finished_at - started_at) * 1000.0
@@ -9,6 +9,8 @@ from collections.abc import Iterator
9
9
  from dataclasses import dataclass
10
10
  from pathlib import Path
11
11
 
12
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
13
+
12
14
  _validators_directory = str(Path(__file__).resolve().parent)
13
15
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
14
16
 
@@ -66,6 +68,7 @@ def check_mypy_available() -> bool:
66
68
  check=False,
67
69
  capture_output=True,
68
70
  text=True,
71
+ creationflags=hidden_window_creation_flags(),
69
72
  )
70
73
  return result.returncode == 0
71
74
  except FileNotFoundError:
@@ -224,6 +227,7 @@ def _run_mypy_subprocess(
224
227
  text=True,
225
228
  cwd=working_directory,
226
229
  timeout=timeout_seconds,
230
+ creationflags=hidden_window_creation_flags(),
227
231
  )
228
232
  except subprocess.TimeoutExpired:
229
233
  logger.warning(detached_timeout_skip_message)
@@ -6,6 +6,8 @@ import sys
6
6
  from dataclasses import dataclass
7
7
  from pathlib import Path
8
8
 
9
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
10
+
9
11
  _validators_directory = str(Path(__file__).resolve().parent)
10
12
  _hooks_directory = str(Path(__file__).resolve().parent.parent)
11
13
 
@@ -53,6 +55,7 @@ def check_ruff_available() -> bool:
53
55
  check=False,
54
56
  capture_output=True,
55
57
  text=True,
58
+ creationflags=hidden_window_creation_flags(),
56
59
  )
57
60
  return result.returncode == 0
58
61
  except FileNotFoundError:
@@ -201,6 +204,7 @@ def _staged_ruff_result(
201
204
  input=staged_content,
202
205
  cwd=str(resolved_pyproject.parent),
203
206
  env=_ruff_subprocess_environment(),
207
+ creationflags=hidden_window_creation_flags(),
204
208
  )
205
209
  return RuffResult(
206
210
  passed=result.returncode == 0,
@@ -247,6 +251,7 @@ def _run_native_ruff_check(
247
251
  capture_output=True,
248
252
  text=True,
249
253
  env=_ruff_subprocess_environment(),
254
+ creationflags=hidden_window_creation_flags(),
250
255
  )
251
256
  return RuffResult(
252
257
  passed=result.returncode == 0,
@@ -308,6 +313,7 @@ def run_ruff_fix(all_files: list[Path]) -> RuffResult:
308
313
  capture_output=True,
309
314
  text=True,
310
315
  env=_ruff_subprocess_environment(),
316
+ creationflags=hidden_window_creation_flags(),
311
317
  )
312
318
  return RuffResult(
313
319
  passed=result.returncode == 0,
@@ -60,6 +60,8 @@ from hooks_constants.multi_edit_reconstruction import (
60
60
  edits_for_tool,
61
61
  )
62
62
 
63
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
64
+
63
65
  VALIDATORS_DIR = Path(__file__).parent
64
66
  hooks_dir = VALIDATORS_DIR.parent
65
67
  package_name = VALIDATORS_DIR.name
@@ -131,6 +133,7 @@ def invoke_validator_module(module_stem: str, forwarded_file_paths: List[str]) -
131
133
  text=True,
132
134
  cwd=working_directory_string,
133
135
  env=environment,
136
+ creationflags=hidden_window_creation_flags(),
134
137
  )
135
138
 
136
139
 
@@ -160,6 +163,7 @@ def run_validators_entrypoint_subprocess(
160
163
  cwd=working_directory_string,
161
164
  env=environment,
162
165
  input=stdin_text,
166
+ creationflags=hidden_window_creation_flags(),
163
167
  )
164
168
 
165
169
 
@@ -347,6 +351,7 @@ def get_project_root() -> Optional[Path]:
347
351
  ["git", "-C", str(hooks_dir), "rev-parse", "--show-toplevel"],
348
352
  capture_output=True,
349
353
  text=True,
354
+ creationflags=hidden_window_creation_flags(),
350
355
  )
351
356
  if completed_git_lookup.returncode == 0:
352
357
  return Path(completed_git_lookup.stdout.strip())
@@ -674,6 +679,7 @@ def get_changed_files() -> List[Path]:
674
679
  ["git", "diff", "--cached", "--name-only"],
675
680
  capture_output=True,
676
681
  text=True,
682
+ creationflags=hidden_window_creation_flags(),
677
683
  )
678
684
 
679
685
  files = result.stdout.strip().split("\n") if result.stdout.strip() else []
@@ -684,6 +690,7 @@ def get_changed_files() -> List[Path]:
684
690
  ["git", "diff", "--name-only", "HEAD~1"],
685
691
  capture_output=True,
686
692
  text=True,
693
+ creationflags=hidden_window_creation_flags(),
687
694
  )
688
695
  files = result.stdout.strip().split("\n") if result.stdout.strip() else []
689
696
 
@@ -0,0 +1,41 @@
1
+ """Reach the hidden-window helpers from the scripts this directory deploys.
2
+
3
+ The installer copies this directory into a profile that holds no installed
4
+ Python distribution, and this repository's own tests run the deployed scripts
5
+ under ``python -S -E``, which hides every installed package from the child
6
+ process. On both routes ``hooks_constants`` is reachable only through the
7
+ deployed ``hooks`` directory, so this module puts that directory on
8
+ ``sys.path`` and re-exports the helpers a caller asks for::
9
+
10
+ from subprocess_window_access import hidden_window_creation_flags
11
+
12
+ The directory goes on the end of ``sys.path``. ``PathFinder`` runs ahead of
13
+ every editable-install finder, so a trailing entry still selects the deployed
14
+ copy, and the head of ``sys.path`` stays exactly as the caller left it. A
15
+ caller that prepended its own roots keeps them first.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ _subprocess_window_hooks_directory = str(
24
+ Path(__file__).resolve().parents[2] / "hooks"
25
+ )
26
+ if _subprocess_window_hooks_directory not in sys.path:
27
+ sys.path.append(_subprocess_window_hooks_directory)
28
+
29
+ from hooks_constants.subprocess_window import (
30
+ HiddenWindowStartupInfo,
31
+ detached_hidden_window_creation_flags,
32
+ hidden_window_creation_flags,
33
+ inherited_stream_startup_info,
34
+ )
35
+
36
+ __all__ = [
37
+ "HiddenWindowStartupInfo",
38
+ "detached_hidden_window_creation_flags",
39
+ "hidden_window_creation_flags",
40
+ "inherited_stream_startup_info",
41
+ ]
@@ -14,6 +14,12 @@ from collections.abc import Mapping
14
14
  from pathlib import Path
15
15
  from types import ModuleType
16
16
 
17
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
18
+ if _subprocess_window_hooks_directory not in sys.path:
19
+ sys.path.append(_subprocess_window_hooks_directory)
20
+
21
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
22
+
17
23
  NOTIFICATION_UTILS_DIRECTORY = os.path.join(
18
24
  os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "notification"
19
25
  )
@@ -118,6 +124,7 @@ def is_untracked_in_git(file_path: str) -> bool:
118
124
  cwd=containing_directory,
119
125
  timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
120
126
  env=_build_git_command_environment(),
127
+ creationflags=hidden_window_creation_flags(),
121
128
  )
122
129
  return git_check.returncode != 0
123
130
  except (FileNotFoundError, subprocess.TimeoutExpired):
@@ -198,6 +205,7 @@ def _run_command(
198
205
  capture_output=True,
199
206
  text=True,
200
207
  timeout=timeout_seconds,
208
+ creationflags=hidden_window_creation_flags(),
201
209
  )
202
210
  except FileNotFoundError:
203
211
  return None, False
@@ -35,6 +35,8 @@ HOOKS_JSON_PATH = os.path.join(HOOKS_DIRECTORY_PATH, "hooks.json")
35
35
  POST_TOOL_USE_DISPATCHER_COMMAND_FRAGMENT = "validation/post_tool_use_dispatcher.py"
36
36
  UNUSED_IMPORT_SOURCE = "import os\n\n\nVALUE = 1\n"
37
37
  HOOK_RUN_TIMEOUT_SECONDS = 60
38
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
39
+ EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
38
40
 
39
41
 
40
42
  def build_fixture_git_environment() -> dict[str, str]:
@@ -415,3 +417,40 @@ class TestPythonFormatTimeoutBudget:
415
417
  budgeted_total = auto_formatter_module.budgeted_python_format_seconds()
416
418
 
417
419
  assert budgeted_total < _registered_auto_formatter_timeout()
420
+
421
+
422
+ def _record_creation_flags(
423
+ auto_formatter_module: ModuleType, monkeypatch: pytest.MonkeyPatch
424
+ ) -> dict[str, object]:
425
+ all_recorded_keyword_arguments: dict[str, object] = {}
426
+
427
+ def _record(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
428
+ del args
429
+ all_recorded_keyword_arguments.update(kwargs)
430
+ return subprocess.CompletedProcess([], 1, stdout="", stderr="")
431
+
432
+ monkeypatch.setattr(auto_formatter_module.subprocess, "run", _record)
433
+ return all_recorded_keyword_arguments
434
+
435
+
436
+ def test_untracked_probe_should_start_without_a_console_window(
437
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
438
+ ) -> None:
439
+ auto_formatter_module = _load_auto_formatter_module()
440
+ all_recorded_keyword_arguments = _record_creation_flags(auto_formatter_module, monkeypatch)
441
+
442
+ assert auto_formatter_module.is_untracked_in_git(str(tmp_path / "module.py")) is True
443
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
444
+
445
+
446
+ def test_formatter_command_should_start_without_a_console_window(
447
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
448
+ ) -> None:
449
+ auto_formatter_module = _load_auto_formatter_module()
450
+ all_recorded_keyword_arguments = _record_creation_flags(auto_formatter_module, monkeypatch)
451
+
452
+ auto_formatter_module._run_command(
453
+ ["ruff", "format"], str(tmp_path / "module.py"), HOOK_RUN_TIMEOUT_SECONDS
454
+ )
455
+
456
+ assert all_recorded_keyword_arguments["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "7.4.1",
3
+ "version": "7.6.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,13 +2,13 @@
2
2
 
3
3
  Route every user-directed question through the `AskUserQuestion` tool — never a plain-text question in a response's final paragraph. Structure: concise `question`, `header` of 12 chars or fewer, 2-4 options (the UI adds the "Other" fallback), `multiSelect` only when choices genuinely combine.
4
4
 
5
- The `question_to_user_enforcer` Stop hook blocks a response whose final paragraph (after stripping code fences, inline code, and blockquotes) ends in a question mark or contains ask-phrases ("would you like", "should I", "let me know if", ...). Rhetorical questions answered in the same paragraph, and questions inside code or blockquotes, pass. `verify-before-asking` gates whether the question belongs to the user at all.
5
+ No hook checks this. Read your own final paragraph before you send it. A paragraph that ends in a question mark, or that carries an ask-phrase such as "would you like", "should I", or "let me know if", belongs in an `AskUserQuestion` call instead. A rhetorical question answered in the same paragraph is fine, and so is a question inside code or a blockquote. `verify-before-asking` settles whether the question belongs to the user at all.
6
6
 
7
7
  ## The question block stays lean
8
8
 
9
9
  `AskUserQuestion` renders as one plain unformatted text block. Detail — plans, counts, tradeoffs, background — goes in chat text before the call. The block itself carries a lean question and short choices. When a choice needs formatting, an inline visualizer tool carries it.
10
10
 
11
- The `ask_user_question_shape_blocker` PreToolUse hook denies an `AskUserQuestion` call whose `question` text or whose `description` under any of the `options` carries chat detail:
11
+ No hook denies an over-full block, so keep an `AskUserQuestion` call inside these caps yourself. They apply to the `question` text and to the `description` under any of the `options`:
12
12
 
13
13
  | What the block carries | Cap |
14
14
  |---|---|
@@ -28,4 +28,4 @@ Structure is read at block level on the raw text: a marker counts when it opens
28
28
 
29
29
  An inline code span — a path, a flag, a command the reader needs verbatim — weighs one word against either word cap on both fields, so a question naming `--dry-run` and a choice naming `C:\dev\gate.py` both pass. A span sits inside a line, so it never opens one with a marker.
30
30
 
31
- A sentence closes on `.`, `!`, or `?` followed by a capitalized word or by the end of the text. A word is any whitespace-separated token carrying a letter or a digit. The denial names each cap the block broke and sends the detail back to chat text.
31
+ A sentence closes on `.`, `!`, or `?` followed by a capitalized word or by the end of the text. A word is any whitespace-separated token carrying a letter or a digit. When a block breaks a cap, move that detail back into chat text before the call.
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Canonical review contract:** [`CODE_RULES.md`](../docs/CODE_RULES.md) — the human and AI review contract for code quality, loaded on demand.
4
4
  > **Checked-in pointer:** [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) — the file Cursor BugBot reads; it points at `CODE_RULES.md`.
5
- > **Production enforcement:** `hooks/blocking/code_rules_enforcer.py` — hand-maintained Write/Edit gates; each mechanical rule carries a synchronization test.
5
+ > **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it. Use `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit, and CI runs the same lint against the merge base. Each mechanical rule carries a synchronization test.
6
6
 
7
7
  ## Policy surface map
8
8
 
@@ -10,7 +10,8 @@
10
10
  |---|---|---|
11
11
  | Contract | `docs/CODE_RULES.md` | Full review criteria for PR agents, loaded on demand |
12
12
  | Pointer | `.cursor/BUGBOT.md` | Checked-in file Cursor BugBot reads; points at `CODE_RULES.md` |
13
- | Enforcer | `hooks/blocking/code_rules_enforcer.py` | Hand-maintained blockers; not generated from the docs |
13
+ | Enforcer | `hooks/blocking/code_rules_enforcer.py` | Hand-maintained checks the staged policy lint runs; not generated from the docs |
14
+ | Lint | `scripts/cde_lint.py` | Runs the enforcer and the other policy rules over staged or changed files |
14
15
  | Session rules | `rules/*.md` | Runtime session policy (questions, tasks, shell) |
15
16
 
16
17
  Load `CODE_RULES.md` when reviewing a PR, resolving a policy conflict, or generating code. Prefer linking this ref over restating rules.
@@ -6,4 +6,6 @@ How to ask: plain language, one option per path with its tradeoff in the descrip
6
6
 
7
7
  Not a fork (just proceed): trivially reversible internal-only choices, anything the codebase / stated goal / an existing rule already determines (see `verify-before-asking`), or a single viable path.
8
8
 
9
+ Naming a gap in chat is not surfacing a fork. Saying "no production path calls this yet" or "that is a follow-up" describes the fork; it does not ask it. Shipping code that nothing calls is a fork, because the alternative — wire the caller, or hold the change — changes the deliverable. Route it through `AskUserQuestion` before the merge, not into the closing summary after it.
10
+
9
11
  When an option you offered names a repair and the answer leaves it undone, write it down before the turn moves on: a todo entry, a `spawn_task` chip, or a filed issue. An answer of "no preference" settles which path you take now. It leaves the repair you found still waiting.