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,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
+ ]
@@ -15,6 +15,9 @@ from verification_notice import build_verification_notice
15
15
  from verification_notice_context import VerificationNoticeContext
16
16
  from verification_start import start_automatic_advisory
17
17
 
18
+ WINDOWS_NO_WINDOW_FLAG = 0x08000000
19
+ EXPECTED_HIDDEN_WINDOW_FLAGS = WINDOWS_NO_WINDOW_FLAG if sys.platform == "win32" else 0
20
+
18
21
 
19
22
  def build_context(
20
23
  repository_root: Path,
@@ -102,6 +105,7 @@ def _assert_launch_matches_runner(
102
105
  assert launch_options["stdin"] is subprocess.DEVNULL
103
106
  assert launch_options["stdout"] is subprocess.DEVNULL
104
107
  assert launch_options["stderr"] is subprocess.DEVNULL
108
+ assert launch_options["creationflags"] == EXPECTED_HIDDEN_WINDOW_FLAGS
105
109
  assert "--once" not in command
106
110
 
107
111
 
@@ -36,6 +36,8 @@ from git_hooks_constants.verification_notice_constants import (
36
36
  )
37
37
  from local_verification.manifest import ManifestRunFatal, load_manifest
38
38
 
39
+ from subprocess_window_access import hidden_window_creation_flags
40
+
39
41
 
40
42
  @dataclass(frozen=True)
41
43
  class VerificationNoticeContext:
@@ -202,6 +204,7 @@ def _run_git_query(
202
204
  encoding=GIT_OUTPUT_ENCODING_NAME,
203
205
  errors=GIT_OUTPUT_DECODE_ERRORS_POLICY,
204
206
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
207
+ creationflags=hidden_window_creation_flags(),
205
208
  )
206
209
  except (OSError, subprocess.SubprocessError, UnicodeError):
207
210
  return None
@@ -13,7 +13,6 @@ from git_hooks_constants.verification_notice_constants import (
13
13
  ALL_RUNNER_FIELDS,
14
14
  AUTOMATIC_ADVISORY_CLI_FILE_NAME,
15
15
  AUTOMATIC_ADVISORY_DIRECTORY_NAME,
16
- CREATE_NO_WINDOW_ATTRIBUTE,
17
16
  EXECUTOR_DIRECTORY_NAME,
18
17
  GIT_DIRECTORY_NAME,
19
18
  JSON_ENCODING,
@@ -22,10 +21,15 @@ from git_hooks_constants.verification_notice_constants import (
22
21
  RUNNER_PYTHON_FIELD,
23
22
  RUNNER_SETTINGS_FIELD,
24
23
  TARGET_REPOSITORY_REMOTE,
25
- WINDOWS_PLATFORM,
26
24
  )
27
25
  from verification_notice_context import VerificationNoticeContext
28
26
 
27
+ _subprocess_window_hooks_directory = str(Path(__file__).resolve().parents[2] / "hooks")
28
+ if _subprocess_window_hooks_directory not in sys.path:
29
+ sys.path.append(_subprocess_window_hooks_directory)
30
+
31
+ from hooks_constants.subprocess_window import hidden_window_creation_flags
32
+
29
33
 
30
34
  def start_automatic_advisory(context: VerificationNoticeContext) -> None:
31
35
  """Start the detached advisory poller when native metadata is valid."""
@@ -66,7 +70,7 @@ def _launch_advisory(
66
70
  stdin=subprocess.DEVNULL,
67
71
  stdout=subprocess.DEVNULL,
68
72
  stderr=subprocess.DEVNULL,
69
- creationflags=_windows_creation_flags(),
73
+ creationflags=hidden_window_creation_flags(),
70
74
  )
71
75
 
72
76
 
@@ -93,8 +97,3 @@ def load_runner_configuration(
93
97
  return None
94
98
  return python_path, settings_path
95
99
 
96
-
97
- def _windows_creation_flags() -> int:
98
- if sys.platform != WINDOWS_PLATFORM:
99
- return 0
100
- return getattr(subprocess, CREATE_NO_WINDOW_ATTRIBUTE, 0)
package/hooks/hooks.json CHANGED
@@ -12,6 +12,16 @@
12
12
  }
13
13
  ]
14
14
  },
15
+ {
16
+ "matcher": "Bash",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/bash_pre_tool_use_dispatcher.py",
21
+ "timeout": 60
22
+ }
23
+ ]
24
+ },
15
25
  {
16
26
  "matcher": "EnterWorktree",
17
27
  "hooks": [
@@ -5,6 +5,10 @@ finishes. Reuses ``BashHostedHookEntry`` and the Bash-only tool-name set from
5
5
  ``bash_pre_tool_use_dispatcher_constants`` -- the entry shape and the tool-name
6
6
  membership question are identical on the PostToolUse side, so this module adds
7
7
  no second copy of either.
8
+
9
+ Also the one home for the harness PostToolUse output contract every hook on
10
+ this roster shares: the exit-code prefix a failed call's response text opens
11
+ with, and the three payload keys that carry a context note back to the agent.
8
12
  """
9
13
 
10
14
  from __future__ import annotations
@@ -17,8 +21,10 @@ from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
17
21
 
18
22
  __all__ = [
19
23
  "ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES",
24
+ "EXIT_CODE_ERROR_PREFIX",
20
25
  "POST_TOOL_USE_HOOK_EVENT_NAME",
21
26
  "HOOK_SPECIFIC_OUTPUT_KEY",
27
+ "HOOK_EVENT_NAME_KEY",
22
28
  "ADDITIONAL_CONTEXT_KEY",
23
29
  "ADDITIONAL_CONTEXT_JOIN_SEPARATOR",
24
30
  ]
@@ -26,9 +32,12 @@ __all__ = [
26
32
  ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
27
33
  BashHostedHookEntry("observability/test_failure_recorder.py", ALL_BASH_ONLY_TOOL_NAMES),
28
34
  BashHostedHookEntry("advisory/pr_done_reminder.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
35
+ BashHostedHookEntry("advisory/msys_path_conversion_advisor.py", ALL_BASH_ONLY_TOOL_NAMES),
29
36
  )
30
37
 
38
+ EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
31
39
  POST_TOOL_USE_HOOK_EVENT_NAME: str = "PostToolUse"
32
40
  HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
41
+ HOOK_EVENT_NAME_KEY: str = "hookEventName"
33
42
  ADDITIONAL_CONTEXT_KEY: str = "additionalContext"
34
43
  ADDITIONAL_CONTEXT_JOIN_SEPARATOR: str = "\n\n"
@@ -54,4 +54,9 @@ class BashHostedHookEntry:
54
54
  applicable_tool_names: frozenset[str]
55
55
 
56
56
 
57
- ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = ()
57
+ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
58
+ BashHostedHookEntry(
59
+ script_relative_path="blocking/msys_rev_path_rewriter.py",
60
+ applicable_tool_names=ALL_BASH_ONLY_TOOL_NAMES,
61
+ ),
62
+ )
@@ -132,12 +132,14 @@ ALL_TOKEN_ANCHORED_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
132
132
  "pragma:",
133
133
  )
134
134
  ALL_TOKEN_ANCHORED_DIRECTIVE_BOUNDARY_CHARACTERS: frozenset[str] = frozenset({":"})
135
+ STEALTH_KEEP_COMMENT_MARKER: str = "STEALTH: Keep"
135
136
  ALL_FREE_FORM_EXEMPT_COMMENT_BODIES: tuple[str, ...] = (
136
137
  "type:",
137
138
  "TODO",
138
139
  "FIXME",
139
140
  "HACK",
140
141
  "XXX",
142
+ STEALTH_KEEP_COMMENT_MARKER,
141
143
  )
142
144
  CHAINED_INLINE_COMMENT_PATTERN = re.compile(r"#")
143
145
  ALL_JAVASCRIPT_EXEMPT_COMMENT_PREFIXES: tuple[str, ...] = (
@@ -0,0 +1,48 @@
1
+ """Constants for the MSYS path-conversion context advisory.
2
+
3
+ The advisory is a PostToolUse observer on Bash. It never blocks. When a git
4
+ call fails because Git Bash rewrote a ``<rev>:<path>`` argument before git saw
5
+ it, the advisory adds one loud note naming the two exports that turn the
6
+ rewriting off.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ __all__ = [
12
+ "ALL_MANGLED_ARGUMENT_MARKERS",
13
+ "MANGLED_ARGUMENT_CLOSING_QUOTE",
14
+ "ALL_MSYS_MANGLING_CHARACTERS",
15
+ "ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES",
16
+ "ADVISORY_HEADER",
17
+ "MANGLED_ARGUMENT_LINE_TEMPLATE",
18
+ "MANGLING_EXPLANATION_LINE",
19
+ "FIX_LINE",
20
+ "SAME_COMMAND_LINE",
21
+ "ADVISORY_LINE_SEPARATOR",
22
+ ]
23
+
24
+ AMBIGUOUS_ARGUMENT_MARKER: str = "fatal: ambiguous argument '"
25
+ INVALID_OBJECT_NAME_MARKER: str = "fatal: invalid object name '"
26
+ ALL_MANGLED_ARGUMENT_MARKERS: tuple[str, ...] = (
27
+ AMBIGUOUS_ARGUMENT_MARKER,
28
+ INVALID_OBJECT_NAME_MARKER,
29
+ )
30
+ MANGLED_ARGUMENT_CLOSING_QUOTE: str = "'"
31
+ ALL_MSYS_MANGLING_CHARACTERS: frozenset[str] = frozenset({"\\", ";"})
32
+ MSYS_PATH_CONVERSION_VARIABLE_NAME: str = "MSYS_NO_PATHCONV"
33
+ MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME: str = "MSYS2_ARG_CONV_EXCL"
34
+ ALL_MSYS_PATH_CONVERSION_VARIABLE_NAMES: tuple[str, ...] = (
35
+ MSYS_PATH_CONVERSION_VARIABLE_NAME,
36
+ MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME,
37
+ )
38
+ MSYS_EXPORT_FIX_LINE: str = "export MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*'"
39
+
40
+ ADVISORY_HEADER: str = "=== MSYS PATH CONVERSION (context reminder, never a block) ==="
41
+ MANGLED_ARGUMENT_LINE_TEMPLATE: str = "Git saw: {argument}"
42
+ MANGLING_EXPLANATION_LINE: str = (
43
+ "Git Bash rewrote a <rev>:<path> argument before git saw it, turning the "
44
+ "colon into a semicolon and the slashes into backslashes."
45
+ )
46
+ FIX_LINE: str = "Fix: " + MSYS_EXPORT_FIX_LINE
47
+ SAME_COMMAND_LINE: str = "Put that export and the git command in the same command."
48
+ ADVISORY_LINE_SEPARATOR: str = "\n"
@@ -0,0 +1,37 @@
1
+ """Constants for the MSYS ``<rev>:<path>`` argument-exclusion rewriter.
2
+
3
+ Git Bash converts a git argument such as ``origin/main:.claude/settings.json``
4
+ before git sees it. ``MSYS2_ARG_CONV_EXCL`` takes a semicolon-separated list of
5
+ argument prefixes to leave alone, so naming one ``<rev>:`` prefix per detected
6
+ token keeps every other argument in the command converting as before.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from hooks_constants.msys_path_conversion_advisor_constants import (
12
+ MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME,
13
+ )
14
+
15
+ __all__ = [
16
+ "GIT_PROGRAM_NAME",
17
+ "REVISION_PATH_SEPARATOR",
18
+ "REVISION_PATH_SPLIT_COUNT",
19
+ "REVISION_SLASH_CHARACTER",
20
+ "ALL_CONVERTED_PATH_START_CHARACTERS",
21
+ "ALL_UNCONVERTED_PATH_START_PREFIXES",
22
+ "ALL_REVISION_FORBIDDEN_CHARACTERS",
23
+ "EXCLUSION_PREFIX_JOIN_SEPARATOR",
24
+ "EXCLUSION_EXPORT_TEMPLATE",
25
+ ]
26
+
27
+ GIT_PROGRAM_NAME: str = "git"
28
+ REVISION_PATH_SEPARATOR: str = ":"
29
+ REVISION_PATH_SPLIT_COUNT: int = 1
30
+ REVISION_SLASH_CHARACTER: str = "/"
31
+ ALL_CONVERTED_PATH_START_CHARACTERS: tuple[str, ...] = ("/", ".")
32
+ ALL_UNCONVERTED_PATH_START_PREFIXES: tuple[str, ...] = ("./", "../")
33
+ ALL_REVISION_FORBIDDEN_CHARACTERS: tuple[str, ...] = ("'", '"')
34
+ EXCLUSION_PREFIX_JOIN_SEPARATOR: str = ";"
35
+ EXCLUSION_EXPORT_TEMPLATE: str = (
36
+ "export " + MSYS_ARGUMENT_CONVERSION_EXCLUSION_VARIABLE_NAME + "='{all_prefixes}'; "
37
+ )
@@ -0,0 +1,34 @@
1
+ """Shared stdout emitter for PostToolUse hooks that add context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ from hooks_constants.bash_post_call_dispatcher_constants import (
9
+ ADDITIONAL_CONTEXT_KEY,
10
+ HOOK_EVENT_NAME_KEY,
11
+ HOOK_SPECIFIC_OUTPUT_KEY,
12
+ POST_TOOL_USE_HOOK_EVENT_NAME,
13
+ )
14
+
15
+
16
+ def write_post_tool_use_context_to_stdout(context_text: str) -> None:
17
+ """Write one PostToolUse additionalContext payload to stdout.
18
+
19
+ A hosted hook writes its note here and the Bash PostToolUse dispatcher
20
+ reads it back, joins it with the other hooks' notes, and writes the joined
21
+ text here in turn. Both ends of that hop share this one payload shape
22
+ rather than each carrying a copy of it.
23
+
24
+ Args:
25
+ context_text: The advisory text the dispatcher forwards.
26
+ """
27
+ payload = {
28
+ HOOK_SPECIFIC_OUTPUT_KEY: {
29
+ HOOK_EVENT_NAME_KEY: POST_TOOL_USE_HOOK_EVENT_NAME,
30
+ ADDITIONAL_CONTEXT_KEY: context_text,
31
+ }
32
+ }
33
+ sys.stdout.write(json.dumps(payload))
34
+ sys.stdout.flush()
@@ -19,7 +19,6 @@ __all__ = [
19
19
  "ALL_GH_PR_VIEW_ARGUMENTS",
20
20
  "GH_PR_VIEW_TIMEOUT_SECONDS",
21
21
  "NO_PULL_REQUEST_MARKER",
22
- "EXIT_CODE_ERROR_PREFIX",
23
22
  "DONE_LABEL_NAME",
24
23
  "MERGEABLE_CLEAN_VALUE",
25
24
  "MERGEABLE_CONFLICTING_VALUE",
@@ -59,7 +58,6 @@ ALL_GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = (
59
58
  )
60
59
  GH_PR_VIEW_TIMEOUT_SECONDS: int = 20
61
60
  NO_PULL_REQUEST_MARKER: str = "no pull requests found"
62
- EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
63
61
 
64
62
  DONE_LABEL_NAME: str = "done"
65
63
 
@@ -0,0 +1,48 @@
1
+ """Shared stdout emitter for PreToolUse hooks that allow a rewritten tool input."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+
8
+ from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
9
+ ALLOW_DECISION,
10
+ HOOK_EVENT_NAME,
11
+ )
12
+
13
+ __all__ = [
14
+ "HOOK_SPECIFIC_OUTPUT_KEY",
15
+ "HOOK_EVENT_NAME_KEY",
16
+ "PERMISSION_DECISION_KEY",
17
+ "UPDATED_INPUT_KEY",
18
+ "write_pre_tool_use_allow_to_stdout",
19
+ ]
20
+
21
+ HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
22
+ HOOK_EVENT_NAME_KEY: str = "hookEventName"
23
+ PERMISSION_DECISION_KEY: str = "permissionDecision"
24
+ UPDATED_INPUT_KEY: str = "updatedInput"
25
+
26
+
27
+ def write_pre_tool_use_allow_to_stdout(
28
+ all_updated_tool_input_fields: dict[str, object],
29
+ ) -> None:
30
+ """Write one PreToolUse allow payload carrying a rewritten tool input.
31
+
32
+ A rewriter writes its replacement input here and the Bash PreToolUse
33
+ dispatcher reads it back, carrying the updatedInput through to the harness.
34
+ Both ends of that hop share this one payload shape rather than each holding
35
+ a copy of it.
36
+
37
+ Args:
38
+ all_updated_tool_input_fields: The full tool_input the tool call should run with.
39
+ """
40
+ payload = {
41
+ HOOK_SPECIFIC_OUTPUT_KEY: {
42
+ HOOK_EVENT_NAME_KEY: HOOK_EVENT_NAME,
43
+ PERMISSION_DECISION_KEY: ALLOW_DECISION,
44
+ UPDATED_INPUT_KEY: all_updated_tool_input_fields,
45
+ }
46
+ }
47
+ sys.stdout.write(json.dumps(payload))
48
+ sys.stdout.flush()
@@ -0,0 +1,141 @@
1
+ """Creation flags that keep a spawned child off the operator's screen on Windows.
2
+
3
+ A hook or script often runs with no console of its own. When such a process
4
+ starts a child on Windows, the operating system gives that child a brand new
5
+ console, and the operator sees a black window flash open and shut while agents
6
+ work. ``CREATE_NO_WINDOW`` tells Windows to skip that console. Every child this
7
+ package spawns passes the flag, so the desktop stays quiet::
8
+
9
+ subprocess.run(
10
+ ["git", "status"],
11
+ creationflags=hidden_window_creation_flags(),
12
+ )
13
+
14
+ On Windows that call returns ``0x08000000`` and no window appears. On Linux and
15
+ macOS it returns ``0``, which is the value ``subprocess`` already uses when a
16
+ caller passes no ``creationflags`` at all, so behaviour there is unchanged.
17
+ ``CREATE_NO_WINDOW`` is absent from a non-Windows Python build, so the lookup
18
+ reads the attribute at call time and falls back to ``0``.
19
+
20
+ ``CREATE_NO_WINDOW`` gives the child a console of its own. A child that writes
21
+ to a stream the caller redirected is unaffected, because the redirected handle
22
+ travels with it. A child that inherits the caller's terminal instead loses that
23
+ terminal, and the operator stops seeing its output. A few call sites hand their
24
+ streams straight to the operator, and those pass
25
+ ``startupinfo=inherited_stream_startup_info()`` instead. That startup record
26
+ keeps the inherited streams and hides the console window, so the operator still
27
+ reads the output and still sees no window.
28
+
29
+ ``subprocess.STARTUPINFO`` is a Windows-only name. Typeshed declares it under a
30
+ ``sys.platform == "win32"`` guard, so a type check that runs on Linux, as this
31
+ repository's continuous integration does, cannot see the name at all. The
32
+ startup record is therefore described here as a structural ``Protocol`` naming
33
+ the two fields this module sets, and the record itself is fetched through
34
+ ``getattr`` at call time. Both moves match what the module already does for the
35
+ creation flags, and they keep the runtime ``sys.platform`` test in one place, so
36
+ one function serves every platform.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import subprocess
42
+ import sys
43
+ from typing import Protocol
44
+
45
+ from hooks_constants.subprocess_window_constants import (
46
+ CREATE_NEW_PROCESS_GROUP_ATTRIBUTE,
47
+ CREATE_NO_WINDOW_ATTRIBUTE,
48
+ HIDE_WINDOW_COMMAND_ATTRIBUTE,
49
+ NO_CREATION_FLAGS,
50
+ SHOW_WINDOW_FLAG_ATTRIBUTE,
51
+ STARTUP_INFO_ATTRIBUTE,
52
+ WINDOWS_PLATFORM,
53
+ )
54
+
55
+ __all__ = [
56
+ "HiddenWindowStartupInfo",
57
+ "detached_hidden_window_creation_flags",
58
+ "hidden_window_creation_flags",
59
+ "inherited_stream_startup_info",
60
+ ]
61
+
62
+
63
+ class HiddenWindowStartupInfo(Protocol):
64
+ """The two ``subprocess.STARTUPINFO`` fields that hide a child's console."""
65
+
66
+ dwFlags: int
67
+ wShowWindow: int
68
+
69
+
70
+ def _windows_flag(flag_attribute_name: str) -> int:
71
+ """Return one Windows creation flag, or zero on any other platform.
72
+
73
+ Args:
74
+ flag_attribute_name: The name of a ``subprocess`` creation-flag constant.
75
+
76
+ Returns:
77
+ The flag value on Windows, and zero elsewhere or when the running
78
+ Python build does not define that constant.
79
+ """
80
+ if sys.platform != WINDOWS_PLATFORM:
81
+ return NO_CREATION_FLAGS
82
+ return int(getattr(subprocess, flag_attribute_name, NO_CREATION_FLAGS))
83
+
84
+
85
+ def hidden_window_creation_flags() -> int:
86
+ """Return the creation flags that start a child without a console window.
87
+
88
+ Pass the result as the ``creationflags`` argument of ``subprocess.run``,
89
+ ``subprocess.Popen``, ``subprocess.check_output``, or ``subprocess.call``::
90
+
91
+ subprocess.run(["git", "rev-parse", "HEAD"],
92
+ creationflags=hidden_window_creation_flags())
93
+
94
+ Returns:
95
+ The hidden-console flag on Windows, and zero on every other platform.
96
+ """
97
+ return _windows_flag(CREATE_NO_WINDOW_ATTRIBUTE)
98
+
99
+
100
+ def detached_hidden_window_creation_flags() -> int:
101
+ """Return the creation flags for a long-lived child that outlives its parent.
102
+
103
+ A detached poller needs its own process group so a console interrupt aimed
104
+ at the parent never reaches it, and it needs the hidden console for the same
105
+ reason every other child does::
106
+
107
+ subprocess.Popen(argv,
108
+ creationflags=detached_hidden_window_creation_flags())
109
+
110
+ Returns:
111
+ A new process group combined with the hidden-console flag on Windows,
112
+ and zero on every other platform.
113
+ """
114
+ return _windows_flag(CREATE_NEW_PROCESS_GROUP_ATTRIBUTE) | hidden_window_creation_flags()
115
+
116
+
117
+ def inherited_stream_startup_info() -> HiddenWindowStartupInfo | None:
118
+ """Return a startup record that hides the console of a stream-inheriting child.
119
+
120
+ Use this at a call site that lets the child write straight to the operator's
121
+ terminal, where ``hidden_window_creation_flags`` would take that terminal
122
+ away::
123
+
124
+ subprocess.run(["python", "-m", "pytest"],
125
+ startupinfo=inherited_stream_startup_info())
126
+
127
+ Returns:
128
+ A Windows startup record asking for a hidden window, and ``None`` on
129
+ every other platform, which is the value ``subprocess`` already uses
130
+ when a caller passes no ``startupinfo`` at all.
131
+ """
132
+ if sys.platform != WINDOWS_PLATFORM:
133
+ return None
134
+ startup_information = getattr(subprocess, STARTUP_INFO_ATTRIBUTE)()
135
+ startup_information.dwFlags |= getattr(
136
+ subprocess, SHOW_WINDOW_FLAG_ATTRIBUTE, NO_CREATION_FLAGS
137
+ )
138
+ startup_information.wShowWindow = getattr(
139
+ subprocess, HIDE_WINDOW_COMMAND_ATTRIBUTE, NO_CREATION_FLAGS
140
+ )
141
+ return startup_information
@@ -0,0 +1,9 @@
1
+ """Configuration constants for the shared hidden-console creation flags."""
2
+
3
+ WINDOWS_PLATFORM: str = "win32"
4
+ CREATE_NO_WINDOW_ATTRIBUTE: str = "CREATE_NO_WINDOW"
5
+ CREATE_NEW_PROCESS_GROUP_ATTRIBUTE: str = "CREATE_NEW_PROCESS_GROUP"
6
+ STARTUP_INFO_ATTRIBUTE: str = "STARTUPINFO"
7
+ SHOW_WINDOW_FLAG_ATTRIBUTE: str = "STARTF_USESHOWWINDOW"
8
+ HIDE_WINDOW_COMMAND_ATTRIBUTE: str = "SW_HIDE"
9
+ NO_CREATION_FLAGS: int = 0
@@ -7,22 +7,29 @@ from hooks_constants.bash_post_call_dispatcher_constants import (
7
7
  from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
8
8
 
9
9
 
10
- def test_roster_runs_the_recorder_then_the_pr_done_reminder() -> None:
10
+ def test_roster_runs_the_recorder_then_the_pr_reminder_then_the_msys_advisor() -> None:
11
11
  all_script_paths = [
12
12
  each_entry.script_relative_path for each_entry in ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES
13
13
  ]
14
14
  assert all_script_paths == [
15
15
  "observability/test_failure_recorder.py",
16
16
  "advisory/pr_done_reminder.py",
17
+ "advisory/msys_path_conversion_advisor.py",
17
18
  ]
18
19
 
19
20
 
20
21
  def test_pr_done_reminder_also_serves_the_powershell_tool() -> None:
21
- reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[-1]
22
+ reminder_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[1]
22
23
  assert reminder_entry.script_relative_path == "advisory/pr_done_reminder.py"
23
24
  assert "PowerShell" in reminder_entry.applicable_tool_names
24
25
 
25
26
 
27
+ def test_msys_advisor_serves_the_bash_tool_only() -> None:
28
+ advisor_entry = ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES[-1]
29
+ assert advisor_entry.script_relative_path == "advisory/msys_path_conversion_advisor.py"
30
+ assert advisor_entry.applicable_tool_names == frozenset({"Bash"})
31
+
32
+
26
33
  def test_context_forwarding_keys_match_the_hook_output_contract() -> None:
27
34
  assert constants.POST_TOOL_USE_HOOK_EVENT_NAME == "PostToolUse"
28
35
  assert constants.HOOK_SPECIFIC_OUTPUT_KEY == "hookSpecificOutput"
@@ -7,7 +7,14 @@ ALL_CONSTANT_BINDINGS = run_path(
7
7
  str(Path(__file__).with_name("bash_pre_tool_use_dispatcher_constants.py"))
8
8
  )
9
9
  ALL_BASH_HOSTED_HOOK_ENTRIES = ALL_CONSTANT_BINDINGS["ALL_BASH_HOSTED_HOOK_ENTRIES"]
10
+ ALL_BASH_ONLY_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_ONLY_TOOL_NAMES"]
11
+ BashHostedHookEntry = ALL_CONSTANT_BINDINGS["BashHostedHookEntry"]
10
12
 
11
13
 
12
- def test_roster_has_no_blocking_hooks() -> None:
13
- assert ALL_BASH_HOSTED_HOOK_ENTRIES == ()
14
+ def test_roster_hosts_only_the_msys_rewriter_and_no_blocking_hook() -> None:
15
+ assert ALL_BASH_HOSTED_HOOK_ENTRIES == (
16
+ BashHostedHookEntry(
17
+ script_relative_path="blocking/msys_rev_path_rewriter.py",
18
+ applicable_tool_names=ALL_BASH_ONLY_TOOL_NAMES,
19
+ ),
20
+ )
@@ -17,6 +17,7 @@ if str(_HOOKS_ROOT) not in sys.path:
17
17
  sys.path.insert(0, str(_HOOKS_ROOT))
18
18
 
19
19
  from hooks_constants.code_rules_enforcer_constants import (
20
+ ALL_FREE_FORM_EXEMPT_COMMENT_BODIES,
20
21
  ALL_VALIDATION_PHASES,
21
22
  CONFIG_DIRECTORY_SEGMENT,
22
23
  CONSTANTS_MODULE_SUFFIX,
@@ -26,6 +27,7 @@ from hooks_constants.code_rules_enforcer_constants import (
26
27
  JSDOC_RETURNS_STRUCTURED_OBJECT_PROMISE_PATTERN,
27
28
  RETURN_CALL_OPENING_PARENTHESIS_PATTERN,
28
29
  SCHEMA_OPTIONS_PROPERTY_KEY_PATTERN,
30
+ STEALTH_KEEP_COMMENT_MARKER,
29
31
  UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE,
30
32
  apply_edits,
31
33
  edits_for_tool,
@@ -106,6 +108,10 @@ def test_constants_module_suffix_and_config_directory_segment_are_defined() -> N
106
108
  assert CONFIG_DIRECTORY_SEGMENT == "config"
107
109
 
108
110
 
111
+ def test_free_form_exempt_comment_bodies_include_stealth_keep_marker() -> None:
112
+ assert STEALTH_KEEP_COMMENT_MARKER in ALL_FREE_FORM_EXEMPT_COMMENT_BODIES
113
+
114
+
109
115
  def test_validation_phase_constants_are_reexported_and_render() -> None:
110
116
  assert ALL_VALIDATION_PHASES == {EDIT_LANE_PHASE, FULL_GATE_PHASE}
111
117
  rendered_message = UNKNOWN_VALIDATION_PHASE_MESSAGE_TEMPLATE.format(
@@ -1,7 +1,7 @@
1
1
  """Constants for the failing-test-run PostToolUse recorder hook.
2
2
 
3
3
  Holds the cheap substring check the hook runs before any shell parsing, the
4
- harness's own exit-code error prefix, the chaining and redirection operator
4
+ chaining and redirection operator
5
5
  tokens that keep the recognizer to one unchained command, the pytest program
6
6
  and Python interpreter basenames, and the flag-token marker the path
7
7
  extractor uses to skip an option rather than an existing test file argument.
@@ -20,7 +20,6 @@ import re
20
20
  BASH_TOOL_NAME = "Bash"
21
21
 
22
22
  ALL_PYTEST_MENTION_SUBSTRINGS: tuple[str, ...] = ("pytest", "py.test")
23
- EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
24
23
  OPTION_TOKEN_PREFIX: str = "-"
25
24
  NODE_ID_FILE_PATH_SEPARATOR: str = "::"
26
25
  ALL_CHAINING_OPERATOR_SUBSTRINGS: tuple[str, ...] = ("&&", "||", ";", "|", "\n")
@@ -0,0 +1,22 @@
1
+ """Tests for the shared PostToolUse context emitter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+ import pytest
8
+
9
+ from hooks_constants.post_tool_use_context import write_post_tool_use_context_to_stdout
10
+
11
+
12
+ def test_should_write_the_post_tool_use_payload_carrying_the_context_text(
13
+ capsys: pytest.CaptureFixture[str],
14
+ ) -> None:
15
+ write_post_tool_use_context_to_stdout("=== PR DONE CHECKLIST ===")
16
+
17
+ assert json.loads(capsys.readouterr().out) == {
18
+ "hookSpecificOutput": {
19
+ "hookEventName": "PostToolUse",
20
+ "additionalContext": "=== PR DONE CHECKLIST ===",
21
+ }
22
+ }