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.
Files changed (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. 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",
@@ -2,6 +2,10 @@
2
2
 
3
3
  import re
4
4
 
5
+ from hooks_constants.blast_radius_constants import ALL_BLAST_RADIUS_SUFFIXES
6
+
7
+ ALL_BLAST_RADIUS_EXCEPTION_SUFFIXES: tuple[str, ...] = ALL_BLAST_RADIUS_SUFFIXES
8
+
5
9
  ALL_BANNED_IDENTIFIERS: frozenset[str] = frozenset(
6
10
  {
7
11
  "result",
@@ -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 test_every_roster_path_points_at_an_existing_hook() -> None:
73
- """Each roster path names a hook script present on disk, catching a typo."""
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 == ()