claude-dev-env 7.0.0 → 7.1.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 +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -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 +78 -0
- package/bin/install-session-continuity.test.mjs +42 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
HOOKS_DIRECTORY = Path(__file__).resolve().parents[2] / "hooks" / "git-hooks"
|
|
11
|
+
NOTICE_SCRIPT = HOOKS_DIRECTORY / "verification_notice.py"
|
|
12
|
+
ALL_REMOTE_URLS = (
|
|
13
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
14
|
+
"git@github.com:JonEcho/python-automation.git",
|
|
15
|
+
"ssh://git@github.com/JonEcho/python-automation.git",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def run_notice(
|
|
20
|
+
repository_path: Path,
|
|
21
|
+
event_name: str = "commit",
|
|
22
|
+
) -> subprocess.CompletedProcess[str]:
|
|
23
|
+
return subprocess.run(
|
|
24
|
+
[
|
|
25
|
+
sys.executable,
|
|
26
|
+
str(NOTICE_SCRIPT),
|
|
27
|
+
"--event",
|
|
28
|
+
event_name,
|
|
29
|
+
"--repo",
|
|
30
|
+
str(repository_path),
|
|
31
|
+
],
|
|
32
|
+
check=False,
|
|
33
|
+
capture_output=True,
|
|
34
|
+
text=True,
|
|
35
|
+
encoding="utf-8",
|
|
36
|
+
env={**os.environ, "PYTHONIOENCODING": "utf-8"},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_target_repository(
|
|
41
|
+
temporary_directory: Path,
|
|
42
|
+
remote_url: str,
|
|
43
|
+
) -> tuple[Path, str]:
|
|
44
|
+
repository_path = temporary_directory / "Jon Echo project with spaces Ω"
|
|
45
|
+
repository_path.mkdir(parents=True)
|
|
46
|
+
run_git(repository_path, "init", "--quiet")
|
|
47
|
+
run_git(repository_path, "config", "user.email", "test@example.invalid")
|
|
48
|
+
run_git(repository_path, "config", "user.name", "Verification Test")
|
|
49
|
+
(repository_path / "README.md").write_text("check\n", encoding="utf-8")
|
|
50
|
+
run_git(repository_path, "add", "README.md")
|
|
51
|
+
run_git(repository_path, "commit", "--quiet", "-m", "initial")
|
|
52
|
+
run_git(repository_path, "remote", "add", "origin", remote_url)
|
|
53
|
+
current_head = run_git(repository_path, "rev-parse", "HEAD").stdout.strip()
|
|
54
|
+
return repository_path, current_head
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_git(
|
|
58
|
+
repository_path: Path,
|
|
59
|
+
*arguments: str,
|
|
60
|
+
) -> subprocess.CompletedProcess[str]:
|
|
61
|
+
return subprocess.run(
|
|
62
|
+
["git", *arguments],
|
|
63
|
+
cwd=repository_path,
|
|
64
|
+
check=True,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
encoding="utf-8",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def write_report(
|
|
72
|
+
repository_path: Path,
|
|
73
|
+
report_fields: dict[str, object],
|
|
74
|
+
) -> None:
|
|
75
|
+
manifest_path = repository_path / "config" / "local-verification.json"
|
|
76
|
+
manifest_path.parent.mkdir(exist_ok=True)
|
|
77
|
+
manifest_mapping = {
|
|
78
|
+
"version": 1,
|
|
79
|
+
"checks": [
|
|
80
|
+
{
|
|
81
|
+
"id": "smoke",
|
|
82
|
+
"argv": ["python", "-c", "pass"],
|
|
83
|
+
"cwd": ".",
|
|
84
|
+
"timeout_seconds": 1.0,
|
|
85
|
+
"minimum_tests": None,
|
|
86
|
+
}
|
|
87
|
+
],
|
|
88
|
+
"exclusions": [],
|
|
89
|
+
}
|
|
90
|
+
manifest_path.write_text(json.dumps(manifest_mapping), encoding="utf-8")
|
|
91
|
+
report_path = repository_path / ".git" / "local-verification" / "report.json"
|
|
92
|
+
report_path.parent.mkdir(exist_ok=True)
|
|
93
|
+
all_report_fields = {
|
|
94
|
+
"selection": {"selected_manifest_path": "config/local-verification.json"},
|
|
95
|
+
"manifest_digest": hashlib.sha256(
|
|
96
|
+
json.dumps(manifest_mapping, sort_keys=True, separators=(",", ":")).encode()
|
|
97
|
+
).hexdigest(),
|
|
98
|
+
**report_fields,
|
|
99
|
+
}
|
|
100
|
+
report_path.write_text(json.dumps(all_report_fields), encoding="utf-8")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def write_passing_report(repository_path: Path) -> str:
|
|
104
|
+
write_report(repository_path, {})
|
|
105
|
+
run_git(repository_path, "add", "config/local-verification.json")
|
|
106
|
+
run_git(repository_path, "commit", "--quiet", "-m", "manifest")
|
|
107
|
+
current_head = run_git(repository_path, "rev-parse", "HEAD").stdout.strip()
|
|
108
|
+
run_git(repository_path, "update-ref", "refs/remotes/origin/main", current_head)
|
|
109
|
+
write_report(
|
|
110
|
+
repository_path,
|
|
111
|
+
{
|
|
112
|
+
"head": current_head,
|
|
113
|
+
"base": current_head,
|
|
114
|
+
"worktree_clean": True,
|
|
115
|
+
"inputs_unchanged": True,
|
|
116
|
+
"publishable": True,
|
|
117
|
+
"status": "passed",
|
|
118
|
+
"aggregate": {
|
|
119
|
+
"status": "passed",
|
|
120
|
+
"total": 1,
|
|
121
|
+
"passed": 1,
|
|
122
|
+
"failed": 0,
|
|
123
|
+
"incomplete": 0,
|
|
124
|
+
"exit_code": 0,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
return current_head
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def write_worktree_passing_report(
|
|
132
|
+
repository_path: Path,
|
|
133
|
+
linked_git_directory: Path,
|
|
134
|
+
) -> str:
|
|
135
|
+
current_head = run_git(repository_path, "rev-parse", "HEAD").stdout.strip()
|
|
136
|
+
source_manifest_path = repository_path / "config" / "local-verification.json"
|
|
137
|
+
source_report_path = repository_path / ".git" / "local-verification" / "report.json"
|
|
138
|
+
metadata_directory = linked_git_directory / "local-verification"
|
|
139
|
+
metadata_directory.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
metadata_manifest_path = metadata_directory / "local-verification.json"
|
|
141
|
+
metadata_manifest_path.write_bytes(source_manifest_path.read_bytes())
|
|
142
|
+
all_report_fields = json.loads(source_report_path.read_text(encoding="utf-8"))
|
|
143
|
+
all_report_fields["selection"]["selected_manifest_path"] = str(
|
|
144
|
+
metadata_manifest_path
|
|
145
|
+
)
|
|
146
|
+
metadata_report_path = metadata_directory / "report.json"
|
|
147
|
+
metadata_report_path.write_text(json.dumps(all_report_fields), encoding="utf-8")
|
|
148
|
+
return current_head
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_notice_matches_remote_formats_and_quotes_paths(tmp_path: Path) -> None:
|
|
152
|
+
for each_index, each_remote_url in enumerate(ALL_REMOTE_URLS):
|
|
153
|
+
repository_path, current_head = create_target_repository(
|
|
154
|
+
tmp_path / str(each_index), each_remote_url
|
|
155
|
+
)
|
|
156
|
+
completed_notice = run_notice(repository_path, "push")
|
|
157
|
+
assert completed_notice.returncode == 0
|
|
158
|
+
assert "LOCAL VERIFICATION ADVISORY" in completed_notice.stdout
|
|
159
|
+
assert f"Current SHA: {current_head}" in completed_notice.stdout
|
|
160
|
+
assert "State: pending" in completed_notice.stdout
|
|
161
|
+
assert "python '" in completed_notice.stdout
|
|
162
|
+
wrapper_suffix = str(Path(".github") / "ci" / "local_verify.py")
|
|
163
|
+
assert f"{wrapper_suffix}' --base origin/main" in completed_notice.stdout
|
|
164
|
+
assert "--executor '" in completed_notice.stdout
|
|
165
|
+
assert "--output '" in completed_notice.stdout
|
|
166
|
+
assert repository_path.name in completed_notice.stdout
|
|
167
|
+
assert "--base origin/main" in completed_notice.stdout
|
|
168
|
+
assert "does not block commit or push" in completed_notice.stdout
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_notice_reports_pending_when_manifest_exists_without_report(
|
|
172
|
+
tmp_path: Path,
|
|
173
|
+
) -> None:
|
|
174
|
+
repository_path, _ = create_target_repository(
|
|
175
|
+
tmp_path,
|
|
176
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
177
|
+
)
|
|
178
|
+
write_report(repository_path, {})
|
|
179
|
+
(repository_path / ".git" / "local-verification" / "report.json").unlink()
|
|
180
|
+
|
|
181
|
+
completed_notice = run_notice(repository_path)
|
|
182
|
+
|
|
183
|
+
assert completed_notice.returncode == 0
|
|
184
|
+
assert "State: pending" in completed_notice.stdout
|
|
185
|
+
assert "manifest is missing or unreadable" not in completed_notice.stdout
|
|
186
|
+
assert "No complete current pass is recorded" in completed_notice.stdout
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_notice_ignores_unrelated_remote(tmp_path: Path) -> None:
|
|
190
|
+
repository_path, _ = create_target_repository(
|
|
191
|
+
tmp_path,
|
|
192
|
+
"https://github.com/other-owner/other-repository.git",
|
|
193
|
+
)
|
|
194
|
+
completed_notice = run_notice(repository_path)
|
|
195
|
+
assert completed_notice.returncode == 0
|
|
196
|
+
assert completed_notice.stdout == ""
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def test_notice_reports_failed_checks_without_pass_label(tmp_path: Path) -> None:
|
|
200
|
+
repository_path, current_head = create_target_repository(
|
|
201
|
+
tmp_path,
|
|
202
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
203
|
+
)
|
|
204
|
+
write_report(repository_path, {"head": current_head, "status": "failed"})
|
|
205
|
+
completed_notice = run_notice(repository_path)
|
|
206
|
+
assert completed_notice.returncode == 0
|
|
207
|
+
assert f"Current SHA: {current_head}" in completed_notice.stdout
|
|
208
|
+
assert "State: failed" in completed_notice.stdout
|
|
209
|
+
assert "recorded local checks failed" in completed_notice.stdout
|
|
210
|
+
assert "local-checks:passed" not in completed_notice.stdout
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_notice_reports_pass_only_for_complete_current_revision(tmp_path: Path) -> None:
|
|
214
|
+
repository_path, _ = create_target_repository(
|
|
215
|
+
tmp_path,
|
|
216
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
217
|
+
)
|
|
218
|
+
current_head = write_passing_report(repository_path)
|
|
219
|
+
completed_notice = run_notice(repository_path)
|
|
220
|
+
assert completed_notice.returncode == 0
|
|
221
|
+
assert f"Verified SHA: {current_head}" in completed_notice.stdout
|
|
222
|
+
assert "State: passed" in completed_notice.stdout
|
|
223
|
+
assert (
|
|
224
|
+
"local-checks:passed is valid only for this exact current SHA"
|
|
225
|
+
in completed_notice.stdout
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_notice_handles_malformed_report_and_missing_repository(tmp_path: Path) -> None:
|
|
230
|
+
repository_path, _ = create_target_repository(
|
|
231
|
+
tmp_path,
|
|
232
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
233
|
+
)
|
|
234
|
+
manifest_path = repository_path / "config" / "local-verification.json"
|
|
235
|
+
manifest_path.parent.mkdir(exist_ok=True)
|
|
236
|
+
manifest_mapping = {
|
|
237
|
+
"version": 1,
|
|
238
|
+
"checks": [
|
|
239
|
+
{
|
|
240
|
+
"id": "smoke",
|
|
241
|
+
"argv": ["python", "-c", "pass"],
|
|
242
|
+
"cwd": ".",
|
|
243
|
+
"timeout_seconds": 1.0,
|
|
244
|
+
"minimum_tests": None,
|
|
245
|
+
}
|
|
246
|
+
],
|
|
247
|
+
"exclusions": [],
|
|
248
|
+
}
|
|
249
|
+
manifest_path.write_text(json.dumps(manifest_mapping), encoding="utf-8")
|
|
250
|
+
report_path = repository_path / ".git" / "local-verification" / "report.json"
|
|
251
|
+
report_path.parent.mkdir(exist_ok=True)
|
|
252
|
+
report_path.write_bytes(b"not-json\xff")
|
|
253
|
+
malformed_notice = run_notice(repository_path)
|
|
254
|
+
missing_notice = run_notice(tmp_path / "missing repository")
|
|
255
|
+
assert malformed_notice.returncode == 0
|
|
256
|
+
assert "State: unverified" in malformed_notice.stdout
|
|
257
|
+
assert missing_notice.returncode == 0
|
|
258
|
+
assert missing_notice.stdout == ""
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_notice_uses_worktree_specific_report_path(tmp_path: Path) -> None:
|
|
262
|
+
repository_path, _ = create_target_repository(
|
|
263
|
+
tmp_path / "main",
|
|
264
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
265
|
+
)
|
|
266
|
+
linked_worktree_path = tmp_path / "linked worktree Ω"
|
|
267
|
+
run_git(repository_path, "worktree", "add", "--detach", str(linked_worktree_path))
|
|
268
|
+
linked_git_directory = Path(
|
|
269
|
+
run_git(linked_worktree_path, "rev-parse", "--absolute-git-dir").stdout.strip()
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
completed_notice = run_notice(linked_worktree_path)
|
|
273
|
+
|
|
274
|
+
expected_report_path = linked_git_directory / "local-verification" / "report.json"
|
|
275
|
+
shared_report_path = repository_path / ".git" / "local-verification" / "report.json"
|
|
276
|
+
assert completed_notice.returncode == 0
|
|
277
|
+
assert str(expected_report_path) in completed_notice.stdout
|
|
278
|
+
assert str(shared_report_path) not in completed_notice.stdout
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_notice_accepts_complete_report_in_worktree_metadata(tmp_path: Path) -> None:
|
|
282
|
+
repository_path, _ = create_target_repository(
|
|
283
|
+
tmp_path / "main",
|
|
284
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
285
|
+
)
|
|
286
|
+
current_head = write_passing_report(repository_path)
|
|
287
|
+
linked_worktree_path = tmp_path / "linked worktree Ω"
|
|
288
|
+
run_git(repository_path, "worktree", "add", "--detach", str(linked_worktree_path))
|
|
289
|
+
linked_git_directory = Path(
|
|
290
|
+
run_git(linked_worktree_path, "rev-parse", "--absolute-git-dir").stdout.strip()
|
|
291
|
+
)
|
|
292
|
+
write_worktree_passing_report(repository_path, linked_git_directory)
|
|
293
|
+
|
|
294
|
+
completed_notice = run_notice(linked_worktree_path)
|
|
295
|
+
|
|
296
|
+
future_path = linked_worktree_path / "future.txt"
|
|
297
|
+
future_path.write_text("future\n", encoding="utf-8")
|
|
298
|
+
run_git(linked_worktree_path, "add", "future.txt")
|
|
299
|
+
run_git(linked_worktree_path, "commit", "--quiet", "-m", "future")
|
|
300
|
+
future_head = run_git(linked_worktree_path, "rev-parse", "HEAD").stdout.strip()
|
|
301
|
+
stale_notice = run_notice(linked_worktree_path)
|
|
302
|
+
run_git(linked_worktree_path, "reset", "--quiet", "--hard", current_head)
|
|
303
|
+
run_git(linked_worktree_path, "update-ref", "refs/remotes/origin/main", future_head)
|
|
304
|
+
moved_base_notice = run_notice(linked_worktree_path)
|
|
305
|
+
|
|
306
|
+
assert "State: passed" in completed_notice.stdout
|
|
307
|
+
assert f"Verified SHA: {current_head}" in completed_notice.stdout
|
|
308
|
+
assert "State: stale" in stale_notice.stdout
|
|
309
|
+
assert "State: unverified" in moved_base_notice.stdout
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_notice_invalidates_pass_after_worktree_edit(tmp_path: Path) -> None:
|
|
313
|
+
repository_path, _ = create_target_repository(
|
|
314
|
+
tmp_path,
|
|
315
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
316
|
+
)
|
|
317
|
+
current_head = write_passing_report(repository_path)
|
|
318
|
+
passed_notice = run_notice(repository_path)
|
|
319
|
+
(repository_path / "untracked.txt").write_text("changed\n", encoding="utf-8")
|
|
320
|
+
|
|
321
|
+
changed_notice = run_notice(repository_path)
|
|
322
|
+
|
|
323
|
+
assert f"Verified SHA: {current_head}" in passed_notice.stdout
|
|
324
|
+
assert "State: passed" in passed_notice.stdout
|
|
325
|
+
assert "Verified SHA: none" in changed_notice.stdout
|
|
326
|
+
assert "State: unverified" in changed_notice.stdout
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_notice_invalidates_pass_after_origin_base_moves(tmp_path: Path) -> None:
|
|
330
|
+
repository_path, _ = create_target_repository(
|
|
331
|
+
tmp_path,
|
|
332
|
+
"https://github.com/JonEcho/python-automation.git",
|
|
333
|
+
)
|
|
334
|
+
current_head = write_passing_report(repository_path)
|
|
335
|
+
passed_notice = run_notice(repository_path)
|
|
336
|
+
(repository_path / "future.txt").write_text("future\n", encoding="utf-8")
|
|
337
|
+
run_git(repository_path, "add", "future.txt")
|
|
338
|
+
run_git(repository_path, "commit", "--quiet", "-m", "future")
|
|
339
|
+
future_head = run_git(repository_path, "rev-parse", "HEAD").stdout.strip()
|
|
340
|
+
run_git(repository_path, "reset", "--quiet", "--hard", current_head)
|
|
341
|
+
run_git(repository_path, "update-ref", "refs/remotes/origin/main", future_head)
|
|
342
|
+
|
|
343
|
+
moved_base_notice = run_notice(repository_path)
|
|
344
|
+
|
|
345
|
+
assert "State: passed" in passed_notice.stdout
|
|
346
|
+
assert "Verified SHA: none" in moved_base_notice.stdout
|
|
347
|
+
assert "State: unverified" in moved_base_notice.stdout
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook that enforces AskUserQuestion field shape."""
|
|
3
|
-
|
|
4
|
-
from __future__ import annotations
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
import sys
|
|
8
|
-
from collections.abc import Mapping, Sequence
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
12
|
-
if _hooks_dir not in sys.path:
|
|
13
|
-
sys.path.insert(0, _hooks_dir)
|
|
14
|
-
|
|
15
|
-
from hooks_constants.ask_user_question_shape import ( # noqa: E402
|
|
16
|
-
analyze_ask_user_question_shape,
|
|
17
|
-
)
|
|
18
|
-
from hooks_constants.ask_user_question_shape_constants import ( # noqa: E402
|
|
19
|
-
ASK_USER_QUESTION_TOOL_NAME,
|
|
20
|
-
LEAN_QUESTION_BLOCK_GUIDANCE,
|
|
21
|
-
LEAN_QUESTION_BLOCK_PREFIX,
|
|
22
|
-
LEAN_QUESTION_VIOLATION_SEPARATOR,
|
|
23
|
-
USER_FACING_LEAN_QUESTION_NOTICE,
|
|
24
|
-
)
|
|
25
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def build_lean_block_reason(all_violations: Sequence[str]) -> str:
|
|
29
|
-
"""Build a deny reason that names each field-shape violation."""
|
|
30
|
-
violation_list = LEAN_QUESTION_VIOLATION_SEPARATOR.join(all_violations)
|
|
31
|
-
return f"{LEAN_QUESTION_BLOCK_PREFIX}{violation_list}. {LEAN_QUESTION_BLOCK_GUIDANCE}"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def evaluate(payload_by_key: Mapping[str, object]) -> str | None:
|
|
35
|
-
"""Return a deny reason when an AskUserQuestion payload breaks field shape."""
|
|
36
|
-
tool_name = payload_by_key.get("tool_name", "")
|
|
37
|
-
tool_input = payload_by_key.get("tool_input", {})
|
|
38
|
-
if tool_name != ASK_USER_QUESTION_TOOL_NAME:
|
|
39
|
-
return None
|
|
40
|
-
if not isinstance(tool_input, Mapping):
|
|
41
|
-
return None
|
|
42
|
-
shape_result = analyze_ask_user_question_shape(tool_input)
|
|
43
|
-
if shape_result.is_lean:
|
|
44
|
-
return None
|
|
45
|
-
return build_lean_block_reason(shape_result.all_violations)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def build_deny_payload(deny_reason: str) -> dict[str, object]:
|
|
49
|
-
"""Build the standard PreToolUse deny payload for a shape violation."""
|
|
50
|
-
log_hook_block(
|
|
51
|
-
calling_hook_name="ask_user_question_shape_blocker.py",
|
|
52
|
-
hook_event="PreToolUse",
|
|
53
|
-
block_reason=deny_reason,
|
|
54
|
-
)
|
|
55
|
-
return {
|
|
56
|
-
"hookSpecificOutput": {
|
|
57
|
-
"hookEventName": "PreToolUse",
|
|
58
|
-
"permissionDecision": "deny",
|
|
59
|
-
"permissionDecisionReason": deny_reason,
|
|
60
|
-
},
|
|
61
|
-
"systemMessage": USER_FACING_LEAN_QUESTION_NOTICE,
|
|
62
|
-
"suppressOutput": True,
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def main() -> None:
|
|
67
|
-
"""Read one PreToolUse payload and deny malformed question shape."""
|
|
68
|
-
try:
|
|
69
|
-
payload_by_key = json.load(sys.stdin)
|
|
70
|
-
except json.JSONDecodeError:
|
|
71
|
-
sys.exit(0)
|
|
72
|
-
|
|
73
|
-
if not isinstance(payload_by_key, dict):
|
|
74
|
-
sys.exit(0)
|
|
75
|
-
deny_reason = evaluate(payload_by_key)
|
|
76
|
-
if deny_reason is None:
|
|
77
|
-
sys.exit(0)
|
|
78
|
-
sys.stdout.write(json.dumps(build_deny_payload(deny_reason)))
|
|
79
|
-
sys.stdout.flush()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if __name__ == "__main__":
|
|
83
|
-
main()
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: block @cursor or @copilot mentions in add_issue_comment body.
|
|
3
|
-
|
|
4
|
-
Bugbot trigger requires exactly ``bugbot run`` with no other text. Copilot review
|
|
5
|
-
requires the GitHub REST API endpoint, not an issue comment mention. This hook
|
|
6
|
-
catches both mistakes and returns the correct procedure for each.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
14
|
-
if _hooks_dir not in sys.path:
|
|
15
|
-
sys.path.insert(0, _hooks_dir)
|
|
16
|
-
|
|
17
|
-
from hooks_constants.bot_mention_comment_blocker_constants import ( # noqa: E402
|
|
18
|
-
COPILOT_MENTION_TOKEN,
|
|
19
|
-
CORRECTIVE_MESSAGE_COPILOT,
|
|
20
|
-
CORRECTIVE_MESSAGE_CURSOR,
|
|
21
|
-
CURSOR_MENTION_TOKEN,
|
|
22
|
-
TOOL_NAME,
|
|
23
|
-
)
|
|
24
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _body_contains_token(body: str, token: str) -> bool:
|
|
28
|
-
return token.lower() in body.lower()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def _detect_bot_mention(body: str) -> str | None:
|
|
32
|
-
"""Return corrective message if body contains a blocked mention, else None."""
|
|
33
|
-
if _body_contains_token(body, COPILOT_MENTION_TOKEN):
|
|
34
|
-
return CORRECTIVE_MESSAGE_COPILOT
|
|
35
|
-
if _body_contains_token(body, CURSOR_MENTION_TOKEN):
|
|
36
|
-
return CORRECTIVE_MESSAGE_CURSOR
|
|
37
|
-
return None
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def main() -> None:
|
|
41
|
-
try:
|
|
42
|
-
hook_input = json.load(sys.stdin)
|
|
43
|
-
except json.JSONDecodeError:
|
|
44
|
-
sys.exit(0)
|
|
45
|
-
|
|
46
|
-
if hook_input.get("tool_name", "") != TOOL_NAME:
|
|
47
|
-
sys.exit(0)
|
|
48
|
-
|
|
49
|
-
body = hook_input.get("tool_input", {}).get("body", "")
|
|
50
|
-
if not body:
|
|
51
|
-
sys.exit(0)
|
|
52
|
-
|
|
53
|
-
corrective_message = _detect_bot_mention(body)
|
|
54
|
-
if corrective_message is None:
|
|
55
|
-
sys.exit(0)
|
|
56
|
-
|
|
57
|
-
deny_payload = {
|
|
58
|
-
"hookSpecificOutput": {
|
|
59
|
-
"hookEventName": "PreToolUse",
|
|
60
|
-
"permissionDecision": "deny",
|
|
61
|
-
"permissionDecisionReason": corrective_message,
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
log_hook_block(
|
|
65
|
-
calling_hook_name="bot_mention_comment_blocker.py",
|
|
66
|
-
hook_event="PreToolUse",
|
|
67
|
-
block_reason=corrective_message,
|
|
68
|
-
tool_name=TOOL_NAME,
|
|
69
|
-
)
|
|
70
|
-
print(json.dumps(deny_payload))
|
|
71
|
-
sys.stdout.flush()
|
|
72
|
-
sys.exit(0)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if __name__ == "__main__":
|
|
76
|
-
main()
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: blocks a per-directory CLAUDE.md that names a file absent from its subtree.
|
|
3
|
-
|
|
4
|
-
A per-directory ``CLAUDE.md`` documents the files reachable from its own
|
|
5
|
-
directory in a markdown table whose first column names each file in backticks,
|
|
6
|
-
and shows run commands inside fenced code blocks that invoke those files. When a
|
|
7
|
-
first-column cell, or an interpreter invocation inside a fenced run command
|
|
8
|
-
(``python script.py``), names a bare filename that exists nowhere under the scan
|
|
9
|
-
root (the CLAUDE.md directory's parent, which covers the directory, its
|
|
10
|
-
subdirectories, and its siblings), the doc points a reader at a file that is not
|
|
11
|
-
there. This hook fires on Write, Edit, and MultiEdit targeting a file named
|
|
12
|
-
``CLAUDE.md`` and blocks the write when any such cell or run command names a file
|
|
13
|
-
absent from the scan root. A table block or a run-command fence whose own region
|
|
14
|
-
declares an explicit relative-path source (a ``../`` token, in the rows/fence or
|
|
15
|
-
the prose that introduces it) documents files outside the subtree, so that block's
|
|
16
|
-
rows or that fence's commands are left alone — the exemption is scoped to the
|
|
17
|
-
region, not the whole file.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
import sys
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
_hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
25
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
26
|
-
for each_bootstrap_directory in (_hooks_root_directory, _blocking_directory):
|
|
27
|
-
if each_bootstrap_directory not in sys.path:
|
|
28
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
29
|
-
from claude_md_orphan_file_blocker_parts import (
|
|
30
|
-
decision,
|
|
31
|
-
references,
|
|
32
|
-
scan_plan,
|
|
33
|
-
subtree_scan,
|
|
34
|
-
)
|
|
35
|
-
from claude_md_orphan_file_blocker_parts.subtree_scan import (
|
|
36
|
-
MAX_SUBTREE_FILES_SCANNED, # noqa: F401
|
|
37
|
-
)
|
|
38
|
-
from inventory_intent_records import records
|
|
39
|
-
|
|
40
|
-
from hooks_constants.pre_tool_use_stdin import (
|
|
41
|
-
read_hook_input_dictionary_from_stdin,
|
|
42
|
-
)
|
|
43
|
-
except ImportError as import_error:
|
|
44
|
-
raise ImportError(
|
|
45
|
-
"claude_md_orphan_file_blocker: cannot import its parts submodules; "
|
|
46
|
-
"ensure the hooks directory is importable."
|
|
47
|
-
) from import_error
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
is_claude_md_file = references.is_claude_md_file
|
|
51
|
-
find_referenced_filenames = references.find_referenced_filenames
|
|
52
|
-
find_run_command_filenames = references.find_run_command_filenames
|
|
53
|
-
find_missing_filenames = subtree_scan.find_missing_filenames
|
|
54
|
-
build_orphan_scan_plan = scan_plan.build_orphan_scan_plan
|
|
55
|
-
collect_missing_filenames = scan_plan.collect_missing_filenames
|
|
56
|
-
deny_orphan_files = decision.deny_orphan_files
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def _partition_by_file_intent(
|
|
60
|
-
session_id: str, directory: str, all_missing_filenames: list[str]
|
|
61
|
-
) -> tuple[list[str], list[str]]:
|
|
62
|
-
"""Split missing filenames into those with a pending file intent and those without."""
|
|
63
|
-
matched_filenames = [
|
|
64
|
-
each_filename
|
|
65
|
-
for each_filename in all_missing_filenames
|
|
66
|
-
if records.has_fresh_file_intent(session_id, directory, each_filename)
|
|
67
|
-
]
|
|
68
|
-
unmatched_filenames = [
|
|
69
|
-
each_filename
|
|
70
|
-
for each_filename in all_missing_filenames
|
|
71
|
-
if each_filename not in matched_filenames
|
|
72
|
-
]
|
|
73
|
-
return matched_filenames, unmatched_filenames
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _emit_orphan_decision(
|
|
77
|
-
input_data: dict,
|
|
78
|
-
tool_name: str,
|
|
79
|
-
file_path: str,
|
|
80
|
-
directory: str,
|
|
81
|
-
all_missing_filenames: list[str],
|
|
82
|
-
) -> None:
|
|
83
|
-
"""Consume covered rows and deny any genuine orphan the change introduces."""
|
|
84
|
-
session_id = str(input_data.get("session_id") or "")
|
|
85
|
-
matched_filenames, unmatched_filenames = _partition_by_file_intent(
|
|
86
|
-
session_id, directory, all_missing_filenames
|
|
87
|
-
)
|
|
88
|
-
if unmatched_filenames:
|
|
89
|
-
for each_filename in unmatched_filenames:
|
|
90
|
-
records.record_row_intent(session_id, directory, each_filename)
|
|
91
|
-
deny_orphan_files(tool_name, file_path, directory, unmatched_filenames)
|
|
92
|
-
return
|
|
93
|
-
for each_filename in matched_filenames:
|
|
94
|
-
records.consume_file_intent(session_id, directory, each_filename)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def main() -> None:
|
|
98
|
-
"""Read the PreToolUse payload from stdin and block an orphan-file CLAUDE.md."""
|
|
99
|
-
input_data = read_hook_input_dictionary_from_stdin()
|
|
100
|
-
if input_data is None:
|
|
101
|
-
sys.exit(0)
|
|
102
|
-
tool_name = input_data.get("tool_name", "")
|
|
103
|
-
if not isinstance(tool_name, str):
|
|
104
|
-
sys.exit(0)
|
|
105
|
-
tool_input = input_data.get("tool_input", {})
|
|
106
|
-
if not isinstance(tool_input, dict):
|
|
107
|
-
sys.exit(0)
|
|
108
|
-
if tool_name not in ("Write", "Edit", "MultiEdit"):
|
|
109
|
-
sys.exit(0)
|
|
110
|
-
file_path = tool_input.get("file_path", "")
|
|
111
|
-
if not isinstance(file_path, str) or not is_claude_md_file(file_path):
|
|
112
|
-
sys.exit(0)
|
|
113
|
-
claude_md_directory = Path(file_path).resolve().parent
|
|
114
|
-
if not claude_md_directory.is_dir():
|
|
115
|
-
sys.exit(0)
|
|
116
|
-
scan_plan_result = build_orphan_scan_plan(tool_name, tool_input, file_path, claude_md_directory)
|
|
117
|
-
if not scan_plan_result.candidate_contents:
|
|
118
|
-
sys.exit(0)
|
|
119
|
-
missing_filenames = collect_missing_filenames(scan_plan_result, claude_md_directory)
|
|
120
|
-
if not missing_filenames:
|
|
121
|
-
sys.exit(0)
|
|
122
|
-
_emit_orphan_decision(
|
|
123
|
-
input_data, tool_name, file_path, str(claude_md_directory), missing_filenames
|
|
124
|
-
)
|
|
125
|
-
sys.exit(0)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if __name__ == "__main__":
|
|
129
|
-
main()
|