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
@@ -1,260 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse gate: deny orchestrator-refresh re-arm when the run is not active.
3
-
4
- Fires on ``ScheduleWakeup`` and ``CronCreate`` when the prompt targets
5
- ``/orchestrator-refresh``. Uses the same status-file gate as
6
- ``skills/orchestrator/scripts/status_gate.py`` so idle loops
7
- cannot be re-armed on the Claude host without an active status file.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import importlib.util
13
- import json
14
- import os
15
- import re
16
- import sys
17
- from pathlib import Path
18
- from types import ModuleType
19
- from typing import Mapping
20
-
21
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
22
- if _hooks_dir not in sys.path:
23
- sys.path.insert(0, _hooks_dir)
24
-
25
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
26
- from hooks_constants.orchestrator_refresh_reschedule_gate_constants import ( # noqa: E402
27
- ALL_HOME_SKILLS_RELATIVE_PARTS,
28
- ALL_PACKAGE_AGENTS_SKILL_SCRIPTS_RELATIVE_PARTS,
29
- ALL_SCHEDULE_TOOL_NAMES,
30
- ALL_SKILL_SCRIPTS_RELATIVE_PARTS,
31
- CALLING_HOOK_NAME,
32
- CRON_CREATE_TOOL_NAME,
33
- CWD_FIELD_NAME,
34
- DENY_REASON_TEMPLATE,
35
- HOOK_EVENT_NAME,
36
- ORCHESTRATOR_REFRESH_PROMPT_TOKEN,
37
- PLUGIN_ROOT_ENV_VAR,
38
- PROMPT_FIELD_NAME,
39
- REASON_CRON_CREATE_FORBIDDEN,
40
- RUN_SLUG_PROMPT_FLAG,
41
- RUN_SLUG_PROMPT_VALUE_PATTERN,
42
- TOOL_INPUT_FIELD_NAME,
43
- TOOL_NAME_FIELD_NAME,
44
- )
45
-
46
-
47
- def _candidate_skill_script_directories() -> list[Path]:
48
- """Return ordered paths that may contain status_gate.py.
49
-
50
- Returns:
51
- Absolute candidate directories in preference order.
52
- """
53
- all_candidates: list[Path] = []
54
- plugin_root = os.environ.get(PLUGIN_ROOT_ENV_VAR)
55
- relative_skill_script_layouts = (
56
- ALL_PACKAGE_AGENTS_SKILL_SCRIPTS_RELATIVE_PARTS,
57
- ALL_SKILL_SCRIPTS_RELATIVE_PARTS,
58
- )
59
- if plugin_root:
60
- plugin_root_path = Path(plugin_root)
61
- for each_relative_parts in relative_skill_script_layouts:
62
- all_candidates.append(plugin_root_path.joinpath(*each_relative_parts))
63
- package_hooks_parent = Path(__file__).resolve().parents[2]
64
- for each_relative_parts in relative_skill_script_layouts:
65
- all_candidates.append(package_hooks_parent.joinpath(*each_relative_parts))
66
- all_candidates.append(Path.home().joinpath(*ALL_HOME_SKILLS_RELATIVE_PARTS))
67
- return all_candidates
68
-
69
-
70
- def _load_status_gate_module() -> ModuleType | None:
71
- """Import status_gate from the skill scripts package.
72
-
73
- Returns:
74
- The loaded module, or None when no candidate path is usable.
75
- """
76
- for each_directory in _candidate_skill_script_directories():
77
- module_path = each_directory / "status_gate.py"
78
- if not module_path.is_file():
79
- continue
80
- scripts_directory = str(each_directory)
81
- if scripts_directory not in sys.path:
82
- sys.path.insert(0, scripts_directory)
83
- spec = importlib.util.spec_from_file_location(
84
- "orchestrator_status_gate_for_reschedule_hook",
85
- module_path,
86
- )
87
- if spec is None or spec.loader is None:
88
- continue
89
- status_gate_module = importlib.util.module_from_spec(spec)
90
- sys.modules[spec.name] = status_gate_module
91
- spec.loader.exec_module(status_gate_module)
92
- return status_gate_module
93
- return None
94
-
95
-
96
- def _prompt_targets_orchestrator_refresh(
97
- all_tool_input: Mapping[str, object],
98
- ) -> bool:
99
- """Return True when the schedule prompt re-enters orchestrator-refresh.
100
-
101
- Args:
102
- all_tool_input: PreToolUse tool_input mapping.
103
-
104
- Returns:
105
- True when the prompt field contains the refresh skill token.
106
- """
107
- prompt_value = all_tool_input.get(PROMPT_FIELD_NAME, "")
108
- if not isinstance(prompt_value, str):
109
- return False
110
- stripped_prompt = prompt_value.lstrip()
111
- if not stripped_prompt.startswith(ORCHESTRATOR_REFRESH_PROMPT_TOKEN):
112
- return False
113
- if len(stripped_prompt) == len(ORCHESTRATOR_REFRESH_PROMPT_TOKEN):
114
- return True
115
- return stripped_prompt[len(ORCHESTRATOR_REFRESH_PROMPT_TOKEN)] in (" ", "\n", "\t")
116
-
117
-
118
- def _should_evaluate(all_payload_by_field: Mapping[str, object]) -> bool:
119
- """Return True for schedule tools re-arming orchestrator-refresh.
120
-
121
- Args:
122
- all_payload_by_field: Full PreToolUse payload.
123
-
124
- Returns:
125
- True when this gate must run.
126
- """
127
- tool_name = all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "")
128
- if tool_name not in ALL_SCHEDULE_TOOL_NAMES:
129
- return False
130
- tool_input = all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
131
- if not isinstance(tool_input, dict):
132
- return False
133
- return _prompt_targets_orchestrator_refresh(tool_input)
134
-
135
-
136
- def _run_slug_from_prompt(prompt_value: str) -> str:
137
- """Extract ``--run-slug`` from a ScheduleWakeup prompt when present.
138
-
139
- Args:
140
- prompt_value: The schedule tool prompt text.
141
-
142
- Returns:
143
- The slug string, or empty when the flag is absent.
144
- """
145
- run_slug_from_prompt_pattern = re.compile(
146
- re.escape(RUN_SLUG_PROMPT_FLAG) + RUN_SLUG_PROMPT_VALUE_PATTERN
147
- )
148
- matched = run_slug_from_prompt_pattern.search(prompt_value)
149
- if matched is None:
150
- return ""
151
- return matched.group(1)
152
-
153
-
154
- def _resolve_status_file_path(
155
- status_gate_module: ModuleType,
156
- all_payload_by_field: Mapping[str, object],
157
- ) -> Path:
158
- """Resolve the run status file for this schedule attempt without chdir.
159
-
160
- Args:
161
- status_gate_module: Loaded status_gate module.
162
- all_payload_by_field: PreToolUse payload (may carry cwd and prompt).
163
-
164
- Returns:
165
- Absolute status file path.
166
- """
167
- base_directory: Path | None = None
168
- working_directory_value = all_payload_by_field.get(CWD_FIELD_NAME)
169
- if isinstance(working_directory_value, str) and working_directory_value:
170
- base_directory = Path(working_directory_value)
171
- run_slug = ""
172
- tool_input = all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
173
- if isinstance(tool_input, dict):
174
- prompt_value = tool_input.get(PROMPT_FIELD_NAME, "")
175
- if isinstance(prompt_value, str):
176
- run_slug = _run_slug_from_prompt(prompt_value)
177
- return status_gate_module.resolve_status_file_path(
178
- None, base_directory, run_slug
179
- )
180
-
181
-
182
- def build_denial_response(reason_code: str) -> dict[str, dict[str, str]]:
183
- """Build the PreToolUse deny payload.
184
-
185
- Args:
186
- reason_code: Machine reason from decide_should_reschedule.
187
-
188
- Returns:
189
- Hook-specific deny JSON structure.
190
- """
191
- denial_reason = DENY_REASON_TEMPLATE.format(reason=reason_code)
192
- return {
193
- "hookSpecificOutput": {
194
- "hookEventName": HOOK_EVENT_NAME,
195
- "permissionDecision": "deny",
196
- "permissionDecisionReason": denial_reason,
197
- }
198
- }
199
-
200
-
201
- def _emit_denial(
202
- reason_code: str,
203
- all_payload_by_field: Mapping[str, object],
204
- ) -> None:
205
- """Log and print a deny decision for the current schedule attempt.
206
-
207
- Args:
208
- reason_code: Machine reason string.
209
- all_payload_by_field: PreToolUse payload for logging context.
210
- """
211
- denial = build_denial_response(reason_code)
212
- hook_specific_output = denial["hookSpecificOutput"]
213
- block_reason = hook_specific_output["permissionDecisionReason"]
214
- log_hook_block(
215
- calling_hook_name=CALLING_HOOK_NAME,
216
- hook_event=HOOK_EVENT_NAME,
217
- block_reason=block_reason,
218
- tool_name=str(all_payload_by_field.get(TOOL_NAME_FIELD_NAME, "")),
219
- offending_input_preview=str(
220
- all_payload_by_field.get(TOOL_INPUT_FIELD_NAME, {})
221
- ),
222
- )
223
- print(json.dumps(denial))
224
-
225
-
226
- def main() -> None:
227
- """Read PreToolUse stdin and deny idle orchestrator-refresh re-arms."""
228
- try:
229
- payload_by_field = json.load(sys.stdin)
230
- except json.JSONDecodeError:
231
- sys.exit(0)
232
-
233
- if not isinstance(payload_by_field, dict):
234
- sys.exit(0)
235
- if not _should_evaluate(payload_by_field):
236
- sys.exit(0)
237
-
238
- tool_name = payload_by_field.get(TOOL_NAME_FIELD_NAME, "")
239
- if tool_name == CRON_CREATE_TOOL_NAME:
240
- _emit_denial(REASON_CRON_CREATE_FORBIDDEN, payload_by_field)
241
- sys.exit(0)
242
-
243
- status_gate_module = _load_status_gate_module()
244
- if status_gate_module is None:
245
- _emit_denial("status_gate_unavailable", payload_by_field)
246
- sys.exit(0)
247
-
248
- status_file_path = _resolve_status_file_path(status_gate_module, payload_by_field)
249
- is_reschedule_allowed, reason_code = status_gate_module.decide_should_reschedule(
250
- status_file_path
251
- )
252
- if is_reschedule_allowed:
253
- sys.exit(0)
254
-
255
- _emit_denial(reason_code, payload_by_field)
256
- sys.exit(0)
257
-
258
-
259
- if __name__ == "__main__":
260
- main()
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: blocks a new production file absent from its package inventory.
3
-
4
- A package directory documents its own files in a sibling inventory document — a
5
- ``README.md`` Layout table, a ``CLAUDE.md`` "Key files" list, or a skill
6
- ``SKILL.md`` Layout table that maps the ``scripts/`` subdirectory — whose entries
7
- name each file in backticks. When a Write creates a new production code file in a
8
- directory whose inventory already names two or more sibling files but carries no
9
- entry naming the new file, the inventory and the directory disagree on the
10
- package's file set: a reader who trusts the inventory to map the directory misses
11
- the new file. This hook fires on a Write that creates such a file and blocks it,
12
- directing the author to add the inventory entry in the same change. Edits to an
13
- existing file, exempt files (``__init__.py``, ``conftest.py``, ``setup.py``,
14
- ``_path_setup.py``), test files, and files inside a directory that carries no
15
- per-file inventory (such as ``config/`` or ``tests/``) are out of scope.
16
- """
17
-
18
- import os
19
- import sys
20
- from pathlib import Path
21
-
22
- try:
23
- _hooks_root_directory = str(Path(__file__).resolve().parent.parent)
24
- _blocking_directory = str(Path(__file__).resolve().parent)
25
- for each_bootstrap_directory in (_hooks_root_directory, _blocking_directory):
26
- if each_bootstrap_directory not in sys.path:
27
- sys.path.insert(0, each_bootstrap_directory)
28
- from inventory_intent_records import records
29
- from package_inventory_stale_blocker_parts import decision, inventory_detection
30
-
31
- from hooks_constants.pre_tool_use_stdin import (
32
- read_hook_input_dictionary_from_stdin,
33
- )
34
- except ImportError as import_error:
35
- raise ImportError(
36
- "package_inventory_stale_blocker: cannot import its parts submodules; "
37
- "ensure the hooks directory is importable."
38
- ) from import_error
39
-
40
-
41
- inventory_named_basenames = inventory_detection.inventory_named_basenames
42
- is_inventoried_production_file = inventory_detection.is_inventoried_production_file
43
- find_stale_inventory = inventory_detection.find_stale_inventory
44
- survey_directory_inventories = inventory_detection.survey_directory_inventories
45
- deny_stale_inventory = decision.deny_stale_inventory
46
-
47
-
48
- def _resolve_payload(input_data: dict) -> tuple[str, dict, str]:
49
- """Return the tool name, tool input, and file path from a PreToolUse payload.
50
-
51
- Args:
52
- input_data: The parsed PreToolUse payload.
53
-
54
- Returns:
55
- The tool name, the tool input dict, and the file path — each emptied when
56
- its value is absent or the wrong type.
57
- """
58
- raw_tool_name = input_data.get("tool_name", "")
59
- tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
60
- tool_input = input_data.get("tool_input", {})
61
- if not isinstance(tool_input, dict):
62
- return tool_name, {}, ""
63
- file_path = tool_input.get("file_path", "")
64
- if not isinstance(file_path, str):
65
- return tool_name, tool_input, ""
66
- return tool_name, tool_input, file_path
67
-
68
-
69
- def _emit_stale_decision(
70
- input_data: dict, file_path: str, survey: inventory_detection._InventorySurvey
71
- ) -> None:
72
- """Allow the write when a pending row intent matches, else record and deny.
73
-
74
- Args:
75
- input_data: The parsed PreToolUse payload.
76
- file_path: The destination path of the new file.
77
- survey: The maintained-inventory survey the file is absent from.
78
- """
79
- session_id = str(input_data.get("session_id") or "")
80
- directory = str(Path(file_path).resolve().parent)
81
- filename = os.path.basename(file_path)
82
- if records.has_fresh_row_intent(session_id, directory, filename):
83
- records.consume_row_intent(session_id, directory, filename)
84
- return
85
- records.record_file_intent(session_id, directory, filename)
86
- deny_stale_inventory(file_path, survey)
87
-
88
-
89
- def main() -> None:
90
- """Read the PreToolUse payload from stdin and block a stale-inventory Write."""
91
- input_data = read_hook_input_dictionary_from_stdin()
92
- if input_data is None:
93
- sys.exit(0)
94
- tool_name, _tool_input, file_path = _resolve_payload(input_data)
95
- if tool_name != "Write" or not file_path:
96
- sys.exit(0)
97
- if os.path.exists(file_path) or not is_inventoried_production_file(file_path):
98
- sys.exit(0)
99
- survey = find_stale_inventory(file_path)
100
- if survey is None:
101
- sys.exit(0)
102
- _emit_stale_decision(input_data, file_path, survey)
103
- sys.exit(0)
104
-
105
-
106
- if __name__ == "__main__":
107
- main()