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
|
@@ -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()
|