claude-dev-env 1.42.0 → 1.44.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/_shared/pr-loop/scripts/_claude_permissions_common.py +1 -5
- package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
- package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +3 -16
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_permissions_constants.py +1 -1
- package/_shared/pr-loop/scripts/preflight.py +13 -31
- package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +3 -16
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +4 -4
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +2 -2
- package/agents/pr-description-writer.md +50 -140
- package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
- package/hooks/_gh_pr_author_swap_utils.py +1 -1
- package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
- package/hooks/blocking/code_rules_enforcer.py +217 -99
- package/hooks/blocking/code_rules_path_utils.py +8 -1
- package/hooks/blocking/destructive_command_blocker.py +1 -1
- package/hooks/blocking/es_exe_path_rewriter.py +7 -13
- package/hooks/blocking/gh_body_arg_blocker.py +6 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
- package/hooks/blocking/gh_pr_author_restore.py +5 -5
- package/hooks/blocking/hedging_language_blocker.py +4 -10
- package/hooks/blocking/md_path_exemptions.py +205 -0
- package/hooks/blocking/md_to_html_blocker.py +48 -20
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
- package/hooks/blocking/pr_description_enforcer.py +626 -41
- package/hooks/blocking/question_to_user_enforcer.py +4 -10
- package/hooks/blocking/state_description_blocker.py +6 -12
- package/hooks/blocking/tdd_enforcer.py +1 -1
- package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +3 -3
- package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
- package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
- package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
- package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
- package/hooks/blocking/test_hedging_language_blocker.py +2 -2
- package/hooks/blocking/test_md_to_html_blocker.py +463 -8
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
- package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
- package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
- package/hooks/blocking/windows_rmtree_blocker.py +5 -11
- package/hooks/diagnostic/hook_log_extractor.py +1 -1
- package/hooks/diagnostic/hook_log_init.py +1 -1
- package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
- package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
- package/hooks/diagnostic/test_hook_log_init.py +2 -2
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
- package/hooks/git-hooks/gate_utils.py +1 -1
- package/hooks/git-hooks/pre_commit.py +1 -1
- package/hooks/git-hooks/pre_push.py +1 -1
- package/hooks/git-hooks/test_config.py +5 -5
- package/hooks/git-hooks/test_pre_push.py +6 -6
- package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
- package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
- package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
- package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
- package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
- package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
- package/hooks/{config → hooks_constants}/test_messages.py +2 -6
- package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
- package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
- package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
- package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
- package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
- package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
- package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
- package/hooks/session/session_env_cleanup.py +4 -10
- package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
- package/hooks/session/test_untracked_repo_detector.py +2 -2
- package/hooks/session/untracked_repo_detector.py +6 -12
- package/hooks/test__gh_pr_author_swap_utils.py +1 -1
- package/hooks/validators/run_all_validators.py +16 -5
- package/hooks/validators/test_output_formatter.py +46 -0
- package/hooks/workflow/doc_gist_auto_publish.py +1 -1
- package/hooks/workflow/md_to_html_companion.py +8 -15
- package/hooks/workflow/test_md_to_html_companion.py +184 -23
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +1 -1
- package/rules/vault-context.md +1 -1
- package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
- package/scripts/setup_project_paths.py +49 -11
- package/scripts/sweep_empty_dirs.py +10 -1
- package/scripts/test_setup_project_paths.py +2 -2
- package/scripts/test_sweep_empty_dirs.py +2 -6
- package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
- package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
- package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
- package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
- package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
- package/skills/bugteam/PROMPTS.md +1 -1
- package/skills/bugteam/SKILL.md +1 -1
- package/skills/bugteam/reference/github-pr-reviews.md +1 -1
- package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +1 -13
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
- package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +2 -8
- package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
- package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +2 -8
- package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
- package/skills/bugteam/scripts/test_agent_config_carveout.py +2 -2
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
- package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
- package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
- package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
- package/skills/doc-gist/SKILL.md +1 -1
- package/skills/doc-gist/scripts/gist_upload.py +1 -1
- package/skills/implement/SKILL.md +2 -2
- package/skills/implement/scripts/append_note.py +1 -1
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +1 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/check_convergence.py +11 -4
- package/skills/pr-converge/scripts/check_pending_reviews.py +1 -1
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +1 -1
- package/skills/pr-converge/scripts/post_fix_reply.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
- package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
- package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
- package/skills/pr-converge/scripts/test_check_convergence.py +18 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- package/skills/pre-compact/SKILL.md +114 -0
- package/skills/session-log/SKILL.md +98 -233
- package/hooks/config/pr_description_enforcer_constants.py +0 -19
- package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
- package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
- package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
- package/skills/pr-converge/scripts/conftest.py +0 -60
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
- /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
- /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
- /package/hooks/{config → hooks_constants}/__init__.py +0 -0
- /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
- /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
- /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
- /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/messages.py +0 -0
- /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
- /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
- /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
- /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
- /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
- /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
- /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
- /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
- /package/skills/{implement/scripts/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/claude_permissions_common_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
- /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
- /package/skills/{pr-converge/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
- /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
- /package/skills/{pr-converge/scripts/config → implement/scripts/implement_scripts_constants}/__init__.py +0 -0
- /package/skills/implement/scripts/{config → implement_scripts_constants}/notes_constants.py +0 -0
- /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -293,14 +293,25 @@ def run_test_safety_checks(files: List[Path]) -> ValidatorResult:
|
|
|
293
293
|
|
|
294
294
|
|
|
295
295
|
def get_project_root() -> Optional[Path]:
|
|
296
|
-
"""Get project root by finding git root.
|
|
297
|
-
|
|
298
|
-
|
|
296
|
+
"""Get project root by finding git root.
|
|
297
|
+
|
|
298
|
+
Uses ``git -C <hooks_dir>`` to pin git's working tree to the hooks
|
|
299
|
+
directory without setting the subprocess cwd. On Windows, ``CreateProcess``
|
|
300
|
+
rejects some UNC working directories, so setting ``cwd=hooks_dir`` would
|
|
301
|
+
fail when ``hooks_dir`` resolves to a UNC path. The ``-C`` flag tells git
|
|
302
|
+
to operate as if started in that directory while the subprocess itself
|
|
303
|
+
inherits a normal cwd from the caller. Anchoring git to ``hooks_dir`` is
|
|
304
|
+
required so the lookup resolves to this repo even when the caller's cwd
|
|
305
|
+
points at an unrelated git checkout (e.g., the user's home), avoiding
|
|
306
|
+
validators that ``rglob`` over tens of thousands of unrelated files.
|
|
307
|
+
"""
|
|
308
|
+
completed_git_lookup = subprocess.run(
|
|
309
|
+
["git", "-C", str(hooks_dir), "rev-parse", "--show-toplevel"],
|
|
299
310
|
capture_output=True,
|
|
300
311
|
text=True,
|
|
301
312
|
)
|
|
302
|
-
if
|
|
303
|
-
return Path(
|
|
313
|
+
if completed_git_lookup.returncode == 0:
|
|
314
|
+
return Path(completed_git_lookup.stdout.strip())
|
|
304
315
|
return None
|
|
305
316
|
|
|
306
317
|
|
|
@@ -14,6 +14,7 @@ from .output_formatter import (
|
|
|
14
14
|
ViolationDict,
|
|
15
15
|
ValidatorResultDict,
|
|
16
16
|
)
|
|
17
|
+
from . import run_all_validators
|
|
17
18
|
from .run_all_validators import run_validators_entrypoint_subprocess
|
|
18
19
|
|
|
19
20
|
|
|
@@ -100,6 +101,51 @@ class TestJsonFlag:
|
|
|
100
101
|
assert "results" in parsed
|
|
101
102
|
assert isinstance(parsed["results"], list)
|
|
102
103
|
|
|
104
|
+
def test_get_project_root_anchored_under_unrelated_cwd(
|
|
105
|
+
self, tmp_path, monkeypatch
|
|
106
|
+
) -> None:
|
|
107
|
+
"""``get_project_root`` anchors git to the hooks tree, not the caller cwd.
|
|
108
|
+
|
|
109
|
+
Regression for the defect where ``get_project_root`` ran
|
|
110
|
+
``git rev-parse --show-toplevel`` without anchoring to the hooks
|
|
111
|
+
directory; under a subprocess fallback cwd outside this repo, git
|
|
112
|
+
returned an unrelated checkout and the File Structure validator
|
|
113
|
+
rglob'd tens of thousands of unrelated files. This test calls the
|
|
114
|
+
helper in-process from a non-git ``tmp_path`` to directly exercise
|
|
115
|
+
the anchoring behavior provided by ``git -C <hooks_dir>``.
|
|
116
|
+
"""
|
|
117
|
+
monkeypatch.chdir(tmp_path)
|
|
118
|
+
resolved_project_root = run_all_validators.get_project_root()
|
|
119
|
+
|
|
120
|
+
assert resolved_project_root is not None
|
|
121
|
+
assert resolved_project_root != tmp_path
|
|
122
|
+
hooks_directory_resolved = run_all_validators.hooks_dir.resolve()
|
|
123
|
+
assert hooks_directory_resolved.is_relative_to(resolved_project_root.resolve())
|
|
124
|
+
|
|
125
|
+
def test_file_structure_validator_output_is_bounded(self) -> None:
|
|
126
|
+
"""File Structure validator output stays under 10 kB end-to-end.
|
|
127
|
+
|
|
128
|
+
Smoke check that the validators entrypoint subprocess returns
|
|
129
|
+
bounded File Structure output (<10 kB). The unrelated-cwd
|
|
130
|
+
anchoring behavior itself is exercised in-process by
|
|
131
|
+
``test_get_project_root_anchored_under_unrelated_cwd``; this
|
|
132
|
+
subprocess test verifies the integrated entrypoint stays within
|
|
133
|
+
a bounded output budget. ``run_validators_entrypoint_subprocess``
|
|
134
|
+
sets its own ``cwd`` via
|
|
135
|
+
``_hooks_subprocess_working_directory_and_environment``, so the
|
|
136
|
+
subprocess cwd is fixed regardless of the test runner's cwd.
|
|
137
|
+
"""
|
|
138
|
+
completed_validation_run = run_validators_entrypoint_subprocess(["--json"])
|
|
139
|
+
|
|
140
|
+
parsed = json.loads(completed_validation_run.stdout.strip())
|
|
141
|
+
file_structure_results = [
|
|
142
|
+
each_validator_result
|
|
143
|
+
for each_validator_result in parsed["results"]
|
|
144
|
+
if each_validator_result["name"] == "File Structure"
|
|
145
|
+
]
|
|
146
|
+
assert len(file_structure_results) == 1
|
|
147
|
+
assert len(file_structure_results[0]["output"]) < 10000
|
|
148
|
+
|
|
103
149
|
|
|
104
150
|
class TestGroupViolationsByFile:
|
|
105
151
|
def test_groups_violations_by_file_path(self) -> None:
|
|
@@ -32,7 +32,7 @@ _hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
|
32
32
|
if _hooks_directory not in sys.path:
|
|
33
33
|
sys.path.insert(0, _hooks_directory)
|
|
34
34
|
|
|
35
|
-
from
|
|
35
|
+
from hooks_constants.doc_gist_auto_publish_constants import ( # noqa: E402
|
|
36
36
|
ALL_TARGET_TOOL_NAMES,
|
|
37
37
|
HOOK_SUBPROCESS_TIMEOUT_SECONDS,
|
|
38
38
|
HTML_FILE_EXTENSION,
|
|
@@ -18,11 +18,15 @@ from urllib.parse import urlparse
|
|
|
18
18
|
|
|
19
19
|
logging.basicConfig(stream=sys.stderr, level=logging.WARNING, format="%(message)s")
|
|
20
20
|
|
|
21
|
-
_hook_dir = str(Path(__file__).
|
|
21
|
+
_hook_dir = str(Path(__file__).resolve().parent.parent)
|
|
22
22
|
if _hook_dir not in sys.path:
|
|
23
23
|
sys.path.insert(0, _hook_dir)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
_blocking_dir = str(Path(__file__).resolve().parent.parent / "blocking")
|
|
26
|
+
if _blocking_dir not in sys.path:
|
|
27
|
+
sys.path.insert(0, _blocking_dir)
|
|
28
|
+
|
|
29
|
+
from hooks_constants.html_companion_constants import ( # noqa: E402
|
|
26
30
|
BLOCKED_URL_SCHEMES,
|
|
27
31
|
CSS_ACCENT_COLOR,
|
|
28
32
|
CSS_BG_COLOR,
|
|
@@ -41,18 +45,7 @@ from config.html_companion_constants import ( # noqa: E402
|
|
|
41
45
|
CSS_TABLE_WIDTH,
|
|
42
46
|
CSS_TH_WEIGHT,
|
|
43
47
|
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _is_exempt_path(file_path: str) -> bool:
|
|
47
|
-
normalized = file_path.replace("\\", "/")
|
|
48
|
-
if "/.claude/" in normalized or normalized.startswith(".claude/"):
|
|
49
|
-
return True
|
|
50
|
-
if normalized.startswith("./"):
|
|
51
|
-
normalized = normalized[2:]
|
|
52
|
-
stripped = normalized.lstrip("/")
|
|
53
|
-
if "/" not in stripped:
|
|
54
|
-
return stripped.lower() in ("readme.md", "changelog.md")
|
|
55
|
-
return False
|
|
48
|
+
from md_path_exemptions import is_exempt_path # noqa: E402
|
|
56
49
|
|
|
57
50
|
|
|
58
51
|
def _md_to_html(markdown_text: str) -> str:
|
|
@@ -330,7 +323,7 @@ def main() -> None:
|
|
|
330
323
|
if not file_path or not file_path.lower().endswith(".md"):
|
|
331
324
|
sys.exit(0)
|
|
332
325
|
|
|
333
|
-
if
|
|
326
|
+
if is_exempt_path(file_path):
|
|
334
327
|
sys.exit(0)
|
|
335
328
|
|
|
336
329
|
if not os.path.exists(file_path):
|
|
@@ -3,18 +3,73 @@
|
|
|
3
3
|
This test suite validates that the md-to-html companion hook correctly
|
|
4
4
|
generates HTML from markdown input, handles edge cases, and produces
|
|
5
5
|
valid HTML output.
|
|
6
|
+
|
|
7
|
+
Sandbox parent is created lazily by a session-scoped fixture rather than at
|
|
8
|
+
module import time, so test collection has no side effect on the filesystem.
|
|
9
|
+
The sandbox is rooted in a per-session unique directory created via
|
|
10
|
+
`tempfile.mkdtemp` so the OS-temp exemption (which the companion shares with
|
|
11
|
+
the blocker) does not silently skip the hook during tests.
|
|
6
12
|
"""
|
|
7
13
|
|
|
14
|
+
import functools
|
|
8
15
|
import json
|
|
9
16
|
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import stat
|
|
10
19
|
import subprocess
|
|
11
20
|
import sys
|
|
12
21
|
import tempfile
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
13
25
|
|
|
14
26
|
|
|
15
27
|
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "md_to_html_companion.py")
|
|
16
28
|
|
|
17
29
|
|
|
30
|
+
def _strip_read_only_and_retry(removal_function, target_path, *_exc_info):
|
|
31
|
+
try:
|
|
32
|
+
os.chmod(target_path, stat.S_IWRITE)
|
|
33
|
+
removal_function(target_path)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _force_rmtree(target_path: str) -> None:
|
|
39
|
+
handler_kw = (
|
|
40
|
+
{"onexc": _strip_read_only_and_retry}
|
|
41
|
+
if sys.version_info >= (3, 12)
|
|
42
|
+
else {"onerror": _strip_read_only_and_retry}
|
|
43
|
+
)
|
|
44
|
+
try:
|
|
45
|
+
shutil.rmtree(target_path, **handler_kw)
|
|
46
|
+
except OSError:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@functools.lru_cache(maxsize=1)
|
|
51
|
+
def _get_sandbox_parent_directory() -> str:
|
|
52
|
+
return tempfile.mkdtemp(prefix="pytest_md_companion_", dir=str(Path.home()))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(scope="session", autouse=True)
|
|
56
|
+
def _cleanup_sandbox_parent_directory():
|
|
57
|
+
yield
|
|
58
|
+
if _get_sandbox_parent_directory.cache_info().currsize:
|
|
59
|
+
_force_rmtree(_get_sandbox_parent_directory())
|
|
60
|
+
_get_sandbox_parent_directory.cache_clear()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _make_sandbox() -> tempfile.TemporaryDirectory:
|
|
64
|
+
"""Return a TemporaryDirectory rooted outside the OS temp directory.
|
|
65
|
+
|
|
66
|
+
The companion exempts the OS temp directory (mirroring the blocker), so
|
|
67
|
+
the default `tempfile.TemporaryDirectory()` would prevent the test hook
|
|
68
|
+
invocation generating any HTML sidecar at all.
|
|
69
|
+
"""
|
|
70
|
+
return tempfile.TemporaryDirectory(dir=_get_sandbox_parent_directory())
|
|
71
|
+
|
|
72
|
+
|
|
18
73
|
class _RunHook:
|
|
19
74
|
def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
|
|
20
75
|
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
@@ -31,7 +86,7 @@ _run_hook = _RunHook()
|
|
|
31
86
|
|
|
32
87
|
|
|
33
88
|
def test_generates_html_companion():
|
|
34
|
-
with
|
|
89
|
+
with _make_sandbox() as tmp:
|
|
35
90
|
md_path = os.path.join(tmp, "guide.md")
|
|
36
91
|
html_path = os.path.join(tmp, "guide.html")
|
|
37
92
|
|
|
@@ -47,7 +102,7 @@ def test_generates_html_companion():
|
|
|
47
102
|
|
|
48
103
|
|
|
49
104
|
def test_html_contains_heading():
|
|
50
|
-
with
|
|
105
|
+
with _make_sandbox() as tmp:
|
|
51
106
|
md_path = os.path.join(tmp, "guide.md")
|
|
52
107
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
53
108
|
f.write("# Hello World")
|
|
@@ -61,7 +116,7 @@ def test_html_contains_heading():
|
|
|
61
116
|
|
|
62
117
|
|
|
63
118
|
def test_html_wraps_in_template():
|
|
64
|
-
with
|
|
119
|
+
with _make_sandbox() as tmp:
|
|
65
120
|
md_path = os.path.join(tmp, "guide.md")
|
|
66
121
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
67
122
|
f.write("plain text")
|
|
@@ -75,7 +130,7 @@ def test_html_wraps_in_template():
|
|
|
75
130
|
|
|
76
131
|
|
|
77
132
|
def test_skips_non_md_files():
|
|
78
|
-
with
|
|
133
|
+
with _make_sandbox() as tmp:
|
|
79
134
|
py_path = os.path.join(tmp, "main.py")
|
|
80
135
|
html_path = os.path.join(tmp, "main.html")
|
|
81
136
|
|
|
@@ -89,7 +144,7 @@ def test_skips_non_md_files():
|
|
|
89
144
|
|
|
90
145
|
|
|
91
146
|
def test_skips_claude_dir():
|
|
92
|
-
with
|
|
147
|
+
with _make_sandbox() as tmp:
|
|
93
148
|
claude_dir = os.path.join(tmp, ".claude")
|
|
94
149
|
md_path = os.path.join(claude_dir, "CLAUDE.md")
|
|
95
150
|
html_path = os.path.join(claude_dir, "CLAUDE.html")
|
|
@@ -124,7 +179,7 @@ def test_nonexistent_md_passes():
|
|
|
124
179
|
|
|
125
180
|
|
|
126
181
|
def test_converts_code_fence():
|
|
127
|
-
with
|
|
182
|
+
with _make_sandbox() as tmp:
|
|
128
183
|
md_path = os.path.join(tmp, "guide.md")
|
|
129
184
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
130
185
|
f.write("```python\nprint('hi')\n```")
|
|
@@ -141,7 +196,7 @@ def test_converts_code_fence():
|
|
|
141
196
|
|
|
142
197
|
|
|
143
198
|
def test_converts_bold():
|
|
144
|
-
with
|
|
199
|
+
with _make_sandbox() as tmp:
|
|
145
200
|
md_path = os.path.join(tmp, "guide.md")
|
|
146
201
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
147
202
|
f.write("This is **bold** text.")
|
|
@@ -154,7 +209,7 @@ def test_converts_bold():
|
|
|
154
209
|
|
|
155
210
|
|
|
156
211
|
def test_escapes_html_special_chars():
|
|
157
|
-
with
|
|
212
|
+
with _make_sandbox() as tmp:
|
|
158
213
|
md_path = os.path.join(tmp, "guide.md")
|
|
159
214
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
160
215
|
f.write("Use <div> for layout & choose \"text\" for quotes.")
|
|
@@ -175,7 +230,7 @@ def test_escapes_html_special_chars():
|
|
|
175
230
|
|
|
176
231
|
|
|
177
232
|
def test_escapes_code_block_content():
|
|
178
|
-
with
|
|
233
|
+
with _make_sandbox() as tmp:
|
|
179
234
|
md_path = os.path.join(tmp, "guide.md")
|
|
180
235
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
181
236
|
f.write("```\nif x < 5 and y > 3:\n print('hello')\n```")
|
|
@@ -195,7 +250,7 @@ def test_escapes_code_block_content():
|
|
|
195
250
|
|
|
196
251
|
|
|
197
252
|
def test_lists_are_wrapped_in_ul():
|
|
198
|
-
with
|
|
253
|
+
with _make_sandbox() as tmp:
|
|
199
254
|
md_path = os.path.join(tmp, "guide.md")
|
|
200
255
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
201
256
|
f.write("- item one\n- item two\n- item three")
|
|
@@ -217,7 +272,7 @@ def test_lists_are_wrapped_in_ul():
|
|
|
217
272
|
|
|
218
273
|
|
|
219
274
|
def test_ordered_lists_are_wrapped_in_ol():
|
|
220
|
-
with
|
|
275
|
+
with _make_sandbox() as tmp:
|
|
221
276
|
md_path = os.path.join(tmp, "guide.md")
|
|
222
277
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
223
278
|
f.write("1. first\n2. second")
|
|
@@ -234,7 +289,7 @@ def test_ordered_lists_are_wrapped_in_ol():
|
|
|
234
289
|
|
|
235
290
|
|
|
236
291
|
def test_handles_curly_braces_in_body():
|
|
237
|
-
with
|
|
292
|
+
with _make_sandbox() as tmp:
|
|
238
293
|
md_path = os.path.join(tmp, "guide.md")
|
|
239
294
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
240
295
|
f.write("# JS Example\n\nUse `{ foo: 1 }` in code.")
|
|
@@ -255,7 +310,7 @@ def test_handles_curly_braces_in_body():
|
|
|
255
310
|
|
|
256
311
|
|
|
257
312
|
def test_escapes_title_in_html_output():
|
|
258
|
-
with
|
|
313
|
+
with _make_sandbox() as tmp:
|
|
259
314
|
md_path = os.path.join(tmp, "guide.md")
|
|
260
315
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
261
316
|
f.write("# Hackers <3 Markdown & <scripts>")
|
|
@@ -275,7 +330,8 @@ def test_escapes_title_in_html_output():
|
|
|
275
330
|
|
|
276
331
|
|
|
277
332
|
def test_skips_root_readme():
|
|
278
|
-
with
|
|
333
|
+
with _make_sandbox() as tmp:
|
|
334
|
+
Path(tmp, ".git").touch()
|
|
279
335
|
original_cwd = os.getcwd()
|
|
280
336
|
try:
|
|
281
337
|
os.chdir(tmp)
|
|
@@ -293,7 +349,8 @@ def test_skips_root_readme():
|
|
|
293
349
|
|
|
294
350
|
|
|
295
351
|
def test_skips_root_changelog():
|
|
296
|
-
with
|
|
352
|
+
with _make_sandbox() as tmp:
|
|
353
|
+
Path(tmp, ".git").touch()
|
|
297
354
|
original_cwd = os.getcwd()
|
|
298
355
|
try:
|
|
299
356
|
os.chdir(tmp)
|
|
@@ -311,7 +368,7 @@ def test_skips_root_changelog():
|
|
|
311
368
|
|
|
312
369
|
|
|
313
370
|
def test_language_class_valid():
|
|
314
|
-
with
|
|
371
|
+
with _make_sandbox() as tmp:
|
|
315
372
|
md_path = os.path.join(tmp, "guide.md")
|
|
316
373
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
317
374
|
f.write("```python\nx = 1\n```")
|
|
@@ -324,7 +381,7 @@ def test_language_class_valid():
|
|
|
324
381
|
|
|
325
382
|
|
|
326
383
|
def test_language_class_skips_invalid():
|
|
327
|
-
with
|
|
384
|
+
with _make_sandbox() as tmp:
|
|
328
385
|
md_path = os.path.join(tmp, "guide.md")
|
|
329
386
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
330
387
|
f.write("```my lang\nx = 1\n```")
|
|
@@ -338,7 +395,7 @@ def test_language_class_skips_invalid():
|
|
|
338
395
|
|
|
339
396
|
|
|
340
397
|
def test_language_class_allows_valid_chars():
|
|
341
|
-
with
|
|
398
|
+
with _make_sandbox() as tmp:
|
|
342
399
|
md_path = os.path.join(tmp, "guide.md")
|
|
343
400
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
344
401
|
f.write("```c++\nint x = 1;\n```")
|
|
@@ -351,7 +408,7 @@ def test_language_class_allows_valid_chars():
|
|
|
351
408
|
|
|
352
409
|
|
|
353
410
|
def test_link_text_asterisks_remain_literal():
|
|
354
|
-
with
|
|
411
|
+
with _make_sandbox() as tmp:
|
|
355
412
|
md_path = os.path.join(tmp, "guide.md")
|
|
356
413
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
357
414
|
f.write("See [text *not italic*](url).")
|
|
@@ -368,7 +425,7 @@ def test_link_text_asterisks_remain_literal():
|
|
|
368
425
|
|
|
369
426
|
|
|
370
427
|
def test_handles_parentheses_in_links():
|
|
371
|
-
with
|
|
428
|
+
with _make_sandbox() as tmp:
|
|
372
429
|
md_path = os.path.join(tmp, "guide.md")
|
|
373
430
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
374
431
|
f.write(
|
|
@@ -394,7 +451,7 @@ def test_handles_parentheses_in_links():
|
|
|
394
451
|
|
|
395
452
|
|
|
396
453
|
def test_does_not_skip_nested_readme():
|
|
397
|
-
with
|
|
454
|
+
with _make_sandbox() as tmp:
|
|
398
455
|
nested_dir = os.path.join(tmp, "docs")
|
|
399
456
|
os.makedirs(nested_dir)
|
|
400
457
|
md_path = os.path.join(nested_dir, "README.md")
|
|
@@ -412,7 +469,7 @@ def test_does_not_skip_nested_readme():
|
|
|
412
469
|
|
|
413
470
|
|
|
414
471
|
def test_inline_code_preserves_asterisks():
|
|
415
|
-
with
|
|
472
|
+
with _make_sandbox() as tmp:
|
|
416
473
|
md_path = os.path.join(tmp, "guide.md")
|
|
417
474
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
418
475
|
f.write("Type `**bold**` in a docstring.")
|
|
@@ -432,7 +489,7 @@ def test_inline_code_preserves_asterisks():
|
|
|
432
489
|
|
|
433
490
|
|
|
434
491
|
def test_blocks_javascript_url_scheme():
|
|
435
|
-
with
|
|
492
|
+
with _make_sandbox() as tmp:
|
|
436
493
|
md_path = os.path.join(tmp, "guide.md")
|
|
437
494
|
with open(md_path, "w", encoding="utf-8") as f:
|
|
438
495
|
f.write("[click me](javascript:alert(1))")
|
|
@@ -450,3 +507,107 @@ def test_blocks_javascript_url_scheme():
|
|
|
450
507
|
assert "javascript:" not in html
|
|
451
508
|
assert "click me" in html
|
|
452
509
|
assert "<a" not in html
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_companion_skips_home_session_log_directory(tmp_path, monkeypatch):
|
|
513
|
+
synthetic_home_directory = tmp_path / "synthetic_home"
|
|
514
|
+
synthetic_home_directory.mkdir()
|
|
515
|
+
monkeypatch.setenv("HOME", str(synthetic_home_directory))
|
|
516
|
+
monkeypatch.setenv("USERPROFILE", str(synthetic_home_directory))
|
|
517
|
+
session_log_directory = synthetic_home_directory / "SessionLog" / "decisions"
|
|
518
|
+
session_log_directory.mkdir(parents=True)
|
|
519
|
+
md_path = str(session_log_directory / "companion_exempt_test.md")
|
|
520
|
+
html_path = str(session_log_directory / "companion_exempt_test.html")
|
|
521
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
522
|
+
f.write("# Note")
|
|
523
|
+
result = _run_hook(
|
|
524
|
+
"Write",
|
|
525
|
+
{"file_path": md_path, "content": "# Note"},
|
|
526
|
+
)
|
|
527
|
+
assert result.returncode == 0
|
|
528
|
+
assert not os.path.exists(html_path)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_companion_skips_skill_md_anywhere():
|
|
532
|
+
with _make_sandbox() as tmp:
|
|
533
|
+
nested_directory = os.path.join(tmp, "packages", "dev-env", "skills", "foo")
|
|
534
|
+
os.makedirs(nested_directory, exist_ok=True)
|
|
535
|
+
md_path = os.path.join(nested_directory, "SKILL.md")
|
|
536
|
+
html_path = os.path.join(nested_directory, "SKILL.html")
|
|
537
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
538
|
+
f.write("# Skill")
|
|
539
|
+
result = _run_hook(
|
|
540
|
+
"Write",
|
|
541
|
+
{"file_path": md_path, "content": "# Skill"},
|
|
542
|
+
)
|
|
543
|
+
assert result.returncode == 0
|
|
544
|
+
assert not os.path.exists(html_path)
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_companion_skips_agents_directory_anywhere():
|
|
548
|
+
with _make_sandbox() as tmp:
|
|
549
|
+
agents_directory = os.path.join(tmp, "packages", "dev-env", "agents")
|
|
550
|
+
os.makedirs(agents_directory, exist_ok=True)
|
|
551
|
+
md_path = os.path.join(agents_directory, "pr-description-writer.md")
|
|
552
|
+
html_path = os.path.join(
|
|
553
|
+
agents_directory, "pr-description-writer.html"
|
|
554
|
+
)
|
|
555
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
556
|
+
f.write("# Agent")
|
|
557
|
+
result = _run_hook(
|
|
558
|
+
"Write",
|
|
559
|
+
{"file_path": md_path, "content": "# Agent"},
|
|
560
|
+
)
|
|
561
|
+
assert result.returncode == 0
|
|
562
|
+
assert not os.path.exists(html_path)
|
|
563
|
+
|
|
564
|
+
|
|
565
|
+
def test_companion_skips_claude_plugin_directory():
|
|
566
|
+
with _make_sandbox() as tmp:
|
|
567
|
+
plugin_directory = os.path.join(tmp, ".claude-plugin")
|
|
568
|
+
os.makedirs(plugin_directory, exist_ok=True)
|
|
569
|
+
md_path = os.path.join(plugin_directory, "manifest.md")
|
|
570
|
+
html_path = os.path.join(plugin_directory, "manifest.html")
|
|
571
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
572
|
+
f.write("# Manifest")
|
|
573
|
+
result = _run_hook(
|
|
574
|
+
"Write",
|
|
575
|
+
{"file_path": md_path, "content": "# Manifest"},
|
|
576
|
+
)
|
|
577
|
+
assert result.returncode == 0
|
|
578
|
+
assert not os.path.exists(html_path)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def test_companion_still_fires_for_ordinary_docs_md_file():
|
|
582
|
+
with _make_sandbox() as tmp:
|
|
583
|
+
docs_directory = os.path.join(tmp, "docs")
|
|
584
|
+
os.makedirs(docs_directory, exist_ok=True)
|
|
585
|
+
md_path = os.path.join(docs_directory, "regular.md")
|
|
586
|
+
html_path = os.path.join(docs_directory, "regular.html")
|
|
587
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
588
|
+
f.write("# Regular")
|
|
589
|
+
result = _run_hook(
|
|
590
|
+
"Write",
|
|
591
|
+
{"file_path": md_path, "content": "# Regular"},
|
|
592
|
+
)
|
|
593
|
+
assert result.returncode == 0
|
|
594
|
+
assert os.path.exists(html_path)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def test_companion_skips_system_temp_directory():
|
|
598
|
+
temp_directory = tempfile.gettempdir()
|
|
599
|
+
md_path = os.path.join(temp_directory, "companion_temp_exempt_test.md")
|
|
600
|
+
html_path = os.path.join(temp_directory, "companion_temp_exempt_test.html")
|
|
601
|
+
try:
|
|
602
|
+
with open(md_path, "w", encoding="utf-8") as f:
|
|
603
|
+
f.write("# Scratch")
|
|
604
|
+
result = _run_hook(
|
|
605
|
+
"Write",
|
|
606
|
+
{"file_path": md_path, "content": "# Scratch"},
|
|
607
|
+
)
|
|
608
|
+
assert result.returncode == 0
|
|
609
|
+
assert not os.path.exists(html_path)
|
|
610
|
+
finally:
|
|
611
|
+
for each_path in (md_path, html_path):
|
|
612
|
+
if os.path.exists(each_path):
|
|
613
|
+
os.remove(each_path)
|
package/package.json
CHANGED
|
@@ -40,5 +40,5 @@ When a question is genuinely for the user, call the tool with:
|
|
|
40
40
|
|
|
41
41
|
- Hook: `packages/claude-dev-env/hooks/blocking/question_to_user_enforcer.py`, registered on the `Stop` matcher in `packages/claude-dev-env/hooks/hooks.json`.
|
|
42
42
|
- Loop prevention: the hook honors Claude Code's `stop_hook_active` flag and does not re-block on retry.
|
|
43
|
-
- User-facing notice: `USER_FACING_ASKUSERQUESTION_NOTICE` in `packages/claude-dev-env/hooks/
|
|
43
|
+
- User-facing notice: `USER_FACING_ASKUSERQUESTION_NOTICE` in `packages/claude-dev-env/hooks/hooks_constants/messages.py`.
|
|
44
44
|
- Related rule: `packages/claude-dev-env/rules/verify-before-asking.md` gates whether the question belongs to the user in the first place.
|
package/rules/vault-context.md
CHANGED
|
@@ -10,7 +10,7 @@ An Obsidian vault stores session reports, decisions, and research documents acro
|
|
|
10
10
|
|
|
11
11
|
## Vault Structure
|
|
12
12
|
|
|
13
|
-
- `sessions/` -- session reports with frontmatter: `type: session-report`, `project`, `session`, `date`, `status`, `blocked`, `tags`
|
|
13
|
+
- `sessions/` -- session reports with frontmatter: `type: session-report`, `project`, `session`, `date`, `status`, `blocked`, `vault_context_retrieved`, `tags`
|
|
14
14
|
- `decisions/` -- decision notes with frontmatter: `type: decision|procedural|fact|gotcha`, `project`, `date`, `status: Active|Superseded`, `tags`
|
|
15
15
|
- `Research/` -- deep research documents
|
|
16
16
|
|
|
@@ -18,17 +18,12 @@ import subprocess
|
|
|
18
18
|
import sys
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
sys.path.
|
|
24
|
-
sys.path.insert(0, _hooks_dir_string)
|
|
21
|
+
_hooks_dir = str(Path(__file__).resolve().parent.parent / "hooks")
|
|
22
|
+
if _hooks_dir not in sys.path:
|
|
23
|
+
sys.path.insert(0, _hooks_dir)
|
|
25
24
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
del sys.modules[_cached_module_name]
|
|
29
|
-
|
|
30
|
-
from config.project_paths_reader import registry_file_path
|
|
31
|
-
from config.setup_project_paths_constants import (
|
|
25
|
+
from hooks_constants.project_paths_reader import registry_file_path # noqa: E402
|
|
26
|
+
from hooks_constants.setup_project_paths_constants import ( # noqa: E402
|
|
32
27
|
ABORTED_NOTHING_WRITTEN_MESSAGE,
|
|
33
28
|
CONFIRMATION_PROMPT_TEXT,
|
|
34
29
|
ES_EXE_BINARY_NAME,
|
|
@@ -85,6 +80,13 @@ def filter_to_git_roots(all_es_exe_paths: list[str]) -> list[str]:
|
|
|
85
80
|
|
|
86
81
|
Rejects siblings like ``.gitignore``, ``.github``, ``.gitattributes`` that
|
|
87
82
|
share the ``.git`` prefix but are not the canonical git metadata directory.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
all_es_exe_paths: Raw folder paths emitted by es.exe.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Parent directory paths of every entry whose final segment is exactly
|
|
89
|
+
``.git``.
|
|
88
90
|
"""
|
|
89
91
|
all_repo_roots: list[str] = []
|
|
90
92
|
for each_es_path in all_es_exe_paths:
|
|
@@ -102,6 +104,12 @@ def apply_exclusion_filter(all_candidate_paths: list[str]) -> list[str]:
|
|
|
102
104
|
Whole-segment matching preserves legitimate names that merely contain an
|
|
103
105
|
excluded substring (for example ``template`` is retained even though
|
|
104
106
|
``temp`` is excluded).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
all_candidate_paths: Repo-root paths discovered by es.exe.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Subset of *all_candidate_paths* with no segment in the exclusion set.
|
|
105
113
|
"""
|
|
106
114
|
all_retained_paths: list[str] = []
|
|
107
115
|
for each_candidate_path in all_candidate_paths:
|
|
@@ -134,6 +142,13 @@ def merge_registries(
|
|
|
134
142
|
Pre-existing entries not in the new set are preserved. On name collisions
|
|
135
143
|
the newly discovered entry wins. The ``_meta.last_scan`` timestamp is
|
|
136
144
|
refreshed to the current UTC time.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
existing_registry: Existing on-disk registry contents.
|
|
148
|
+
new_path_by_name: Newly discovered name-to-path entries.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Merged registry with refreshed ``_meta.last_scan`` timestamp.
|
|
137
152
|
"""
|
|
138
153
|
merged_registry: dict = {
|
|
139
154
|
each_key: each_value
|
|
@@ -191,6 +206,11 @@ def write_registry_atomically(registry_to_write: dict, target_file: Path) -> Non
|
|
|
191
206
|
Caller is responsible for reading the existing registry, verifying the
|
|
192
207
|
schema version, and merging before calling this function. This function
|
|
193
208
|
performs no file reads and no schema checks.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
registry_to_write: Registry content to serialize.
|
|
212
|
+
target_file: Final destination path; the temp sibling is created
|
|
213
|
+
adjacent and renamed over the destination.
|
|
194
214
|
"""
|
|
195
215
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
196
216
|
temp_suffix = ".tmp"
|
|
@@ -228,7 +248,11 @@ def _run_es_exe_folders_query() -> list[str]:
|
|
|
228
248
|
|
|
229
249
|
|
|
230
250
|
def discover_repo_roots_via_everything() -> list[str]:
|
|
231
|
-
"""Run es.exe, filter to genuine git roots, deduplicate, and sort.
|
|
251
|
+
"""Run es.exe, filter to genuine git roots, deduplicate, and sort.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Deduplicated, sorted list of repo-root paths discovered by es.exe.
|
|
255
|
+
"""
|
|
232
256
|
all_raw_paths = _run_es_exe_folders_query()
|
|
233
257
|
all_git_roots = filter_to_git_roots(all_raw_paths)
|
|
234
258
|
all_included = apply_exclusion_filter(all_git_roots)
|
|
@@ -291,6 +315,11 @@ def prompt_and_write(
|
|
|
291
315
|
|
|
292
316
|
Reads and validates the existing registry BEFORE prompting so the user
|
|
293
317
|
learns of any schema or read error early. Declining writes nothing.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
path_by_name: Proposed name-to-path mapping to present to the user.
|
|
321
|
+
save_path: Registry destination path used for the schema check
|
|
322
|
+
and atomic write target.
|
|
294
323
|
"""
|
|
295
324
|
existing_registry = _load_and_validate_registry(save_path)
|
|
296
325
|
_display_proposed_mapping(path_by_name, save_path)
|
|
@@ -319,6 +348,15 @@ def _build_path_by_name_from_roots(all_repo_roots: list[str]) -> dict[str, str]:
|
|
|
319
348
|
|
|
320
349
|
|
|
321
350
|
def main() -> int:
|
|
351
|
+
"""Run the discovery, prompt, and atomic-write flow for the registry.
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
``0`` on success or when no candidate repositories were found.
|
|
355
|
+
|
|
356
|
+
Raises:
|
|
357
|
+
SystemExit: When es.exe scan fails or the existing registry is
|
|
358
|
+
malformed.
|
|
359
|
+
"""
|
|
322
360
|
if not _everything_binary_is_available():
|
|
323
361
|
print(
|
|
324
362
|
f"ERROR: {ES_EXE_BINARY_NAME} not found on PATH. Install Everything "
|