claude-dev-env 7.0.0 → 7.2.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/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Evaluate local verification state for the native Git notice."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from git_hooks_constants.verification_notice_constants import (
|
|
10
|
+
ALL_GIT_BASE_QUERY_PREFIX,
|
|
11
|
+
GIT_COMMAND_SUCCESS_EXIT_CODE,
|
|
12
|
+
HEX_DIGITS,
|
|
13
|
+
NOTICE_FAILED_LINE,
|
|
14
|
+
NOTICE_NO_PASS_LINE,
|
|
15
|
+
NOTICE_PASSED_LINE,
|
|
16
|
+
NOTICE_SETUP_PENDING_LINE,
|
|
17
|
+
NOTICE_STALE_LINE,
|
|
18
|
+
REPORT_AGGREGATE_FIELD,
|
|
19
|
+
REPORT_BASE_FIELD,
|
|
20
|
+
REPORT_EXIT_CODE_FIELD,
|
|
21
|
+
REPORT_FAILED_FIELD,
|
|
22
|
+
REPORT_HEAD_FIELD,
|
|
23
|
+
REPORT_INCOMPLETE_FIELD,
|
|
24
|
+
REPORT_INPUTS_UNCHANGED_FIELD,
|
|
25
|
+
REPORT_MANIFEST_DIGEST_FIELD,
|
|
26
|
+
REPORT_PASSED_FIELD,
|
|
27
|
+
REPORT_PUBLISHABLE_FIELD,
|
|
28
|
+
REPORT_STATUS_FIELD,
|
|
29
|
+
REPORT_TOTAL_FIELD,
|
|
30
|
+
REPORT_WORKTREE_CLEAN_FIELD,
|
|
31
|
+
RESOLVED_SHA_LENGTH,
|
|
32
|
+
STATUS_FAILED,
|
|
33
|
+
STATUS_PASSED,
|
|
34
|
+
STATUS_PENDING,
|
|
35
|
+
STATUS_STALE,
|
|
36
|
+
STATUS_UNVERIFIED,
|
|
37
|
+
)
|
|
38
|
+
from local_verification.manifest import (
|
|
39
|
+
ManifestRunFatal,
|
|
40
|
+
compute_manifest_digest,
|
|
41
|
+
load_manifest,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _VerificationNoticeContext(Protocol):
|
|
46
|
+
@property
|
|
47
|
+
def current_head(self) -> str | None: ...
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def repository_root(self) -> Path: ...
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def manifest_path(self) -> Path | None: ...
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def manifest_is_available(self) -> bool: ...
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def report_is_present(self) -> bool: ...
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def all_report_fields(self) -> Mapping[str, object] | None: ...
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _evaluate_context(
|
|
66
|
+
context: _VerificationNoticeContext,
|
|
67
|
+
run_git_query: Callable[[Path, tuple[str, ...], bool], str | None],
|
|
68
|
+
) -> tuple[str, str | None]:
|
|
69
|
+
context_state = _context_setup_state(context)
|
|
70
|
+
if context_state is not None:
|
|
71
|
+
return context_state, None
|
|
72
|
+
if context.current_head is None or context.all_report_fields is None:
|
|
73
|
+
return STATUS_UNVERIFIED, None
|
|
74
|
+
reported_head = _read_text_field(context.all_report_fields, REPORT_HEAD_FIELD)
|
|
75
|
+
if reported_head is None:
|
|
76
|
+
return STATUS_UNVERIFIED, None
|
|
77
|
+
if reported_head != context.current_head:
|
|
78
|
+
return STATUS_STALE, None
|
|
79
|
+
if _read_report_status(context.all_report_fields) == STATUS_FAILED:
|
|
80
|
+
return STATUS_FAILED, None
|
|
81
|
+
if _is_complete_pass(context, run_git_query):
|
|
82
|
+
return STATUS_PASSED, context.current_head
|
|
83
|
+
return STATUS_UNVERIFIED, None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _context_setup_state(context: _VerificationNoticeContext) -> str | None:
|
|
87
|
+
if context.all_report_fields is None:
|
|
88
|
+
return STATUS_UNVERIFIED if context.report_is_present else STATUS_PENDING
|
|
89
|
+
if context.all_report_fields is not None and context.manifest_path is None:
|
|
90
|
+
return STATUS_UNVERIFIED
|
|
91
|
+
if not context.manifest_is_available:
|
|
92
|
+
return STATUS_PENDING
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_text_field(
|
|
97
|
+
all_fields: Mapping[str, object],
|
|
98
|
+
field_name: str,
|
|
99
|
+
) -> str | None:
|
|
100
|
+
field_text = all_fields.get(field_name)
|
|
101
|
+
if not isinstance(field_text, str):
|
|
102
|
+
return None
|
|
103
|
+
return field_text.strip() or None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_report_status(all_report_fields: Mapping[str, object]) -> str | None:
|
|
107
|
+
status_text = _read_text_field(all_report_fields, REPORT_STATUS_FIELD)
|
|
108
|
+
if status_text:
|
|
109
|
+
return status_text
|
|
110
|
+
all_aggregate_fields = all_report_fields.get(REPORT_AGGREGATE_FIELD)
|
|
111
|
+
if isinstance(all_aggregate_fields, Mapping):
|
|
112
|
+
return _read_text_field(all_aggregate_fields, REPORT_STATUS_FIELD)
|
|
113
|
+
return None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _build_state_message(state: str, is_manifest_available: bool) -> str:
|
|
117
|
+
if not is_manifest_available:
|
|
118
|
+
return NOTICE_SETUP_PENDING_LINE
|
|
119
|
+
message_by_state = {
|
|
120
|
+
STATUS_STALE: NOTICE_STALE_LINE,
|
|
121
|
+
STATUS_FAILED: NOTICE_FAILED_LINE,
|
|
122
|
+
STATUS_PASSED: NOTICE_PASSED_LINE,
|
|
123
|
+
}
|
|
124
|
+
return message_by_state.get(state, NOTICE_NO_PASS_LINE)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _is_complete_pass(
|
|
128
|
+
context: _VerificationNoticeContext,
|
|
129
|
+
run_git_query: Callable[[Path, tuple[str, ...], bool], str | None],
|
|
130
|
+
) -> bool:
|
|
131
|
+
all_report_fields = context.all_report_fields
|
|
132
|
+
if all_report_fields is None or context.manifest_path is None:
|
|
133
|
+
return False
|
|
134
|
+
base_revision = _read_text_field(all_report_fields, REPORT_BASE_FIELD)
|
|
135
|
+
if not _report_has_complete_pass(all_report_fields, base_revision):
|
|
136
|
+
return False
|
|
137
|
+
if base_revision is None or not _base_matches_current(
|
|
138
|
+
context.repository_root,
|
|
139
|
+
base_revision,
|
|
140
|
+
run_git_query,
|
|
141
|
+
):
|
|
142
|
+
return False
|
|
143
|
+
if not _worktree_is_clean(context.repository_root, run_git_query):
|
|
144
|
+
return False
|
|
145
|
+
if not _manifest_digest_matches(all_report_fields, context.manifest_path):
|
|
146
|
+
return False
|
|
147
|
+
all_aggregate_fields = all_report_fields.get(REPORT_AGGREGATE_FIELD)
|
|
148
|
+
return isinstance(all_aggregate_fields, Mapping) and _aggregate_is_complete_pass(
|
|
149
|
+
all_aggregate_fields
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _report_has_complete_pass(
|
|
154
|
+
all_report_fields: Mapping[str, object],
|
|
155
|
+
base_revision: str | None,
|
|
156
|
+
) -> bool:
|
|
157
|
+
return (
|
|
158
|
+
_read_report_status(all_report_fields) == STATUS_PASSED
|
|
159
|
+
and _report_is_publishable(all_report_fields)
|
|
160
|
+
and base_revision is not None
|
|
161
|
+
and _is_resolved_sha(base_revision)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _report_is_publishable(all_report_fields: Mapping[str, object]) -> bool:
|
|
166
|
+
return all(
|
|
167
|
+
all_report_fields.get(each_field) is True
|
|
168
|
+
for each_field in (
|
|
169
|
+
REPORT_PUBLISHABLE_FIELD,
|
|
170
|
+
REPORT_WORKTREE_CLEAN_FIELD,
|
|
171
|
+
REPORT_INPUTS_UNCHANGED_FIELD,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _base_matches_current(
|
|
177
|
+
repository_root: Path,
|
|
178
|
+
base_revision: str,
|
|
179
|
+
run_git_query: Callable[[Path, tuple[str, ...], bool], str | None],
|
|
180
|
+
) -> bool:
|
|
181
|
+
current_base_revision = run_git_query(
|
|
182
|
+
repository_root,
|
|
183
|
+
(*ALL_GIT_BASE_QUERY_PREFIX, "origin/main^{commit}"),
|
|
184
|
+
False,
|
|
185
|
+
)
|
|
186
|
+
return current_base_revision == base_revision
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _worktree_is_clean(
|
|
190
|
+
repository_root: Path,
|
|
191
|
+
run_git_query: Callable[[Path, tuple[str, ...], bool], str | None],
|
|
192
|
+
) -> bool:
|
|
193
|
+
status_text = run_git_query(
|
|
194
|
+
repository_root,
|
|
195
|
+
("status", "--porcelain=v1", "--untracked-files=all"),
|
|
196
|
+
True,
|
|
197
|
+
)
|
|
198
|
+
return status_text == ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _is_resolved_sha(revision_text: str) -> bool:
|
|
202
|
+
return len(revision_text) == RESOLVED_SHA_LENGTH and all(
|
|
203
|
+
each_character.casefold() in HEX_DIGITS for each_character in revision_text
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _manifest_digest_matches(
|
|
208
|
+
all_report_fields: Mapping[str, object],
|
|
209
|
+
manifest_path: Path | None,
|
|
210
|
+
) -> bool:
|
|
211
|
+
manifest_digest = _read_text_field(all_report_fields, REPORT_MANIFEST_DIGEST_FIELD)
|
|
212
|
+
if not manifest_digest or manifest_path is None:
|
|
213
|
+
return False
|
|
214
|
+
try:
|
|
215
|
+
loaded_manifest = load_manifest(manifest_path)
|
|
216
|
+
actual_digest = compute_manifest_digest(loaded_manifest)
|
|
217
|
+
except (ManifestRunFatal, OSError, UnicodeError, ValueError):
|
|
218
|
+
return False
|
|
219
|
+
return actual_digest == manifest_digest.casefold()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _aggregate_is_complete_pass(
|
|
223
|
+
all_aggregate_fields: Mapping[str, object],
|
|
224
|
+
) -> bool:
|
|
225
|
+
total_checks = all_aggregate_fields.get(REPORT_TOTAL_FIELD)
|
|
226
|
+
passed_checks = all_aggregate_fields.get(REPORT_PASSED_FIELD)
|
|
227
|
+
failed_checks = all_aggregate_fields.get(REPORT_FAILED_FIELD)
|
|
228
|
+
incomplete_checks = all_aggregate_fields.get(REPORT_INCOMPLETE_FIELD)
|
|
229
|
+
exit_code = all_aggregate_fields.get(REPORT_EXIT_CODE_FIELD)
|
|
230
|
+
return (
|
|
231
|
+
isinstance(total_checks, int)
|
|
232
|
+
and total_checks > 0
|
|
233
|
+
and passed_checks == total_checks
|
|
234
|
+
and failed_checks == 0
|
|
235
|
+
and incomplete_checks == 0
|
|
236
|
+
and exit_code == GIT_COMMAND_SUCCESS_EXIT_CODE
|
|
237
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Start automatic local verification for a supported native Git event."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Mapping
|
|
10
|
+
|
|
11
|
+
from git_hooks_constants.verification_notice_constants import (
|
|
12
|
+
ALL_NOTICE_EVENTS,
|
|
13
|
+
ALL_RUNNER_FIELDS,
|
|
14
|
+
AUTOMATIC_ADVISORY_CLI_FILE_NAME,
|
|
15
|
+
AUTOMATIC_ADVISORY_DIRECTORY_NAME,
|
|
16
|
+
CREATE_NO_WINDOW_ATTRIBUTE,
|
|
17
|
+
EXECUTOR_DIRECTORY_NAME,
|
|
18
|
+
GIT_DIRECTORY_NAME,
|
|
19
|
+
JSON_ENCODING,
|
|
20
|
+
LOCAL_VERIFICATION_DIRECTORY_NAME,
|
|
21
|
+
RUNNER_FILE_NAME,
|
|
22
|
+
RUNNER_PYTHON_FIELD,
|
|
23
|
+
RUNNER_SETTINGS_FIELD,
|
|
24
|
+
TARGET_REPOSITORY_REMOTE,
|
|
25
|
+
WINDOWS_PLATFORM,
|
|
26
|
+
)
|
|
27
|
+
from verification_notice_context import VerificationNoticeContext
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def start_automatic_advisory(context: VerificationNoticeContext) -> None:
|
|
31
|
+
"""Start the detached advisory poller when native metadata is valid."""
|
|
32
|
+
if context.repository_remote != TARGET_REPOSITORY_REMOTE:
|
|
33
|
+
return
|
|
34
|
+
if context.event not in ALL_NOTICE_EVENTS:
|
|
35
|
+
return
|
|
36
|
+
runner_configuration = load_runner_configuration(context)
|
|
37
|
+
if runner_configuration is None:
|
|
38
|
+
return
|
|
39
|
+
python_path, settings_path = runner_configuration
|
|
40
|
+
advisory_cli_path = (
|
|
41
|
+
Path(__file__).resolve().parents[2]
|
|
42
|
+
/ EXECUTOR_DIRECTORY_NAME
|
|
43
|
+
/ AUTOMATIC_ADVISORY_DIRECTORY_NAME
|
|
44
|
+
/ AUTOMATIC_ADVISORY_CLI_FILE_NAME
|
|
45
|
+
)
|
|
46
|
+
try:
|
|
47
|
+
_launch_advisory(
|
|
48
|
+
context,
|
|
49
|
+
python_path,
|
|
50
|
+
settings_path,
|
|
51
|
+
advisory_cli_path,
|
|
52
|
+
)
|
|
53
|
+
except (OSError, subprocess.SubprocessError):
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _launch_advisory(
|
|
58
|
+
context: VerificationNoticeContext,
|
|
59
|
+
python_path: str,
|
|
60
|
+
settings_path: str,
|
|
61
|
+
advisory_cli_path: Path,
|
|
62
|
+
) -> None:
|
|
63
|
+
subprocess.Popen(
|
|
64
|
+
[python_path, str(advisory_cli_path), "--settings", settings_path, "--start"],
|
|
65
|
+
cwd=context.repository_root,
|
|
66
|
+
stdin=subprocess.DEVNULL,
|
|
67
|
+
stdout=subprocess.DEVNULL,
|
|
68
|
+
stderr=subprocess.DEVNULL,
|
|
69
|
+
creationflags=_windows_creation_flags(),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_runner_configuration(
|
|
74
|
+
context: VerificationNoticeContext,
|
|
75
|
+
) -> tuple[str, str] | None:
|
|
76
|
+
git_directory = context.git_directory or context.repository_root / GIT_DIRECTORY_NAME
|
|
77
|
+
runner_path = git_directory / LOCAL_VERIFICATION_DIRECTORY_NAME / RUNNER_FILE_NAME
|
|
78
|
+
try:
|
|
79
|
+
runner_document = json.loads(runner_path.read_text(encoding=JSON_ENCODING))
|
|
80
|
+
except (OSError, UnicodeError, ValueError):
|
|
81
|
+
return None
|
|
82
|
+
if not isinstance(runner_document, Mapping):
|
|
83
|
+
return None
|
|
84
|
+
if set(runner_document) != ALL_RUNNER_FIELDS:
|
|
85
|
+
return None
|
|
86
|
+
python_path = runner_document.get(RUNNER_PYTHON_FIELD)
|
|
87
|
+
settings_path = runner_document.get(RUNNER_SETTINGS_FIELD)
|
|
88
|
+
if not isinstance(python_path, str) or not python_path.strip():
|
|
89
|
+
return None
|
|
90
|
+
if not isinstance(settings_path, str) or not settings_path.strip():
|
|
91
|
+
return None
|
|
92
|
+
if not Path(python_path).is_absolute() or not Path(settings_path).is_absolute():
|
|
93
|
+
return None
|
|
94
|
+
return python_path, settings_path
|
|
95
|
+
|
|
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
|
@@ -2,16 +2,6 @@
|
|
|
2
2
|
"description": "Code standards enforcement, safety guards, and development workflow hooks",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
|
-
{
|
|
6
|
-
"matcher": "Write|Edit|MultiEdit|apply_patch",
|
|
7
|
-
"hooks": [
|
|
8
|
-
{
|
|
9
|
-
"type": "command",
|
|
10
|
-
"command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\" --pre-tool-use",
|
|
11
|
-
"timeout": 30
|
|
12
|
-
}
|
|
13
|
-
]
|
|
14
|
-
},
|
|
15
5
|
{
|
|
16
6
|
"matcher": "Write|Edit|MultiEdit|apply_patch",
|
|
17
7
|
"hooks": [
|
|
@@ -22,91 +12,6 @@
|
|
|
22
12
|
}
|
|
23
13
|
]
|
|
24
14
|
},
|
|
25
|
-
{
|
|
26
|
-
"matcher": "Bash|PowerShell",
|
|
27
|
-
"hooks": [
|
|
28
|
-
{
|
|
29
|
-
"type": "command",
|
|
30
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/bash_pre_tool_use_dispatcher.py",
|
|
31
|
-
"timeout": 400
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"matcher": "mcp__plugin_github_github__add_issue_comment",
|
|
37
|
-
"hooks": [
|
|
38
|
-
{
|
|
39
|
-
"type": "command",
|
|
40
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/bot_mention_comment_blocker.py",
|
|
41
|
-
"timeout": 10
|
|
42
|
-
}
|
|
43
|
-
]
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"matcher": "mcp__plugin_github_github__.*",
|
|
47
|
-
"hooks": [
|
|
48
|
-
{
|
|
49
|
-
"type": "command",
|
|
50
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pii_prevention_blocker.py",
|
|
51
|
-
"timeout": 20
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
"matcher": "Agent|Task",
|
|
57
|
-
"hooks": [
|
|
58
|
-
{
|
|
59
|
-
"type": "command",
|
|
60
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/fable_spawn_gate.py",
|
|
61
|
-
"timeout": 10
|
|
62
|
-
}
|
|
63
|
-
]
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
"matcher": "Agent|Task|multi_agent_v1__spawn_agent",
|
|
67
|
-
"hooks": [
|
|
68
|
-
{
|
|
69
|
-
"type": "command",
|
|
70
|
-
"command": "python ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/luna_fast_mode_gate.py",
|
|
71
|
-
"timeout": 10
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
"matcher": "ScheduleWakeup|CronCreate",
|
|
77
|
-
"hooks": [
|
|
78
|
-
{
|
|
79
|
-
"type": "command",
|
|
80
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/orchestrator_refresh_reschedule_gate.py",
|
|
81
|
-
"timeout": 15
|
|
82
|
-
}
|
|
83
|
-
]
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
"matcher": "AskUserQuestion",
|
|
87
|
-
"hooks": [
|
|
88
|
-
{
|
|
89
|
-
"type": "command",
|
|
90
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/plain_language_blocker.py",
|
|
91
|
-
"timeout": 10
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
"type": "command",
|
|
95
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/ask_user_question_shape_blocker.py",
|
|
96
|
-
"timeout": 10
|
|
97
|
-
}
|
|
98
|
-
]
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
"matcher": "SendUserFile",
|
|
102
|
-
"hooks": [
|
|
103
|
-
{
|
|
104
|
-
"type": "command",
|
|
105
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/send_user_file_open_locally_blocker.py",
|
|
106
|
-
"timeout": 10
|
|
107
|
-
}
|
|
108
|
-
]
|
|
109
|
-
},
|
|
110
15
|
{
|
|
111
16
|
"matcher": "EnterWorktree",
|
|
112
17
|
"hooks": [
|
|
@@ -172,18 +77,6 @@
|
|
|
172
77
|
]
|
|
173
78
|
}
|
|
174
79
|
],
|
|
175
|
-
"Stop": [
|
|
176
|
-
{
|
|
177
|
-
"matcher": "",
|
|
178
|
-
"hooks": [
|
|
179
|
-
{
|
|
180
|
-
"type": "command",
|
|
181
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/stop_dispatcher.py",
|
|
182
|
-
"timeout": 90
|
|
183
|
-
}
|
|
184
|
-
]
|
|
185
|
-
}
|
|
186
|
-
],
|
|
187
80
|
"SessionEnd": [
|
|
188
81
|
{
|
|
189
82
|
"matcher": "",
|
|
@@ -201,18 +94,6 @@
|
|
|
201
94
|
]
|
|
202
95
|
}
|
|
203
96
|
],
|
|
204
|
-
"ConfigChange": [
|
|
205
|
-
{
|
|
206
|
-
"matcher": "user_settings",
|
|
207
|
-
"hooks": [
|
|
208
|
-
{
|
|
209
|
-
"type": "command",
|
|
210
|
-
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/config_change_guard.py",
|
|
211
|
-
"timeout": 10
|
|
212
|
-
}
|
|
213
|
-
]
|
|
214
|
-
}
|
|
215
|
-
],
|
|
216
97
|
"PostToolUse": [
|
|
217
98
|
{
|
|
218
99
|
"matcher": "Write|Edit",
|
|
@@ -54,18 +54,4 @@ class BashHostedHookEntry:
|
|
|
54
54
|
applicable_tool_names: frozenset[str]
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
58
|
-
BashHostedHookEntry("blocking/destructive_command_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
59
|
-
BashHostedHookEntry("blocking/shell_substitution_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
60
|
-
BashHostedHookEntry("blocking/piped_pytest_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
61
|
-
BashHostedHookEntry(
|
|
62
|
-
"blocking/cursor_cli_python_misfire_blocker.py",
|
|
63
|
-
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
64
|
-
),
|
|
65
|
-
BashHostedHookEntry("blocking/unscoped_search_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
66
|
-
BashHostedHookEntry("blocking/nas_ssh_binary_enforcer.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
67
|
-
BashHostedHookEntry("blocking/pii_prevention_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
68
|
-
BashHostedHookEntry("blocking/block_main_commit.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
69
|
-
BashHostedHookEntry("blocking/session_edit_stage_gate.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
70
|
-
BashHostedHookEntry("blocking/windows_rmtree_blocker.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
71
|
-
)
|
|
57
|
+
ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = ()
|
|
@@ -54,10 +54,6 @@ class PostHostedHookEntry:
|
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
ALL_POST_HOSTED_HOOK_ENTRIES: tuple[PostHostedHookEntry, ...] = (
|
|
57
|
-
PostHostedHookEntry(
|
|
58
|
-
script_relative_path="validation/mypy_validator.py",
|
|
59
|
-
is_blocking=True,
|
|
60
|
-
),
|
|
61
57
|
PostHostedHookEntry(
|
|
62
58
|
script_relative_path="workflow/auto_formatter.py",
|
|
63
59
|
is_blocking=False,
|
|
@@ -26,7 +26,6 @@ __all__ = [
|
|
|
26
26
|
"ALL_WRITE_AND_EDIT_TOOL_NAMES",
|
|
27
27
|
"ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES",
|
|
28
28
|
"ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES",
|
|
29
|
-
"ALL_IMMEDIATE_HARM_SCRIPT_PATHS",
|
|
30
29
|
"STATE_DESCRIPTION_BLOCKER_MODULE_NAME",
|
|
31
30
|
"SYSTEM_MESSAGE_JOIN_SEPARATOR",
|
|
32
31
|
"HostedHookEntry",
|
|
@@ -56,11 +55,6 @@ ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES: frozenset[str] = frozenset(
|
|
|
56
55
|
ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES | {APPLY_PATCH_TOOL_NAME}
|
|
57
56
|
)
|
|
58
57
|
|
|
59
|
-
ALL_IMMEDIATE_HARM_SCRIPT_PATHS: tuple[str, ...] = (
|
|
60
|
-
"blocking/pii_prevention_blocker.py",
|
|
61
|
-
"blocking/sensitive_file_protector.py",
|
|
62
|
-
"blocking/write_existing_file_blocker.py",
|
|
63
|
-
)
|
|
64
58
|
|
|
65
59
|
|
|
66
60
|
STATE_DESCRIPTION_BLOCKER_MODULE_NAME = "state_description_blocker"
|
|
@@ -89,50 +83,6 @@ class HostedHookEntry:
|
|
|
89
83
|
|
|
90
84
|
|
|
91
85
|
ALL_HOSTED_HOOK_ENTRIES: tuple[HostedHookEntry, ...] = (
|
|
92
|
-
HostedHookEntry(
|
|
93
|
-
script_relative_path="blocking/write_existing_file_blocker.py",
|
|
94
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
|
|
95
|
-
),
|
|
96
|
-
HostedHookEntry(
|
|
97
|
-
script_relative_path="blocking/sensitive_file_protector.py",
|
|
98
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
|
|
99
|
-
),
|
|
100
|
-
HostedHookEntry(
|
|
101
|
-
script_relative_path="blocking/pii_prevention_blocker.py",
|
|
102
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_APPLY_PATCH_TOOL_NAMES,
|
|
103
|
-
),
|
|
104
|
-
HostedHookEntry(
|
|
105
|
-
script_relative_path="validation/hook_format_validator.py",
|
|
106
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
107
|
-
),
|
|
108
|
-
HostedHookEntry(
|
|
109
|
-
script_relative_path="blocking/duplicate_rmtree_helper_blocker.py",
|
|
110
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
111
|
-
),
|
|
112
|
-
HostedHookEntry(
|
|
113
|
-
script_relative_path="blocking/claude_md_orphan_file_blocker.py",
|
|
114
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
115
|
-
),
|
|
116
|
-
HostedHookEntry(
|
|
117
|
-
script_relative_path="blocking/package_inventory_stale_blocker.py",
|
|
118
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
119
|
-
),
|
|
120
|
-
HostedHookEntry(
|
|
121
|
-
script_relative_path="blocking/env_var_table_code_drift_blocker.py",
|
|
122
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
123
|
-
),
|
|
124
|
-
HostedHookEntry(
|
|
125
|
-
script_relative_path="blocking/pytest_testpaths_orphan_blocker.py",
|
|
126
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
127
|
-
),
|
|
128
|
-
HostedHookEntry(
|
|
129
|
-
script_relative_path="blocking/open_questions_in_plans_blocker.py",
|
|
130
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
131
|
-
),
|
|
132
|
-
HostedHookEntry(
|
|
133
|
-
script_relative_path="blocking/docstring_rule_gate_count_blocker.py",
|
|
134
|
-
applicable_tool_names=ALL_WRITE_EDIT_MULTI_EDIT_TOOL_NAMES,
|
|
135
|
-
),
|
|
136
86
|
HostedHookEntry(
|
|
137
87
|
script_relative_path="advisory/refactor_guard.py",
|
|
138
88
|
applicable_tool_names=frozenset({EDIT_TOOL_NAME, MULTI_EDIT_TOOL_NAME}),
|
|
@@ -17,7 +17,4 @@ BLOCK_DECISION = "block"
|
|
|
17
17
|
DECISION_KEY = "decision"
|
|
18
18
|
REASON_KEY = "reason"
|
|
19
19
|
|
|
20
|
-
ALL_STOP_HOSTED_HOOK_PATHS: tuple[str, ...] = (
|
|
21
|
-
"blocking/question_to_user_enforcer.py",
|
|
22
|
-
"blocking/session_handoff_blocker.py",
|
|
23
|
-
)
|
|
20
|
+
ALL_STOP_HOSTED_HOOK_PATHS: tuple[str, ...] = ()
|
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
"""Tests for the Bash and PowerShell dispatcher hosted-hook roster.
|
|
2
|
-
|
|
3
|
-
The roster order and per-hook applicable-tool sets fix the firing sequence and
|
|
4
|
-
tool coverage the dispatcher must reproduce, so these tests pin both against the
|
|
5
|
-
registration order the standalone chain used.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
1
|
+
"""Tests for the Bash and PowerShell dispatcher hosted-hook roster."""
|
|
9
2
|
|
|
10
3
|
from pathlib import Path
|
|
11
4
|
from runpy import run_path
|
|
@@ -13,64 +6,8 @@ from runpy import run_path
|
|
|
13
6
|
ALL_CONSTANT_BINDINGS = run_path(
|
|
14
7
|
str(Path(__file__).with_name("bash_pre_tool_use_dispatcher_constants.py"))
|
|
15
8
|
)
|
|
16
|
-
ALL_BASH_AND_POWERSHELL_TOOL_NAMES = ALL_CONSTANT_BINDINGS["ALL_BASH_AND_POWERSHELL_TOOL_NAMES"]
|
|
17
9
|
ALL_BASH_HOSTED_HOOK_ENTRIES = ALL_CONSTANT_BINDINGS["ALL_BASH_HOSTED_HOOK_ENTRIES"]
|
|
18
|
-
BASH_TOOL_NAME = ALL_CONSTANT_BINDINGS["BASH_TOOL_NAME"]
|
|
19
|
-
POWERSHELL_TOOL_NAME = ALL_CONSTANT_BINDINGS["POWERSHELL_TOOL_NAME"]
|
|
20
|
-
|
|
21
|
-
_EXPECTED_BASH_ORDER = (
|
|
22
|
-
"blocking/destructive_command_blocker.py",
|
|
23
|
-
"blocking/shell_substitution_blocker.py",
|
|
24
|
-
"blocking/piped_pytest_blocker.py",
|
|
25
|
-
"blocking/cursor_cli_python_misfire_blocker.py",
|
|
26
|
-
"blocking/unscoped_search_blocker.py",
|
|
27
|
-
"blocking/nas_ssh_binary_enforcer.py",
|
|
28
|
-
"blocking/pii_prevention_blocker.py",
|
|
29
|
-
"blocking/block_main_commit.py",
|
|
30
|
-
"blocking/session_edit_stage_gate.py",
|
|
31
|
-
"blocking/windows_rmtree_blocker.py",
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
_POWERSHELL_APPLICABLE = (
|
|
35
|
-
"blocking/cursor_cli_python_misfire_blocker.py",
|
|
36
|
-
"blocking/unscoped_search_blocker.py",
|
|
37
|
-
"blocking/pii_prevention_blocker.py",
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def test_roster_lists_all_bash_hooks_in_registration_order() -> None:
|
|
42
|
-
"""The roster reproduces the standalone Bash chain order exactly."""
|
|
43
|
-
actual_order = tuple(
|
|
44
|
-
each_entry.script_relative_path for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
45
|
-
)
|
|
46
|
-
assert actual_order == _EXPECTED_BASH_ORDER
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def test_every_hook_applies_to_the_bash_tool() -> None:
|
|
50
|
-
"""Every hosted hook runs on a Bash tool call."""
|
|
51
|
-
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
52
|
-
assert BASH_TOOL_NAME in each_entry.applicable_tool_names
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def test_powershell_applicable_hooks_include_the_shared_gates() -> None:
|
|
56
|
-
"""PowerShell runs the shared unscoped-search and PII gates."""
|
|
57
|
-
powershell_hooks = tuple(
|
|
58
|
-
each_entry.script_relative_path
|
|
59
|
-
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES
|
|
60
|
-
if POWERSHELL_TOOL_NAME in each_entry.applicable_tool_names
|
|
61
|
-
)
|
|
62
|
-
assert powershell_hooks == _POWERSHELL_APPLICABLE
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def test_powershell_hooks_carry_the_shared_tool_set() -> None:
|
|
66
|
-
"""The PowerShell-applicable hooks name both Bash and PowerShell."""
|
|
67
|
-
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
68
|
-
if each_entry.script_relative_path in _POWERSHELL_APPLICABLE:
|
|
69
|
-
assert each_entry.applicable_tool_names == ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
70
10
|
|
|
71
11
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
hooks_root = Path(__file__).resolve().parent.parent
|
|
75
|
-
for each_entry in ALL_BASH_HOSTED_HOOK_ENTRIES:
|
|
76
|
-
assert (hooks_root / each_entry.script_relative_path).is_file()
|
|
12
|
+
def test_roster_has_no_blocking_hooks() -> None:
|
|
13
|
+
assert ALL_BASH_HOSTED_HOOK_ENTRIES == ()
|