claude-dev-env 1.40.0 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CLAUDE.md +1 -1
  2. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +53 -3
  3. package/_shared/pr-loop/scripts/post_audit_thread.py +2 -2
  4. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +68 -3
  5. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +1 -1
  6. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
  7. package/hooks/_gh_pr_author_swap_utils.py +1211 -0
  8. package/hooks/blocking/gh_body_arg_blocker.py +9 -6
  9. package/hooks/blocking/gh_pr_author_enforcer.py +480 -0
  10. package/hooks/blocking/gh_pr_author_restore.py +100 -0
  11. package/hooks/blocking/pr_converge_bugteam_enforcer.py +170 -0
  12. package/hooks/blocking/pr_description_enforcer.py +1 -3
  13. package/hooks/blocking/test_gh_body_arg_blocker.py +25 -3
  14. package/hooks/blocking/test_gh_pr_author_enforcer.py +1166 -0
  15. package/hooks/blocking/test_gh_pr_author_restore.py +512 -0
  16. package/hooks/blocking/test_gh_pr_author_swap_utils.py +910 -0
  17. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +311 -0
  18. package/hooks/config/gh_pr_author_swap_constants.py +76 -0
  19. package/hooks/config/pr_converge_bugteam_enforcer_constants.py +55 -0
  20. package/hooks/config/pr_converge_bugteam_enforcer_state.py +67 -0
  21. package/hooks/config/pr_description_enforcer_constants.py +5 -0
  22. package/hooks/config/test_pr_description_enforcer_constants.py +82 -0
  23. package/hooks/hooks.json +40 -0
  24. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +204 -0
  25. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +283 -0
  26. package/hooks/session/gh_pr_author_session_cleanup.py +171 -0
  27. package/hooks/session/test_gh_pr_author_session_cleanup.py +575 -0
  28. package/hooks/test__gh_pr_author_swap_utils.py +333 -0
  29. package/package.json +1 -1
  30. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  31. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  32. package/skills/bugteam/reference/audit-contract.md +22 -0
  33. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  34. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +8 -2
  35. package/skills/bugteam/scripts/test__claude_permissions_common.py +48 -0
  36. package/skills/bugteam/scripts/test_claude_permissions_common.py +18 -10
  37. package/skills/pr-converge/SKILL.md +8 -2
  38. package/skills/pr-converge/config/constants.py +2 -1
  39. package/skills/pr-converge/reference/state-schema.md +36 -8
package/CLAUDE.md CHANGED
@@ -5,7 +5,7 @@ The user delegates execution to you and expects zero manual steps unless strictl
5
5
  ## Code Rules
6
6
  @~/.claude/docs/CODE_RULES.md
7
7
 
8
- When a UNC path is mapped to a drive letter, ALWAYS prefer the drive letter and NEVER use UNC.
8
+ ALWAYS call the AskUserQuestion tool if you have a question for the user. Provide content-appropriate default options, with a flag for the recommended one.
9
9
 
10
10
  ## GOTCHAS
11
11
  When making code changes, make sure you are working in the proper worktree path for the task at hand.
@@ -9,9 +9,22 @@ the changes applied. No-op when the entries already exist.
9
9
  import sys
10
10
  from pathlib import Path
11
11
 
12
- sys.modules.pop("config", None)
13
- if str(Path(__file__).resolve().parent) not in sys.path:
14
- sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+ parent_directory = str(Path(__file__).absolute().parent)
13
+ try:
14
+ sys.path.remove(parent_directory)
15
+ except ValueError:
16
+ pass
17
+ if parent_directory not in sys.path:
18
+ sys.path.insert(0, parent_directory)
19
+
20
+ for each_cached_module_name in [
21
+ each_module_key
22
+ for each_module_key in list(sys.modules)
23
+ if each_module_key == "config"
24
+ or each_module_key.startswith("config.")
25
+ or each_module_key == "_claude_permissions_common"
26
+ ]:
27
+ sys.modules.pop(each_cached_module_name, None)
15
28
 
16
29
  from _claude_permissions_common import ( # noqa: E402
17
30
  append_if_missing,
@@ -41,6 +54,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
41
54
  def add_rules_to_allow_list(
42
55
  all_settings: dict[str, object], all_rules_to_add: list[str]
43
56
  ) -> int:
57
+ """Add permission rules to the settings allow list.
58
+
59
+ Args:
60
+ all_settings: The parsed settings dictionary.
61
+ all_rules_to_add: Permission rule strings to append.
62
+
63
+ Returns:
64
+ Number of rules actually added (new entries).
65
+ """
44
66
  permissions_section = ensure_dict_section(
45
67
  all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
46
68
  )
@@ -57,6 +79,15 @@ def add_rules_to_allow_list(
57
79
  def add_directory_to_additional_directories(
58
80
  all_settings: dict[str, object], directory_path: str
59
81
  ) -> int:
82
+ """Add a project path to the additionalDirectories allow list.
83
+
84
+ Args:
85
+ all_settings: The parsed settings dictionary.
86
+ directory_path: The project directory path to add.
87
+
88
+ Returns:
89
+ 1 when the entry was added, 0 when it already existed.
90
+ """
60
91
  permissions_section = ensure_dict_section(
61
92
  all_settings, CLAUDE_SETTINGS_PERMISSIONS_KEY
62
93
  )
@@ -71,6 +102,15 @@ def add_directory_to_additional_directories(
71
102
  def add_auto_mode_environment_entry(
72
103
  all_settings: dict[str, object], entry_text: str
73
104
  ) -> int:
105
+ """Add an auto-mode environment entry for the project.
106
+
107
+ Args:
108
+ all_settings: The parsed settings dictionary.
109
+ entry_text: The environment entry text to add.
110
+
111
+ Returns:
112
+ 1 when the entry was added, 0 when it already existed.
113
+ """
74
114
  auto_mode_section = ensure_dict_section(
75
115
  all_settings, CLAUDE_SETTINGS_AUTO_MODE_KEY
76
116
  )
@@ -83,6 +123,16 @@ def add_auto_mode_environment_entry(
83
123
 
84
124
 
85
125
  def grant_permissions_for_current_directory() -> None:
126
+ """Grant Edit/Write/Read permissions for the current project directory.
127
+
128
+ Reads the current project path, constructs permission rules from config
129
+ constants, and writes them to ~/.claude/settings.json atomically.
130
+
131
+ Raises:
132
+ SystemExit: When the current directory is not a valid project root.
133
+ ValueError: Propagated from get_current_project_path() when the path
134
+ contains glob metacharacters.
135
+ """
86
136
  claude_user_settings_path: Path = get_claude_user_settings_path()
87
137
  project_root_path = Path.cwd()
88
138
  if not is_valid_project_root(project_root_path):
@@ -33,8 +33,8 @@ from pathlib import Path
33
33
  from typing import NoReturn
34
34
 
35
35
  sys.modules.pop("config", None)
36
- if str(Path(__file__).resolve().parent) not in sys.path:
37
- sys.path.insert(0, str(Path(__file__).resolve().parent))
36
+ if str(Path(__file__).absolute().parent) not in sys.path:
37
+ sys.path.insert(0, str(Path(__file__).absolute().parent))
38
38
 
39
39
  from config.post_audit_thread_constants import (
40
40
  ALL_GH_API_COMMAND_PARTS,
@@ -10,9 +10,22 @@ autoMode sections so repeated grant/revoke cycles leave no dead structure.
10
10
  import sys
11
11
  from pathlib import Path
12
12
 
13
- sys.modules.pop("config", None)
14
- if str(Path(__file__).resolve().parent) not in sys.path:
15
- sys.path.insert(0, str(Path(__file__).resolve().parent))
13
+ parent_directory = str(Path(__file__).absolute().parent)
14
+ try:
15
+ sys.path.remove(parent_directory)
16
+ except ValueError:
17
+ pass
18
+ if parent_directory not in sys.path:
19
+ sys.path.insert(0, parent_directory)
20
+
21
+ for each_cached_module_name in [
22
+ each_module_key
23
+ for each_module_key in list(sys.modules)
24
+ if each_module_key == "config"
25
+ or each_module_key.startswith("config.")
26
+ or each_module_key == "_claude_permissions_common"
27
+ ]:
28
+ sys.modules.pop(each_cached_module_name, None)
16
29
 
17
30
  from _claude_permissions_common import ( # noqa: E402
18
31
  build_permission_rules,
@@ -40,6 +53,15 @@ from config.claude_settings_keys_constants import ( # noqa: E402
40
53
  def remove_values_from_list(
41
54
  all_target_list: list[object], all_values_to_remove: set[str]
42
55
  ) -> int:
56
+ """Remove matching values from a list in place.
57
+
58
+ Args:
59
+ all_target_list: The list to remove values from.
60
+ all_values_to_remove: Set of string values to remove.
61
+
62
+ Returns:
63
+ Number of values removed.
64
+ """
43
65
  original_length = len(all_target_list)
44
66
  all_target_list[:] = [
45
67
  each_value
@@ -52,6 +74,15 @@ def remove_values_from_list(
52
74
  def remove_rules_from_allow_list(
53
75
  all_settings: dict[str, object], all_rules_to_remove: list[str]
54
76
  ) -> int:
77
+ """Remove matching permission rules from the settings allow list.
78
+
79
+ Args:
80
+ all_settings: The parsed settings dictionary.
81
+ all_rules_to_remove: Permission rule strings to remove.
82
+
83
+ Returns:
84
+ Number of rules removed.
85
+ """
55
86
  permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
56
87
  if not isinstance(permissions_section, dict):
57
88
  return 0
@@ -64,6 +95,15 @@ def remove_rules_from_allow_list(
64
95
  def remove_directory_from_additional_directories(
65
96
  all_settings: dict[str, object], directory_path: str
66
97
  ) -> int:
98
+ """Remove a project path from the additionalDirectories list.
99
+
100
+ Args:
101
+ all_settings: The parsed settings dictionary.
102
+ directory_path: The project directory path to remove.
103
+
104
+ Returns:
105
+ 1 when the entry was removed, 0 when not found.
106
+ """
67
107
  permissions_section = all_settings.get(CLAUDE_SETTINGS_PERMISSIONS_KEY)
68
108
  if not isinstance(permissions_section, dict):
69
109
  return 0
@@ -78,6 +118,15 @@ def remove_directory_from_additional_directories(
78
118
  def remove_auto_mode_environment_entry(
79
119
  all_settings: dict[str, object], entry_text: str
80
120
  ) -> int:
121
+ """Remove an auto-mode environment entry for the project.
122
+
123
+ Args:
124
+ all_settings: The parsed settings dictionary.
125
+ entry_text: The environment entry text to remove.
126
+
127
+ Returns:
128
+ 1 when the entry was removed, 0 when not found.
129
+ """
81
130
  auto_mode_section = all_settings.get(CLAUDE_SETTINGS_AUTO_MODE_KEY)
82
131
  if not isinstance(auto_mode_section, dict):
83
132
  return 0
@@ -88,6 +137,11 @@ def remove_auto_mode_environment_entry(
88
137
 
89
138
 
90
139
  def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
140
+ """Remove empty lists and their parent sections after revoking entries.
141
+
142
+ Args:
143
+ all_settings: The parsed settings dictionary to prune in place.
144
+ """
91
145
  prune_empty_list_then_empty_section(
92
146
  all_settings,
93
147
  CLAUDE_SETTINGS_PERMISSIONS_KEY,
@@ -106,6 +160,17 @@ def prune_settings_after_revoke(all_settings: dict[str, object]) -> None:
106
160
 
107
161
 
108
162
  def revoke_permissions_for_current_directory() -> None:
163
+ """Revoke permissions previously granted for the current project directory.
164
+
165
+ Reads the current project path, constructs the matching permission rules,
166
+ removes them from ~/.claude/settings.json, and prunes any newly empty
167
+ sections.
168
+
169
+ Raises:
170
+ SystemExit: When the current directory is not a valid project root.
171
+ ValueError: Propagated from get_current_project_path() when the path
172
+ contains glob metacharacters.
173
+ """
109
174
  claude_user_settings_path: Path = get_claude_user_settings_path()
110
175
  project_root_path = Path.cwd()
111
176
  if not is_valid_project_root(project_root_path):
@@ -43,7 +43,7 @@ def test_grant_module_guards_sys_path_insert_against_duplicates() -> None:
43
43
  module_source = (
44
44
  Path(__file__).parent.parent / "grant_project_claude_permissions.py"
45
45
  ).read_text(encoding="utf-8")
46
- assert "if str(Path(__file__).resolve().parent) not in sys.path:" in module_source, (
46
+ assert "if parent_directory not in sys.path:" in module_source, (
47
47
  "grant_project_claude_permissions.py must guard sys.path.insert against "
48
48
  "duplicate entries on reload (consistent with sibling modules)"
49
49
  )
@@ -43,7 +43,7 @@ def test_revoke_module_guards_sys_path_insert_against_duplicates() -> None:
43
43
  module_source = (
44
44
  Path(__file__).parent.parent / "revoke_project_claude_permissions.py"
45
45
  ).read_text(encoding="utf-8")
46
- assert "if str(Path(__file__).resolve().parent) not in sys.path:" in module_source, (
46
+ assert "if parent_directory not in sys.path:" in module_source, (
47
47
  "revoke_project_claude_permissions.py must guard sys.path.insert against "
48
48
  "duplicate entries on reload (consistent with sibling modules)"
49
49
  )