claude-dev-env 1.41.0 → 1.43.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/CLAUDE.md +8 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
- 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 +124 -20
- package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
- package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
- 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 +76 -33
- package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -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 +5 -3
- 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} +110 -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/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
- 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 +71 -25
- 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 +356 -0
- 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 +66 -0
- package/skills/implement/scripts/append_note.py +133 -0
- package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
- package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
- package/skills/implement/scripts/test_append_note.py +191 -0
- package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
- package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
- package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
- package/skills/pr-converge/scripts/check_convergence.py +175 -29
- package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
- package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
- 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_bugbot_ci.py +1 -1
- package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
- package/skills/refine/SKILL.md +257 -0
- package/skills/refine/templates/implementation-notes-template.html +56 -0
- package/skills/refine/templates/plan-template.md +60 -0
- package/skills/session-log/SKILL.md +98 -233
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
- 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/config/claude_permissions_common_constants.py +0 -20
- 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/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}/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/{pr-converge/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}/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/scripts/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 → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Behavior tests for the agent-config carve-out and stale-trust-entry purge.
|
|
2
|
+
|
|
3
|
+
Covers two Bugbot findings on PR #467:
|
|
4
|
+
- Deny rules must be written to permissions.deny so agent-config edits
|
|
5
|
+
require explicit per-edit user approval.
|
|
6
|
+
- Trust entries in autoMode.environment must be purged on grant
|
|
7
|
+
(preventing accumulation across template revisions) and removed on
|
|
8
|
+
revoke regardless of the exact template wording.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
_script_directory = str(Path(__file__).resolve().parent)
|
|
21
|
+
if _script_directory not in sys.path:
|
|
22
|
+
sys.path.insert(0, _script_directory)
|
|
23
|
+
|
|
24
|
+
import _bugteam_permissions_common as common_module
|
|
25
|
+
import grant_project_claude_permissions as grant_module
|
|
26
|
+
import revoke_project_claude_permissions as revoke_module
|
|
27
|
+
from bugteam_scripts_constants.claude_permissions_common_constants import (
|
|
28
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
29
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
30
|
+
ALL_PERMISSION_ALLOW_TOOLS,
|
|
31
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _make_fake_project(tmp_path: Path) -> Path:
|
|
36
|
+
fake_project_root = tmp_path / "fake_project"
|
|
37
|
+
(fake_project_root / ".claude").mkdir(parents=True)
|
|
38
|
+
return fake_project_root
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _project_path_as_posix(fake_project_root: Path) -> str:
|
|
42
|
+
return str(fake_project_root).replace("\\", "/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _redirect_settings_to_fake_path(
|
|
46
|
+
fake_settings_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
47
|
+
) -> None:
|
|
48
|
+
fake_home_directory = fake_settings_path.parent.parent
|
|
49
|
+
monkeypatch.setattr(Path, "home", classmethod(lambda _cls: fake_home_directory))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _prepare_fake_home(tmp_path: Path) -> Path:
|
|
53
|
+
fake_home_directory = tmp_path / "fake_home"
|
|
54
|
+
claude_settings_directory = fake_home_directory / ".claude"
|
|
55
|
+
claude_settings_directory.mkdir(parents=True)
|
|
56
|
+
return claude_settings_directory / "settings.json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _seed_grant_then_run(
|
|
60
|
+
fake_settings_path: Path,
|
|
61
|
+
fake_project_root: Path,
|
|
62
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
63
|
+
pre_existing_settings: dict[str, Any],
|
|
64
|
+
) -> None:
|
|
65
|
+
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
66
|
+
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
67
|
+
monkeypatch.chdir(fake_project_root)
|
|
68
|
+
grant_module.grant_permissions_for_current_directory()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _seed_revoke_then_run(
|
|
72
|
+
fake_settings_path: Path,
|
|
73
|
+
fake_project_root: Path,
|
|
74
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
75
|
+
pre_existing_settings: dict[str, Any],
|
|
76
|
+
) -> None:
|
|
77
|
+
fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
|
|
78
|
+
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
79
|
+
monkeypatch.chdir(fake_project_root)
|
|
80
|
+
revoke_module.revoke_permissions_for_current_directory()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_grant_writes_deny_rules_for_every_tool_and_pattern(
|
|
84
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
85
|
+
) -> None:
|
|
86
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
87
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
88
|
+
_seed_grant_then_run(
|
|
89
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
90
|
+
)
|
|
91
|
+
capsys.readouterr()
|
|
92
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
93
|
+
deny_list = written_settings["permissions"]["deny"]
|
|
94
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
95
|
+
for each_tool in ALL_AGENT_CONFIG_DENY_TOOLS:
|
|
96
|
+
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
97
|
+
expected_rule = f"{each_tool}({project_path_posix}/.claude/{each_pattern})"
|
|
98
|
+
assert expected_rule in deny_list, (
|
|
99
|
+
f"deny list missing expected rule {expected_rule!r}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_grant_writes_glob_deny_rules_for_every_agent_config_pattern(
|
|
104
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Glob must be in the deny tuple so agent-config paths require approval.
|
|
107
|
+
|
|
108
|
+
The AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE promises Edit/Write/Read/Glob
|
|
109
|
+
trust EXCEPT for agent-config files. Glob deny rules are how the EXCEPT
|
|
110
|
+
clause is honored for the Glob tool.
|
|
111
|
+
"""
|
|
112
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
113
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
114
|
+
_seed_grant_then_run(
|
|
115
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
116
|
+
)
|
|
117
|
+
capsys.readouterr()
|
|
118
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
119
|
+
deny_list = written_settings["permissions"]["deny"]
|
|
120
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
121
|
+
assert "Glob" in ALL_AGENT_CONFIG_DENY_TOOLS
|
|
122
|
+
assert "Glob" not in ALL_PERMISSION_ALLOW_TOOLS
|
|
123
|
+
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
124
|
+
expected_glob_rule = f"Glob({project_path_posix}/.claude/{each_pattern})"
|
|
125
|
+
assert expected_glob_rule in deny_list, (
|
|
126
|
+
f"deny list missing expected Glob rule {expected_glob_rule!r}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_grant_purges_stale_trust_entries_then_writes_current_template(
|
|
131
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
132
|
+
) -> None:
|
|
133
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
134
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
135
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
136
|
+
stale_entry_a = (
|
|
137
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** old wording form A"
|
|
138
|
+
)
|
|
139
|
+
stale_entry_b = (
|
|
140
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** "
|
|
141
|
+
f"different earlier wording"
|
|
142
|
+
)
|
|
143
|
+
unrelated_entry = "Some unrelated environment hint"
|
|
144
|
+
pre_existing_settings: dict[str, Any] = {
|
|
145
|
+
"autoMode": {
|
|
146
|
+
"environment": [stale_entry_a, stale_entry_b, unrelated_entry],
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
_seed_grant_then_run(
|
|
150
|
+
fake_settings_path,
|
|
151
|
+
fake_project_root,
|
|
152
|
+
monkeypatch,
|
|
153
|
+
pre_existing_settings=pre_existing_settings,
|
|
154
|
+
)
|
|
155
|
+
captured = capsys.readouterr()
|
|
156
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
157
|
+
environment_list = written_settings["autoMode"]["environment"]
|
|
158
|
+
assert stale_entry_a not in environment_list
|
|
159
|
+
assert stale_entry_b not in environment_list
|
|
160
|
+
assert unrelated_entry in environment_list
|
|
161
|
+
matching_trust_entries = [
|
|
162
|
+
each_entry
|
|
163
|
+
for each_entry in environment_list
|
|
164
|
+
if isinstance(each_entry, str)
|
|
165
|
+
and each_entry.startswith("Trusted local workspace:")
|
|
166
|
+
and f"{project_path_posix}/.claude/**" in each_entry
|
|
167
|
+
]
|
|
168
|
+
assert len(matching_trust_entries) == 1
|
|
169
|
+
assert "Stale auto-mode environment entries purged" in captured.out
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_revoke_removes_deny_rules(
|
|
173
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
174
|
+
) -> None:
|
|
175
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
176
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
177
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
178
|
+
all_deny_rules = common_module.build_agent_config_deny_rules(
|
|
179
|
+
project_path_posix,
|
|
180
|
+
ALL_AGENT_CONFIG_DENY_TOOLS,
|
|
181
|
+
ALL_AGENT_CONFIG_PATH_PATTERNS,
|
|
182
|
+
)
|
|
183
|
+
pre_existing_settings: dict[str, Any] = {
|
|
184
|
+
"permissions": {
|
|
185
|
+
"deny": list(all_deny_rules),
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
_seed_revoke_then_run(
|
|
189
|
+
fake_settings_path,
|
|
190
|
+
fake_project_root,
|
|
191
|
+
monkeypatch,
|
|
192
|
+
pre_existing_settings=pre_existing_settings,
|
|
193
|
+
)
|
|
194
|
+
capsys.readouterr()
|
|
195
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
196
|
+
permissions_section = written_settings.get("permissions", {})
|
|
197
|
+
remaining_deny_list = permissions_section.get("deny", [])
|
|
198
|
+
for each_rule in all_deny_rules:
|
|
199
|
+
assert each_rule not in remaining_deny_list
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_revoke_removes_every_legacy_trust_entry_for_project(
|
|
203
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
204
|
+
) -> None:
|
|
205
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
206
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
207
|
+
project_path_posix = _project_path_as_posix(fake_project_root)
|
|
208
|
+
legacy_entry_a = (
|
|
209
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** template revision A"
|
|
210
|
+
)
|
|
211
|
+
legacy_entry_b = (
|
|
212
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** template revision B"
|
|
213
|
+
)
|
|
214
|
+
unrelated_other_project_entry = (
|
|
215
|
+
"Trusted local workspace: /some/other/project/.claude/** still valid"
|
|
216
|
+
)
|
|
217
|
+
pre_existing_settings: dict[str, Any] = {
|
|
218
|
+
"autoMode": {
|
|
219
|
+
"environment": [
|
|
220
|
+
legacy_entry_a,
|
|
221
|
+
legacy_entry_b,
|
|
222
|
+
unrelated_other_project_entry,
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
}
|
|
226
|
+
_seed_revoke_then_run(
|
|
227
|
+
fake_settings_path,
|
|
228
|
+
fake_project_root,
|
|
229
|
+
monkeypatch,
|
|
230
|
+
pre_existing_settings=pre_existing_settings,
|
|
231
|
+
)
|
|
232
|
+
written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
|
|
233
|
+
environment_list = written_settings.get("autoMode", {}).get("environment", [])
|
|
234
|
+
assert legacy_entry_a not in environment_list
|
|
235
|
+
assert legacy_entry_b not in environment_list
|
|
236
|
+
assert unrelated_other_project_entry in environment_list
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_template_constant_documents_agent_config_carveout() -> None:
|
|
240
|
+
assert "agent-config files always require explicit per-edit user approval" in (
|
|
241
|
+
AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_is_trust_entry_for_project_predicate_filters_by_prefix_and_project_path() -> (
|
|
246
|
+
None
|
|
247
|
+
):
|
|
248
|
+
project_path_posix = "/fake/proj"
|
|
249
|
+
trust_prefix = "Trusted local workspace:"
|
|
250
|
+
non_string_value: object = 42
|
|
251
|
+
assert (
|
|
252
|
+
common_module.is_trust_entry_for_project(
|
|
253
|
+
non_string_value, project_path_posix, trust_prefix
|
|
254
|
+
)
|
|
255
|
+
is False
|
|
256
|
+
)
|
|
257
|
+
wrong_prefix_entry = (
|
|
258
|
+
f"Something else: {project_path_posix}/.claude/** with marker token"
|
|
259
|
+
)
|
|
260
|
+
assert (
|
|
261
|
+
common_module.is_trust_entry_for_project(
|
|
262
|
+
wrong_prefix_entry, project_path_posix, trust_prefix
|
|
263
|
+
)
|
|
264
|
+
is False
|
|
265
|
+
)
|
|
266
|
+
different_project_entry = (
|
|
267
|
+
"Trusted local workspace: /other/project/.claude/** unrelated"
|
|
268
|
+
)
|
|
269
|
+
assert (
|
|
270
|
+
common_module.is_trust_entry_for_project(
|
|
271
|
+
different_project_entry, project_path_posix, trust_prefix
|
|
272
|
+
)
|
|
273
|
+
is False
|
|
274
|
+
)
|
|
275
|
+
matching_entry = (
|
|
276
|
+
f"Trusted local workspace: {project_path_posix}/.claude/** any wording form"
|
|
277
|
+
)
|
|
278
|
+
assert (
|
|
279
|
+
common_module.is_trust_entry_for_project(
|
|
280
|
+
matching_entry, project_path_posix, trust_prefix
|
|
281
|
+
)
|
|
282
|
+
is True
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
|
|
287
|
+
"""When the project_path is a path suffix of an unrelated entry's path,
|
|
288
|
+
the predicate must reject the unrelated entry (the boundary anchor case)."""
|
|
289
|
+
short_project_path = "/projects/foo"
|
|
290
|
+
trust_prefix = "Trusted local workspace:"
|
|
291
|
+
longer_unrelated_path_entry = (
|
|
292
|
+
"Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
|
|
293
|
+
)
|
|
294
|
+
assert (
|
|
295
|
+
common_module.is_trust_entry_for_project(
|
|
296
|
+
longer_unrelated_path_entry, short_project_path, trust_prefix
|
|
297
|
+
)
|
|
298
|
+
is False
|
|
299
|
+
)
|
|
300
|
+
quoted_matching_entry = (
|
|
301
|
+
f'Trusted local workspace: "{short_project_path}/.claude/**" quoted form'
|
|
302
|
+
)
|
|
303
|
+
assert (
|
|
304
|
+
common_module.is_trust_entry_for_project(
|
|
305
|
+
quoted_matching_entry, short_project_path, trust_prefix
|
|
306
|
+
)
|
|
307
|
+
is True
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def test_second_grant_is_idempotent_when_no_other_settings_changed(
|
|
312
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
313
|
+
) -> None:
|
|
314
|
+
"""Running grant twice in a row must perform zero changes the second time.
|
|
315
|
+
|
|
316
|
+
On the second call the existing trust entry is byte-identical to the
|
|
317
|
+
freshly-formatted current entry, so purge_stale_trust_entries treats it as
|
|
318
|
+
protected and does not remove it; add_auto_mode_environment_entry then
|
|
319
|
+
no-ops because the entry is already present.
|
|
320
|
+
"""
|
|
321
|
+
fake_project_root = _make_fake_project(tmp_path)
|
|
322
|
+
fake_settings_path = _prepare_fake_home(tmp_path)
|
|
323
|
+
_seed_grant_then_run(
|
|
324
|
+
fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
|
|
325
|
+
)
|
|
326
|
+
first_run_output = capsys.readouterr()
|
|
327
|
+
assert "No changes needed" not in first_run_output.out
|
|
328
|
+
_redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
|
|
329
|
+
monkeypatch.chdir(fake_project_root)
|
|
330
|
+
grant_module.grant_permissions_for_current_directory()
|
|
331
|
+
second_run_output = capsys.readouterr()
|
|
332
|
+
assert "No changes needed; settings file left untouched." in second_run_output.out
|
|
333
|
+
assert "Stale auto-mode environment entries purged" not in second_run_output.out
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
|
|
337
|
+
"""Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
|
|
338
|
+
rendered template through its derived human-readable form, and the
|
|
339
|
+
template must still expose the {project_path} placeholder for .format()
|
|
340
|
+
substitution at runtime."""
|
|
341
|
+
template_text: str = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
|
|
342
|
+
assert "{project_path}" in template_text
|
|
343
|
+
for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
|
|
344
|
+
if each_pattern.endswith("/**"):
|
|
345
|
+
directory_name = each_pattern[: -len("/**")]
|
|
346
|
+
expected_phrase = f"anything under {directory_name}/"
|
|
347
|
+
elif each_pattern == "mcp.json":
|
|
348
|
+
expected_phrase = "the mcp.json file"
|
|
349
|
+
else:
|
|
350
|
+
expected_phrase = each_pattern
|
|
351
|
+
assert expected_phrase in template_text, (
|
|
352
|
+
f"template missing derived phrase for pattern {each_pattern!r}: "
|
|
353
|
+
f"expected {expected_phrase!r}"
|
|
354
|
+
)
|
|
355
|
+
rendered_template_text = template_text.format(project_path="/tmp/x")
|
|
356
|
+
assert "/tmp/x" in rendered_template_text
|
|
@@ -356,29 +356,3 @@ def test_read_global_core_hooks_path_returns_empty_when_key_unset() -> None:
|
|
|
356
356
|
with patch.object(subprocess, "run", fake_run):
|
|
357
357
|
result = bugteam_fix_hookspath.read_global_core_hooks_path(None)
|
|
358
358
|
assert result == ""
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def test_module_import_evicts_cached_config_submodules() -> None:
|
|
362
|
-
"""Importing bugteam_fix_hookspath must evict cached `config.*` submodules.
|
|
363
|
-
|
|
364
|
-
Regression for loop1-1: without a defensive cache pop above sys.path.insert,
|
|
365
|
-
a previously-cached `config` package shadows scripts/config/ and the
|
|
366
|
-
from-import raises ModuleNotFoundError.
|
|
367
|
-
"""
|
|
368
|
-
fake_submodule_name = "config.bugteam_fix_hookspath_constants"
|
|
369
|
-
fake_parent_name = "config"
|
|
370
|
-
sentinel_module_a = ModuleType(fake_parent_name)
|
|
371
|
-
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
372
|
-
sys.modules[fake_parent_name] = sentinel_module_a
|
|
373
|
-
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
374
|
-
try:
|
|
375
|
-
_load_fix_module()
|
|
376
|
-
finally:
|
|
377
|
-
sys.modules.pop(fake_parent_name, None)
|
|
378
|
-
sys.modules.pop(fake_submodule_name, None)
|
|
379
|
-
assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
|
|
380
|
-
"parent `config` cache entry must be evicted on module import"
|
|
381
|
-
)
|
|
382
|
-
assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
|
|
383
|
-
"cached `config.<submodule>` entries must be evicted on module import"
|
|
384
|
-
)
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import importlib
|
|
2
1
|
import sys
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from types import ModuleType
|
|
5
3
|
from unittest.mock import patch
|
|
6
4
|
|
|
7
5
|
import pytest
|
|
@@ -10,14 +8,14 @@ _script_directory = str(Path(__file__).resolve().parent)
|
|
|
10
8
|
if _script_directory not in sys.path:
|
|
11
9
|
sys.path.insert(0, _script_directory)
|
|
12
10
|
|
|
13
|
-
import
|
|
14
|
-
from
|
|
11
|
+
import _bugteam_permissions_common as common_module
|
|
12
|
+
from _bugteam_permissions_common import (
|
|
15
13
|
build_permission_rule,
|
|
16
14
|
get_current_project_path,
|
|
17
15
|
path_contains_glob_metacharacters,
|
|
18
16
|
save_settings,
|
|
19
17
|
)
|
|
20
|
-
from
|
|
18
|
+
from bugteam_scripts_constants.claude_permissions_common_constants import DEFAULT_SETTINGS_FILE_MODE
|
|
21
19
|
import grant_project_claude_permissions as grant_module
|
|
22
20
|
import revoke_project_claude_permissions as revoke_module
|
|
23
21
|
|
|
@@ -140,64 +138,3 @@ def test_is_valid_project_root_exported_from_consumer_modules(
|
|
|
140
138
|
assert revoke_module.is_valid_project_root(bare_directory) is False
|
|
141
139
|
|
|
142
140
|
|
|
143
|
-
def _reload_with_stale_config_cache(module_name: str) -> ModuleType:
|
|
144
|
-
fake_submodule_name = "config.claude_permissions_common_constants"
|
|
145
|
-
fake_parent_name = "config"
|
|
146
|
-
sentinel_module_a = ModuleType(fake_parent_name)
|
|
147
|
-
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
148
|
-
sys.modules[fake_parent_name] = sentinel_module_a
|
|
149
|
-
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
150
|
-
try:
|
|
151
|
-
target_module = sys.modules.get(module_name)
|
|
152
|
-
if target_module is None:
|
|
153
|
-
target_module = importlib.import_module(module_name)
|
|
154
|
-
else:
|
|
155
|
-
target_module = importlib.reload(target_module)
|
|
156
|
-
finally:
|
|
157
|
-
sys.modules.pop(fake_parent_name, None)
|
|
158
|
-
sys.modules.pop(fake_submodule_name, None)
|
|
159
|
-
return target_module
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def test_grant_module_import_evicts_cached_config_submodules(
|
|
163
|
-
tmp_path: Path,
|
|
164
|
-
) -> None:
|
|
165
|
-
"""grant_project_claude_permissions must evict cached `config.*` on import.
|
|
166
|
-
|
|
167
|
-
Regression for loop1-2: without a defensive cache pop above sys.path.insert,
|
|
168
|
-
a cached `config` package shadows scripts/config/ and the from-import raises.
|
|
169
|
-
Calls the rebound `is_valid_project_root` to confirm the real implementation
|
|
170
|
-
survived the cache eviction (a stale shadow would either raise on import or
|
|
171
|
-
bind a placeholder that returns the wrong value).
|
|
172
|
-
"""
|
|
173
|
-
reloaded_module = _reload_with_stale_config_cache(
|
|
174
|
-
"grant_project_claude_permissions"
|
|
175
|
-
)
|
|
176
|
-
real_project_root = tmp_path / "project_root"
|
|
177
|
-
(real_project_root / ".claude").mkdir(parents=True)
|
|
178
|
-
bare_directory = tmp_path / "no_claude_marker"
|
|
179
|
-
bare_directory.mkdir()
|
|
180
|
-
assert reloaded_module.is_valid_project_root(real_project_root) is True
|
|
181
|
-
assert reloaded_module.is_valid_project_root(bare_directory) is False
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def test_revoke_module_import_evicts_cached_config_submodules(
|
|
185
|
-
tmp_path: Path,
|
|
186
|
-
) -> None:
|
|
187
|
-
"""revoke_project_claude_permissions must evict cached `config.*` on import.
|
|
188
|
-
|
|
189
|
-
Regression for loop1-3: without a defensive cache pop above sys.path.insert,
|
|
190
|
-
a cached `config` package shadows scripts/config/ and the from-import raises.
|
|
191
|
-
Calls the rebound `is_valid_project_root` to confirm the real implementation
|
|
192
|
-
survived the cache eviction (a stale shadow would either raise on import or
|
|
193
|
-
bind a placeholder that returns the wrong value).
|
|
194
|
-
"""
|
|
195
|
-
reloaded_module = _reload_with_stale_config_cache(
|
|
196
|
-
"revoke_project_claude_permissions"
|
|
197
|
-
)
|
|
198
|
-
real_project_root = tmp_path / "project_root"
|
|
199
|
-
(real_project_root / ".claude").mkdir(parents=True)
|
|
200
|
-
bare_directory = tmp_path / "no_claude_marker"
|
|
201
|
-
bare_directory.mkdir()
|
|
202
|
-
assert reloaded_module.is_valid_project_root(real_project_root) is True
|
|
203
|
-
assert reloaded_module.is_valid_project_root(bare_directory) is False
|
|
@@ -210,38 +210,13 @@ def test_should_exit_nonzero_when_subprocess_run_raises_os_error(
|
|
|
210
210
|
)
|
|
211
211
|
|
|
212
212
|
|
|
213
|
-
def test_module_import_evicts_cached_config_submodules() -> None:
|
|
214
|
-
"""Importing bugteam_preflight must evict cached `config.*` submodules.
|
|
215
|
-
|
|
216
|
-
Regression for loop1-4: a single `sys.modules.pop("config", None)` only
|
|
217
|
-
removes the parent key, leaving stale `config.<submodule>` entries that
|
|
218
|
-
satisfy the next from-import with the wrong bindings.
|
|
219
|
-
"""
|
|
220
|
-
fake_submodule_name = "config.bugteam_preflight_constants"
|
|
221
|
-
fake_parent_name = "config"
|
|
222
|
-
sentinel_module_a = ModuleType(fake_parent_name)
|
|
223
|
-
sentinel_module_b = ModuleType(fake_submodule_name)
|
|
224
|
-
sys.modules[fake_parent_name] = sentinel_module_a
|
|
225
|
-
sys.modules[fake_submodule_name] = sentinel_module_b
|
|
226
|
-
try:
|
|
227
|
-
_load_preflight_module()
|
|
228
|
-
finally:
|
|
229
|
-
sys.modules.pop(fake_parent_name, None)
|
|
230
|
-
sys.modules.pop(fake_submodule_name, None)
|
|
231
|
-
assert sys.modules.get(fake_parent_name) is not sentinel_module_a, (
|
|
232
|
-
"parent `config` cache entry must be evicted on module import"
|
|
233
|
-
)
|
|
234
|
-
assert sys.modules.get(fake_submodule_name) is not sentinel_module_b, (
|
|
235
|
-
"cached `config.<submodule>` entries must be evicted on module import"
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
|
|
239
213
|
def test_has_pytest_configuration_finds_pytest_ini(tmp_path: Path) -> None:
|
|
240
214
|
"""has_pytest_configuration must detect pytest.ini at the repo root.
|
|
241
215
|
|
|
242
216
|
Regression for loop1-17/loop1-18: the literals "pytest.ini",
|
|
243
217
|
"pyproject.toml", and "[tool.pytest" were inlined in production function
|
|
244
|
-
bodies; centralizing them in
|
|
218
|
+
bodies; centralizing them in bugteam_scripts_constants and importing here
|
|
219
|
+
pins the contract.
|
|
245
220
|
"""
|
|
246
221
|
repository_root = tmp_path / "repo"
|
|
247
222
|
repository_root.mkdir()
|
|
@@ -16,7 +16,7 @@ import stat
|
|
|
16
16
|
import sys
|
|
17
17
|
from collections.abc import Callable
|
|
18
18
|
|
|
19
|
-
from
|
|
19
|
+
from bugteam_scripts_constants.windows_safe_rmtree_constants import (
|
|
20
20
|
EXIT_CODE_REMOVE_TREE_FAILURE,
|
|
21
21
|
EXIT_CODE_USAGE_ERROR,
|
|
22
22
|
EXPECTED_ARGUMENT_COUNT,
|
package/skills/doc-gist/SKILL.md
CHANGED
|
@@ -94,6 +94,6 @@ Read the matching example for the artifact you're designing. Crib palette, typog
|
|
|
94
94
|
|
|
95
95
|
- `SKILL.md` — this file.
|
|
96
96
|
- `skills/doc-gist/scripts/gist_upload.py` — transport: HTML in, gist + preview URLs out.
|
|
97
|
-
- `skills/doc-gist/scripts/
|
|
97
|
+
- `skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py` — the URL prefixes and template strings.
|
|
98
98
|
- `references/examples/` — Thariq's 20 html-effectiveness prototypes.
|
|
99
99
|
- (PostToolUse hook lives in `packages/claude-dev-env/hooks/workflow/doc_gist_auto_publish.py` — wired into the plugin's `hooks.json`.)
|
|
@@ -30,7 +30,7 @@ _script_directory = str(Path(__file__).resolve().parent)
|
|
|
30
30
|
if _script_directory not in sys.path:
|
|
31
31
|
sys.path.insert(0, _script_directory)
|
|
32
32
|
|
|
33
|
-
from
|
|
33
|
+
from doc_gist_scripts_constants.gist_upload_constants import ( # noqa: E402
|
|
34
34
|
GIST_DEFAULT_FILENAME,
|
|
35
35
|
GIST_HOST_PREFIX,
|
|
36
36
|
MINIMUM_GIST_URL_PARTS,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: implement
|
|
3
|
+
description: "Implement a spec while maintaining a running implementation-notes.html file that captures design decisions, deviations, tradeoffs, and open questions. Triggers: /implement, implement this spec, build out this plan and keep notes."
|
|
4
|
+
argument-hint: "[path to spec file, or omit to use a spec already in context]"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# implement
|
|
8
|
+
|
|
9
|
+
Execute a spec end-to-end while keeping a sidecar `implementation-notes.html` that the user can read to see how the build diverged from or interpreted the written plan.
|
|
10
|
+
|
|
11
|
+
## Instructions
|
|
12
|
+
|
|
13
|
+
Carry out the following prompt against the spec resolved below.
|
|
14
|
+
|
|
15
|
+
### Resolve `<SPEC>`
|
|
16
|
+
|
|
17
|
+
- If `$ARGUMENTS` is non-empty, treat it as the path to the spec file and read it.
|
|
18
|
+
- Otherwise, use the most recent plan / spec / design doc already present in the conversation context.
|
|
19
|
+
- If neither is available, ask the user for the spec path via `AskUserQuestion` before proceeding.
|
|
20
|
+
|
|
21
|
+
### Prompt to execute
|
|
22
|
+
|
|
23
|
+
> Implement `<SPEC>`. As you work maintain a running `implementation-notes.html` file that captures anything I should know about how the implementation diverges from or interprets the spec, including:
|
|
24
|
+
>
|
|
25
|
+
> - **Design decisions:** choices you made where the spec was ambiguous
|
|
26
|
+
> - **Deviations:** places where you intentionally departed from the spec, and why
|
|
27
|
+
> - **Tradeoffs:** alternatives you considered and why you picked what you did
|
|
28
|
+
> - **Open questions:** anything you'd want me to confirm or revise
|
|
29
|
+
|
|
30
|
+
### How to write notes
|
|
31
|
+
|
|
32
|
+
Run `${CLAUDE_SKILL_DIR}/scripts/append_note.py` to append each entry. The script creates `implementation-notes.html` with the four sections on first run, then inserts a new `<li>` under the requested section. HTML-escapes `--about` and `--note` automatically. `${CLAUDE_SKILL_DIR}` is host-substituted by Claude Code at runtime so the bundled CLI is found regardless of the current working directory.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
python "${CLAUDE_SKILL_DIR}/scripts/append_note.py" \
|
|
36
|
+
--section decisions \
|
|
37
|
+
--about "Storage location" \
|
|
38
|
+
--note "Wrote notes next to the spec because the spec path was provided." \
|
|
39
|
+
--file /path/to/spec-dir/implementation-notes.html
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`--section` choices (slug → heading):
|
|
43
|
+
|
|
44
|
+
| Slug | Heading |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `decisions` | Design decisions |
|
|
47
|
+
| `deviations` | Deviations |
|
|
48
|
+
| `tradeoffs` | Tradeoffs |
|
|
49
|
+
| `questions` | Open questions |
|
|
50
|
+
|
|
51
|
+
`--file` is optional. When omitted, the script writes to `./implementation-notes.html` in the current working directory. When a spec path is known, pass `--file` so notes land next to the spec rather than in CWD.
|
|
52
|
+
|
|
53
|
+
Append entries as decisions are made — do not batch them until the end.
|
|
54
|
+
|
|
55
|
+
## Gotchas
|
|
56
|
+
|
|
57
|
+
- **Do not hand-edit `implementation-notes.html`.** The append script locates each section by its `<section id="...">` marker and the first `</ul>` after it. Editing the structure breaks subsequent appends; the script raises a `RuntimeError` naming the missing marker.
|
|
58
|
+
- **`--about` and `--note` are HTML-escaped automatically** — pass raw text, not pre-escaped HTML.
|
|
59
|
+
|
|
60
|
+
## File index
|
|
61
|
+
|
|
62
|
+
| File | Purpose |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `SKILL.md` | This hub |
|
|
65
|
+
| `packages/claude-dev-env/skills/implement/scripts/append_note.py` | CLI to append one entry to a section |
|
|
66
|
+
| `packages/claude-dev-env/skills/implement/scripts/implement_scripts_constants/notes_constants.py` | Section slugs → headings and default filename |
|