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.
- package/.agents/skills/everything-search/scripts/everything_search.py +11 -0
- package/.agents/skills/everything-search/scripts/test_everything_search.py +1 -0
- package/.agents/skills/fresh-branch/SKILL.md +1 -0
- package/.agents/skills/fresh-branch/scripts/fresh_branch_git_commands.py +3 -0
- package/.agents/skills/fresh-branch/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/syncing-submodules/SKILL.md +1 -0
- package/.agents/skills/syncing-submodules/scripts/submodule_sync.py +4 -0
- package/.agents/skills/syncing-submodules/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/SKILL.md +1 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +5 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +11 -0
- package/.agents/skills/test-runner/scripts/subprocess_window_access.py +43 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +9 -7
- package/_shared/pr-loop/scripts/check_bugbot_ci.py +3 -0
- package/_shared/pr-loop/scripts/check_convergence_gates.py +3 -0
- package/_shared/pr-loop/scripts/check_pending_reviews.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +5 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +4 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +3 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +3 -0
- package/_shared/pr-loop/scripts/codex_usage_probe.py +3 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +3 -0
- package/_shared/pr-loop/scripts/fetch_copilot_reviews.py +3 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +7 -0
- package/_shared/pr-loop/scripts/post_fix_reply.py +4 -0
- package/_shared/pr-loop/scripts/preflight.py +7 -0
- package/_shared/pr-loop/scripts/preflight_self_heal.py +5 -0
- package/_shared/pr-loop/scripts/run_codex_review.py +3 -0
- package/_shared/pr-loop/scripts/subprocess_window_access.py +41 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +5 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +3 -0
- package/_shared/process-tree/scripts/subprocess_window_access.py +41 -0
- package/bin/install-constants.mjs +4 -2
- package/bin/install.cursor-rules.test.mjs +57 -15
- package/bin/install.mjs +10 -16
- package/bin/install.policy-lint.test.mjs +1 -2
- package/bin/install.test.mjs +0 -1
- package/bin/pstack-model-preferences.claude.json +23 -0
- package/hooks/advisory/msys_path_conversion_advisor.py +200 -0
- package/hooks/advisory/pr_done_reminder.py +14 -13
- package/hooks/advisory/refactor_guard.py +8 -0
- package/hooks/advisory/test_msys_path_conversion_advisor.py +223 -0
- package/hooks/advisory/test_pr_done_reminder.py +22 -0
- package/hooks/advisory/test_refactor_guard.py +40 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +6 -11
- package/hooks/blocking/block_main_commit.py +4 -0
- package/hooks/blocking/code_rules_comments.py +39 -5
- package/hooks/blocking/msys_rev_path_rewriter.py +192 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +3 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +19 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +4 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +25 -0
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +16 -3
- package/hooks/blocking/test_bash_post_call_dispatcher.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +27 -0
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +10 -0
- package/hooks/blocking/test_msys_rev_path_rewriter.py +141 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +0 -2
- package/hooks/git-hooks/pull_request_handoff.py +3 -0
- package/hooks/git-hooks/subprocess_window_access.py +41 -0
- package/hooks/git-hooks/test_verification_start.py +4 -0
- package/hooks/git-hooks/verification_notice_context.py +3 -0
- package/hooks/git-hooks/verification_start.py +7 -8
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +9 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +6 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/msys_path_conversion_advisor_constants.py +48 -0
- package/hooks/hooks_constants/msys_rev_path_rewriter_constants.py +37 -0
- package/hooks/hooks_constants/post_tool_use_context.py +34 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +0 -2
- package/hooks/hooks_constants/pre_tool_use_allow_output.py +48 -0
- package/hooks/hooks_constants/subprocess_window.py +141 -0
- package/hooks/hooks_constants/subprocess_window_constants.py +9 -0
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +9 -2
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +6 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +1 -2
- package/hooks/hooks_constants/test_post_tool_use_context.py +22 -0
- package/hooks/hooks_constants/test_pre_tool_use_allow_output.py +27 -0
- package/hooks/hooks_constants/test_subprocess_window.py +128 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +3 -0
- package/hooks/observability/test_failure_recorder.py +3 -1
- package/hooks/validation/mypy_validator.py +4 -0
- package/hooks/validators/README.md +1 -0
- package/hooks/validators/git_checks.py +4 -0
- package/hooks/validators/health_check.py +3 -0
- package/hooks/validators/hook_timing_harness.py +3 -0
- package/hooks/validators/mypy_integration.py +4 -0
- package/hooks/validators/ruff_integration.py +6 -0
- package/hooks/validators/run_all_validators.py +7 -0
- package/hooks/validators/subprocess_window_access.py +41 -0
- package/hooks/workflow/auto_formatter.py +8 -0
- package/hooks/workflow/test_auto_formatter.py +39 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +3 -3
- package/rules/code-standards.md +3 -2
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/destructive-commands.md +19 -19
- package/rules/doc-inventory-integrity.md +4 -4
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +4 -2
- package/rules/filesystem-search.md +1 -1
- package/rules/git-workflow.md +34 -0
- package/rules/long-horizon-autonomy.md +2 -0
- package/rules/nas-ssh-invocation.md +3 -3
- package/rules/no-cross-skill-duplicate-helpers.md +6 -6
- package/rules/orphan-css-class.md +5 -5
- package/rules/paired-test-coverage.md +6 -6
- package/rules/plain-illustrative-docstrings.md +3 -3
- package/rules/pstack-models.md +4 -3
- package/rules/re-stage-before-commit.md +7 -7
- package/rules/retired-hook-prose.md +16 -0
- package/rules/shell-invocation.md +17 -1
- package/rules/testing.md +3 -5
- package/rules/verify-runtime-state.md +1 -1
- package/rules/windows-filesystem-safe.md +2 -2
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/automatic_advisory/checkout.py +8 -1
- package/scripts/automatic_advisory/cli.py +9 -3
- package/scripts/automatic_advisory/config/constants.py +0 -2
- package/scripts/automatic_advisory/execution.py +7 -1
- package/scripts/automatic_advisory/git.py +9 -1
- package/scripts/automatic_advisory/process_host.py +3 -0
- package/scripts/claude_chain_runner.py +3 -0
- package/scripts/durable_post_lint_config/config/constants.py +0 -3
- package/scripts/durable_post_lint_config/config/test_constants.py +5 -0
- package/scripts/gh_artifact_upload.py +3 -0
- package/scripts/grok_headless_runner.py +3 -0
- package/scripts/grok_patch_artifacts.py +4 -0
- package/scripts/grok_worker_preflight.py +3 -0
- package/scripts/invoke_code_review.py +3 -0
- package/scripts/local_verification/command_runner.py +3 -0
- package/scripts/local_verification/git_state.py +3 -0
- package/scripts/policy_lint/adapter_configuration.py +24 -0
- package/scripts/policy_lint/adapter_detectors.py +48 -2
- package/scripts/policy_lint/adapter_pairing.py +150 -2
- package/scripts/policy_lint/adapter_retired_hook_prose.py +201 -0
- package/scripts/policy_lint/adapters.py +82 -5
- package/scripts/policy_lint/config/approved_test_pairs.py +25 -9
- package/scripts/policy_lint/config/constants.py +77 -0
- package/scripts/policy_lint/registry.py +6 -1
- package/scripts/policy_lint/selection.py +37 -18
- package/scripts/policy_lint/selection_git.py +17 -2
- package/scripts/repository_checks/config/constants.py +5 -0
- package/scripts/repository_checks/config/policy_document.py +19 -0
- package/scripts/repository_checks/email_exemptions.py +14 -71
- package/scripts/repository_checks/path_exemptions.py +35 -0
- package/scripts/repository_checks/policy_document.py +120 -0
- package/scripts/repository_checks/tracked_secrets.py +27 -4
- package/scripts/setup_project_paths.py +3 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/subprocess_window_access.py +41 -0
- package/scripts/test_dispatcher_profile_import.py +4 -0
- package/scripts/test_durable_post_lint.py +42 -9
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +17 -11
- package/scripts/tests/test_email_exemptions.py +37 -0
- package/scripts/tests/test_path_exemptions.py +164 -0
- package/scripts/tests/test_policy_document.py +104 -0
- package/scripts/tests/test_policy_lint_rules_function_length_scope.py +101 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +375 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +116 -0
- package/scripts/tests/test_policy_lint_rules_retired_hook_prose.py +197 -0
- package/scripts/tests/test_policy_lint_selection.py +141 -0
- package/scripts/tests/test_subprocess_window_access.py +118 -0
- package/scripts/tests/test_tracked_secrets.py +79 -0
- package/scripts/validate_instruction_pairs.py +3 -0
- package/scripts/verify_installable_package.py +5 -0
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -8
- package/hooks/hooks_constants/destructive_command_environment_constants.py +0 -18
- package/hooks/hooks_constants/test_text_stripping.py +0 -39
- package/hooks/hooks_constants/text_stripping.py +0 -36
- package/scripts/automatic_advisory/window_flags.py +0 -41
- 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=
|
|
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
|
|
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[
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|