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.
Files changed (214) hide show
  1. package/CLAUDE.md +8 -0
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +232 -8
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +293 -8
  4. package/_shared/pr-loop/scripts/fix_hookspath.py +96 -5
  5. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +124 -20
  6. package/_shared/pr-loop/scripts/post_audit_thread.py +4 -4
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +90 -0
  8. package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/claude_settings_keys_constants.py +2 -0
  9. package/_shared/pr-loop/scripts/preflight.py +13 -31
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -16
  11. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +76 -33
  12. package/_shared/pr-loop/scripts/tests/conftest.py +1 -51
  13. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +385 -0
  14. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +4 -2
  15. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +37 -2
  16. package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +4 -2
  17. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +4 -2
  18. package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +6 -2
  19. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +2 -2
  20. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1 -2
  21. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +4 -2
  22. package/_shared/pr-loop/scripts/tests/test_preflight.py +17 -52
  23. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +6 -2
  24. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +5 -3
  25. package/agents/pr-description-writer.md +50 -140
  26. package/docs/PR_DESCRIPTION_GUIDE.md +101 -102
  27. package/hooks/_gh_pr_author_swap_utils.py +1 -1
  28. package/hooks/blocking/bot_mention_comment_blocker.py +4 -10
  29. package/hooks/blocking/code_rules_enforcer.py +217 -99
  30. package/hooks/blocking/code_rules_path_utils.py +8 -1
  31. package/hooks/blocking/destructive_command_blocker.py +1 -1
  32. package/hooks/blocking/es_exe_path_rewriter.py +7 -13
  33. package/hooks/blocking/gh_body_arg_blocker.py +6 -1
  34. package/hooks/blocking/gh_pr_author_enforcer.py +5 -5
  35. package/hooks/blocking/gh_pr_author_restore.py +5 -5
  36. package/hooks/blocking/hedging_language_blocker.py +4 -10
  37. package/hooks/blocking/md_path_exemptions.py +205 -0
  38. package/hooks/blocking/md_to_html_blocker.py +48 -20
  39. package/hooks/blocking/pr_converge_bugteam_enforcer.py +5 -11
  40. package/hooks/blocking/pr_description_enforcer.py +626 -41
  41. package/hooks/blocking/question_to_user_enforcer.py +4 -10
  42. package/hooks/blocking/state_description_blocker.py +6 -12
  43. package/hooks/blocking/tdd_enforcer.py +1 -1
  44. package/hooks/blocking/test_bot_mention_comment_blocker.py +1 -1
  45. package/hooks/blocking/test_code_rules_enforcer.py +3 -3
  46. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +1 -1
  47. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -2
  48. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +184 -0
  49. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +82 -0
  50. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +29 -29
  51. package/hooks/blocking/test_gh_body_arg_blocker.py +7 -8
  52. package/hooks/blocking/test_gh_pr_author_enforcer.py +1 -1
  53. package/hooks/blocking/test_gh_pr_author_restore.py +1 -1
  54. package/hooks/blocking/test_hedging_language_blocker.py +2 -2
  55. package/hooks/blocking/test_md_to_html_blocker.py +463 -8
  56. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +1 -1
  57. package/hooks/blocking/test_pr_description_enforcer.py +1210 -13
  58. package/hooks/blocking/test_question_to_user_enforcer.py +1 -1
  59. package/hooks/blocking/windows_rmtree_blocker.py +5 -11
  60. package/hooks/diagnostic/hook_log_extractor.py +1 -1
  61. package/hooks/diagnostic/hook_log_init.py +1 -1
  62. package/hooks/diagnostic/hook_log_stop_wrapper.py +1 -1
  63. package/hooks/diagnostic/test_hook_log_extractor.py +1 -1
  64. package/hooks/diagnostic/test_hook_log_init.py +2 -2
  65. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +1 -1
  66. package/hooks/git-hooks/gate_utils.py +1 -1
  67. package/hooks/git-hooks/pre_commit.py +1 -1
  68. package/hooks/git-hooks/pre_push.py +1 -1
  69. package/hooks/git-hooks/test_config.py +5 -5
  70. package/hooks/git-hooks/test_pre_push.py +6 -6
  71. package/hooks/{config → hooks_constants}/code_rules_enforcer_constants.py +37 -0
  72. package/hooks/hooks_constants/code_rules_path_utils_constants.py +28 -0
  73. package/hooks/hooks_constants/md_to_html_blocker_constants.py +82 -0
  74. package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_state.py +1 -1
  75. package/hooks/hooks_constants/pr_description_enforcer_constants.py +154 -0
  76. package/hooks/{config → hooks_constants}/pre_tool_use_stdin.py +1 -1
  77. package/hooks/{config → hooks_constants}/project_paths_reader.py +2 -2
  78. package/hooks/{config → hooks_constants}/test_banned_identifiers_constants.py +1 -1
  79. package/hooks/{config → hooks_constants}/test_dynamic_stderr_handler.py +1 -1
  80. package/hooks/{config → hooks_constants}/test_hardcoded_user_path_constants.py +1 -1
  81. package/hooks/{config → hooks_constants}/test_hook_log_extractor_constants.py +2 -2
  82. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +110 -0
  83. package/hooks/{config → hooks_constants}/test_messages.py +2 -6
  84. package/hooks/{config → hooks_constants}/test_path_rewriter_constants.py +1 -1
  85. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +292 -0
  86. package/hooks/{config → hooks_constants}/test_pre_tool_use_stdin.py +2 -2
  87. package/hooks/{config → hooks_constants}/test_project_paths_reader.py +3 -3
  88. package/hooks/{config → hooks_constants}/test_session_env_cleanup_constants.py +1 -1
  89. package/hooks/{config → hooks_constants}/test_setup_project_paths_constants.py +2 -2
  90. package/hooks/{config → hooks_constants}/test_unused_module_import_constants.py +1 -1
  91. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +5 -11
  92. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +1 -1
  93. package/hooks/session/gh_pr_author_session_cleanup.py +5 -6
  94. package/hooks/session/session_env_cleanup.py +4 -10
  95. package/hooks/session/test_gh_pr_author_session_cleanup.py +1 -1
  96. package/hooks/session/test_untracked_repo_detector.py +2 -2
  97. package/hooks/session/untracked_repo_detector.py +6 -12
  98. package/hooks/test__gh_pr_author_swap_utils.py +1 -1
  99. package/hooks/validators/run_all_validators.py +16 -5
  100. package/hooks/validators/test_output_formatter.py +46 -0
  101. package/hooks/workflow/doc_gist_auto_publish.py +1 -1
  102. package/hooks/workflow/md_to_html_companion.py +8 -15
  103. package/hooks/workflow/test_md_to_html_companion.py +184 -23
  104. package/package.json +1 -1
  105. package/rules/ask-user-question-required.md +1 -1
  106. package/rules/vault-context.md +1 -1
  107. package/scripts/{config → dev_env_scripts_constants}/timing.py +1 -1
  108. package/scripts/setup_project_paths.py +49 -11
  109. package/scripts/sweep_empty_dirs.py +10 -1
  110. package/scripts/test_setup_project_paths.py +2 -2
  111. package/scripts/test_sweep_empty_dirs.py +2 -6
  112. package/skills/_shared/pr-loop/scripts/_path_resolver.py +1 -1
  113. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  114. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  115. package/skills/_shared/pr-loop/scripts/init_loop_state.py +1 -1
  116. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +1 -1
  117. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +2 -2
  118. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +2 -2
  119. package/skills/bugteam/PROMPTS.md +1 -1
  120. package/skills/bugteam/SKILL.md +1 -1
  121. package/skills/bugteam/reference/github-pr-reviews.md +1 -1
  122. package/skills/bugteam/scripts/{_claude_permissions_common.py → _bugteam_permissions_common.py} +110 -13
  123. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +1 -13
  124. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +1 -16
  125. package/skills/bugteam/scripts/bugteam_preflight.py +1 -13
  126. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +69 -0
  127. package/skills/bugteam/scripts/grant_project_claude_permissions.py +117 -12
  128. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +1 -1
  129. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  130. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +71 -25
  131. package/skills/bugteam/scripts/{test__claude_permissions_common.py → test__bugteam_permissions_common.py} +4 -4
  132. package/skills/bugteam/scripts/test_agent_config_carveout.py +356 -0
  133. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -26
  134. package/skills/bugteam/scripts/{test_claude_permissions_common.py → test_bugteam_permissions_common.py} +3 -66
  135. package/skills/bugteam/scripts/test_bugteam_preflight.py +2 -27
  136. package/skills/bugteam/scripts/windows_safe_rmtree.py +1 -1
  137. package/skills/doc-gist/SKILL.md +1 -1
  138. package/skills/doc-gist/scripts/gist_upload.py +1 -1
  139. package/skills/implement/SKILL.md +66 -0
  140. package/skills/implement/scripts/append_note.py +133 -0
  141. package/skills/implement/scripts/implement_scripts_constants/__init__.py +0 -0
  142. package/skills/implement/scripts/implement_scripts_constants/notes_constants.py +12 -0
  143. package/skills/implement/scripts/test_append_note.py +191 -0
  144. package/skills/pr-converge/pr_converge_skill_constants/__init__.py +0 -0
  145. package/skills/pr-converge/{config → pr_converge_skill_constants}/constants.py +6 -1
  146. package/skills/pr-converge/scripts/check_bugbot_ci.py +2 -2
  147. package/skills/pr-converge/scripts/check_convergence.py +175 -29
  148. package/skills/pr-converge/scripts/check_pending_reviews.py +2 -2
  149. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +2 -2
  150. package/skills/pr-converge/scripts/post_fix_reply.py +2 -2
  151. package/skills/pr-converge/scripts/pr_converge_scripts_constants/__init__.py +0 -0
  152. package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/pr_converge_constants.py +1 -1
  153. package/skills/pr-converge/scripts/reflow_skill_md.py +90 -16
  154. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +1 -1
  155. package/skills/pr-converge/scripts/test_check_convergence.py +324 -0
  156. package/skills/pr-converge/scripts/test_reflow_skill_md.py +0 -31
  157. package/skills/refine/SKILL.md +257 -0
  158. package/skills/refine/templates/implementation-notes-template.html +56 -0
  159. package/skills/refine/templates/plan-template.md +60 -0
  160. package/skills/session-log/SKILL.md +98 -233
  161. package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +0 -36
  162. package/hooks/config/pr_description_enforcer_constants.py +0 -19
  163. package/hooks/config/test_pr_description_enforcer_constants.py +0 -82
  164. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +0 -20
  165. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +0 -55
  166. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +0 -55
  167. package/skills/pr-converge/scripts/evict_cached_config_modules.py +0 -20
  168. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +0 -22
  169. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/__init__.py +0 -0
  170. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/code_rules_gate_constants.py +0 -0
  171. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/fix_hookspath_constants.py +0 -0
  172. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/post_audit_thread_constants.py +0 -0
  173. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/preflight_constants.py +0 -0
  174. /package/_shared/pr-loop/scripts/{config → pr_loop_shared_constants}/reviews_disabled_constants.py +0 -0
  175. /package/hooks/git-hooks/{config.py → git_hooks_constants/__init__.py} +0 -0
  176. /package/hooks/{config → hooks_constants}/__init__.py +0 -0
  177. /package/hooks/{config → hooks_constants}/any_type_config.py +0 -0
  178. /package/hooks/{config → hooks_constants}/banned_identifiers_constants.py +0 -0
  179. /package/hooks/{config → hooks_constants}/blocking_check_limits.py +0 -0
  180. /package/hooks/{config → hooks_constants}/bot_mention_comment_blocker_constants.py +0 -0
  181. /package/hooks/{config → hooks_constants}/convergence_branch_constants.py +0 -0
  182. /package/hooks/{config → hooks_constants}/doc_gist_auto_publish_constants.py +0 -0
  183. /package/hooks/{config → hooks_constants}/dynamic_stderr_handler.py +0 -0
  184. /package/hooks/{config → hooks_constants}/gh_pr_author_swap_constants.py +0 -0
  185. /package/hooks/{config → hooks_constants}/hardcoded_user_path_constants.py +0 -0
  186. /package/hooks/{config → hooks_constants}/hook_log_extractor_constants.py +0 -0
  187. /package/hooks/{config → hooks_constants}/html_companion_constants.py +0 -0
  188. /package/hooks/{config → hooks_constants}/inline_tuple_string_magic_constants.py +0 -0
  189. /package/hooks/{config → hooks_constants}/messages.py +0 -0
  190. /package/hooks/{config → hooks_constants}/path_rewriter_constants.py +0 -0
  191. /package/hooks/{config → hooks_constants}/pr_converge_bugteam_enforcer_constants.py +0 -0
  192. /package/hooks/{config → hooks_constants}/session_env_cleanup_constants.py +0 -0
  193. /package/hooks/{config → hooks_constants}/setup_project_paths_constants.py +0 -0
  194. /package/hooks/{config → hooks_constants}/state_description_blocker_constants.py +0 -0
  195. /package/hooks/{config → hooks_constants}/stuttering_check_config.py +0 -0
  196. /package/hooks/{config → hooks_constants}/stuttering_import_binding_constants.py +0 -0
  197. /package/hooks/{config → hooks_constants}/sys_path_insert_constants.py +0 -0
  198. /package/hooks/{config → hooks_constants}/unused_module_import_constants.py +0 -0
  199. /package/hooks/{config → hooks_constants}/windows_rmtree_blocker_constants.py +0 -0
  200. /package/{skills/_shared/pr-loop/scripts/config → hooks/lifecycle}/__init__.py +0 -0
  201. /package/{skills/bugteam/scripts/config → hooks/session}/__init__.py +0 -0
  202. /package/scripts/{config → dev_env_scripts_constants}/__init__.py +0 -0
  203. /package/skills/{doc-gist/scripts/config → _shared/pr-loop/scripts/skills_pr_loop_constants}/__init__.py +0 -0
  204. /package/skills/_shared/pr-loop/scripts/{config → skills_pr_loop_constants}/path_resolver_constants.py +0 -0
  205. /package/skills/{pr-converge/config → bugteam/scripts/bugteam_scripts_constants}/__init__.py +0 -0
  206. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_code_rules_gate_constants.py +0 -0
  207. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_fix_hookspath_constants.py +0 -0
  208. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/bugteam_preflight_constants.py +0 -0
  209. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/probe_code_rules_enforcer_check_constants.py +0 -0
  210. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/reflow_skill_md_constants.py +0 -0
  211. /package/skills/bugteam/scripts/{config → bugteam_scripts_constants}/windows_safe_rmtree_constants.py +0 -0
  212. /package/skills/{pr-converge/scripts/config → doc-gist/scripts/doc_gist_scripts_constants}/__init__.py +0 -0
  213. /package/skills/doc-gist/scripts/{config → doc_gist_scripts_constants}/gist_upload_constants.py +0 -0
  214. /package/skills/pr-converge/scripts/{config → pr_converge_scripts_constants}/reflow_skill_md_constants.py +0 -0
package/CLAUDE.md CHANGED
@@ -23,6 +23,14 @@ When writing tests, always write tests that actually test the behavior of the fu
23
23
 
24
24
  When writing tests, always ensure you utilize the production code paths instead of duplicating explicitly for the test.
25
25
 
26
+ ## Research via Subagents
27
+
28
+ Delegate exploration whose raw content you won't directly edit or reuse. If you'd `Read` more than one file or `Grep` more than one pattern just to extract a fact, dispatch an `Explore` subagent.
29
+
30
+ Ask the subagent for a specific answer: "return the file:line where X is defined." For multiple unrelated questions, fan out parallel subagents — issue several `Agent` calls in a single response.
31
+
32
+ Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compose subagent prompts via the protocol in `agent-spawn-protocol`.
33
+
26
34
  ## Additional Non-overlapping Rules
27
35
 
28
36
  - **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
@@ -13,14 +13,12 @@ import os
13
13
  import secrets
14
14
  import stat
15
15
  import sys
16
+ from collections.abc import Callable
16
17
  from pathlib import Path
17
18
  from typing import NoReturn
18
19
 
19
- sys.modules.pop("config", None)
20
- if str(Path(__file__).resolve().parent) not in sys.path:
21
- sys.path.insert(0, str(Path(__file__).resolve().parent))
22
-
23
- from config.claude_permissions_constants import (
20
+ from pr_loop_shared_constants.claude_permissions_constants import (
21
+ ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
24
22
  CLAUDE_SETTINGS_DIRECTORY_NAME,
25
23
  GIT_DIRECTORY_NAME,
26
24
  TEXT_FILE_ENCODING as TEXT_FILE_ENCODING,
@@ -29,11 +27,27 @@ from config.claude_permissions_constants import (
29
27
 
30
28
 
31
29
  def exit_with_error(message: str) -> NoReturn:
30
+ """Print an error message to stderr and terminate the process.
31
+
32
+ Args:
33
+ message: The error message to print to stderr.
34
+
35
+ Raises:
36
+ SystemExit: Always raised with a non-zero exit code.
37
+ """
32
38
  print(f"Error: {message}", file=sys.stderr)
33
39
  raise SystemExit(1)
34
40
 
35
41
 
36
42
  def path_contains_glob_metacharacters(candidate_path: str) -> bool:
43
+ """Check whether a path contains characters reserved for glob patterns.
44
+
45
+ Args:
46
+ candidate_path: The file path string to inspect.
47
+
48
+ Returns:
49
+ True when any glob metacharacter is present in the path.
50
+ """
37
51
  all_glob_metacharacters_in_path: tuple[str, ...] = (
38
52
  "*",
39
53
  "?",
@@ -49,6 +63,14 @@ def path_contains_glob_metacharacters(candidate_path: str) -> bool:
49
63
 
50
64
 
51
65
  def get_current_project_path() -> str:
66
+ """Return the normalized current working directory path.
67
+
68
+ Returns:
69
+ The cwd as a POSIX-style path string.
70
+
71
+ Raises:
72
+ ValueError: When the path contains glob metacharacters.
73
+ """
52
74
  normalized_project_path = str(Path.cwd()).replace("\\", "/")
53
75
  if path_contains_glob_metacharacters(normalized_project_path):
54
76
  raise ValueError(
@@ -65,13 +87,142 @@ def build_permission_rule(tool_name: str, project_path: str) -> str:
65
87
  def build_permission_rules(
66
88
  project_path: str, all_permission_allow_tools: tuple[str, ...]
67
89
  ) -> list[str]:
90
+ """Construct permission rule strings for each tool.
91
+
92
+ Args:
93
+ project_path: The POSIX-style project root path.
94
+ all_permission_allow_tools: Tool names to build rules for.
95
+
96
+ Returns:
97
+ List of permission rule strings for the given project path.
98
+ """
68
99
  return [
69
100
  build_permission_rule(each_tool, project_path)
70
101
  for each_tool in all_permission_allow_tools
71
102
  ]
72
103
 
73
104
 
105
+ def build_agent_config_deny_rule(
106
+ tool_name: str, project_path: str, agent_config_path_pattern: str
107
+ ) -> str:
108
+ """Construct a deny rule for a single agent-config path pattern.
109
+
110
+ Args:
111
+ tool_name: The permission tool name (e.g., "Edit", "Write", "Read").
112
+ project_path: The POSIX-style project root path.
113
+ agent_config_path_pattern: The agent-config path pattern under .claude/.
114
+
115
+ Returns:
116
+ The deny rule string Claude Code matches against tool invocations.
117
+ """
118
+ return f"{tool_name}({project_path}/.claude/{agent_config_path_pattern})"
119
+
120
+
121
+ def build_agent_config_deny_rules(
122
+ project_path: str,
123
+ all_permission_allow_tools: tuple[str, ...],
124
+ all_agent_config_path_patterns: tuple[str, ...],
125
+ ) -> list[str]:
126
+ """Construct deny rules covering every tool and pattern pair.
127
+
128
+ Args:
129
+ project_path: The POSIX-style project root path.
130
+ all_permission_allow_tools: Tool names to build deny rules for.
131
+ all_agent_config_path_patterns: Agent-config path patterns to deny under .claude/.
132
+
133
+ Returns:
134
+ List of deny rule strings, one per tool/pattern combination.
135
+ """
136
+ return [
137
+ build_agent_config_deny_rule(each_tool, project_path, each_pattern)
138
+ for each_tool in all_permission_allow_tools
139
+ for each_pattern in all_agent_config_path_patterns
140
+ ]
141
+
142
+
143
+ def _is_project_path_token_at_word_boundary(
144
+ body_after_prefix: str, token_position: int
145
+ ) -> bool:
146
+ if token_position == 0:
147
+ return True
148
+ preceding_character = body_after_prefix[token_position - 1]
149
+ if preceding_character.isspace():
150
+ return True
151
+ return preceding_character in ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS
152
+
153
+
154
+ def is_trust_entry_for_project(
155
+ candidate_entry: object, project_path: str, prefix: str
156
+ ) -> bool:
157
+ """Detect whether an autoMode.environment entry is a trust entry for the project.
158
+
159
+ The predicate matches any string entry whose prefix matches the trust-entry
160
+ marker and that contains the project's .claude/** path token anchored on a
161
+ non-path boundary (the start of the body after the prefix, a whitespace
162
+ character, or a quote character). The boundary anchor prevents
163
+ cross-project false positives where the current project's path is a path
164
+ suffix of an unrelated entry's path. The exact wording after the prefix is
165
+ allowed to vary between template revisions.
166
+
167
+ Args:
168
+ candidate_entry: The autoMode.environment list value to inspect.
169
+ project_path: The POSIX-style project root path.
170
+ prefix: The literal prefix that marks a trust entry.
171
+
172
+ Returns:
173
+ True when the entry is a prior trust entry for this project.
174
+ """
175
+ if not isinstance(candidate_entry, str):
176
+ return False
177
+ if not candidate_entry.startswith(prefix):
178
+ return False
179
+ project_path_token = f"{project_path}/.claude/**"
180
+ body_after_prefix = candidate_entry[len(prefix):]
181
+ token_position = body_after_prefix.find(project_path_token)
182
+ while token_position != -1:
183
+ if _is_project_path_token_at_word_boundary(body_after_prefix, token_position):
184
+ return True
185
+ next_search_start = token_position + 1
186
+ token_position = body_after_prefix.find(project_path_token, next_search_start)
187
+ return False
188
+
189
+
190
+ def remove_matching_entries_from_list(
191
+ all_target_list: list[object],
192
+ match_predicate: Callable[[object], bool],
193
+ ) -> int:
194
+ """Remove every entry from a list that satisfies the predicate.
195
+
196
+ Args:
197
+ all_target_list: The list to filter in place.
198
+ match_predicate: Function returning True for entries to remove.
199
+
200
+ Returns:
201
+ Number of entries removed.
202
+ """
203
+ original_length = len(all_target_list)
204
+ all_target_list[:] = [
205
+ each_value
206
+ for each_value in all_target_list
207
+ if not match_predicate(each_value)
208
+ ]
209
+ return original_length - len(all_target_list)
210
+
211
+
74
212
  def load_settings(settings_path: Path) -> dict[str, object]:
213
+ """Read and parse a JSON settings file from disk.
214
+
215
+ Args:
216
+ settings_path: Path to the JSON settings file.
217
+
218
+ Returns:
219
+ Parsed settings dictionary. Returns an empty dict when the file
220
+ does not exist.
221
+
222
+ Raises:
223
+ SystemExit: When the file exists but is not valid JSON, or when
224
+ its root value is not a JSON object.
225
+ """
75
226
  if not settings_path.exists():
76
227
  return {}
77
228
  parsed_settings: dict[str, object] = {}
@@ -96,6 +247,14 @@ def load_settings(settings_path: Path) -> dict[str, object]:
96
247
 
97
248
 
98
249
  def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
250
+ """Serialize a settings dictionary to JSON text with stable formatting.
251
+
252
+ Args:
253
+ all_settings: The settings dictionary to serialize.
254
+
255
+ Returns:
256
+ Pretty-printed JSON string with sorted keys.
257
+ """
99
258
  json_indent_width_columns: int = len(" ")
100
259
  return json.dumps(
101
260
  all_settings,
@@ -105,6 +264,15 @@ def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
105
264
 
106
265
 
107
266
  def get_mode_to_preserve(settings_path: Path) -> int:
267
+ """Return the file permission bits from an existing settings file.
268
+
269
+ Args:
270
+ settings_path: Path to the target settings file to stat.
271
+
272
+ Returns:
273
+ The permission bits from the file mode. Returns the default
274
+ secure mode when the file does not exist.
275
+ """
108
276
  default_settings_file_mode: int = 0o600
109
277
  try:
110
278
  stat_result = os.stat(settings_path)
@@ -118,6 +286,23 @@ def get_mode_to_preserve(settings_path: Path) -> int:
118
286
  def write_atomically_with_mode(
119
287
  temporary_path: Path, serialized_content: str, file_mode: int
120
288
  ) -> None:
289
+ """Create and write to a temporary file with the given mode.
290
+
291
+ Uses os.open with O_CREAT | O_EXCL to create the file securely, then
292
+ writes the serialized content. The caller is responsible for replacing
293
+ the target file with os.replace afterward.
294
+
295
+ Args:
296
+ temporary_path: Path for the temporary file (sibling of target).
297
+ serialized_content: The content to write to the temporary file.
298
+ file_mode: Unix permission bits for the new file.
299
+
300
+ Raises:
301
+ OSError: When os.open or os.fdopen fails. The raw file descriptor
302
+ is closed before re-raising so the descriptor does not leak.
303
+ MemoryError: When os.fdopen runs out of buffer memory; the file
304
+ descriptor is closed before re-raising.
305
+ """
121
306
  file_descriptor = os.open(
122
307
  str(temporary_path),
123
308
  os.O_WRONLY | os.O_CREAT | os.O_EXCL,
@@ -125,7 +310,7 @@ def write_atomically_with_mode(
125
310
  )
126
311
  try:
127
312
  writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
128
- except BaseException:
313
+ except (OSError, MemoryError):
129
314
  os.close(file_descriptor)
130
315
  raise
131
316
  with writer:
@@ -133,6 +318,15 @@ def write_atomically_with_mode(
133
318
 
134
319
 
135
320
  def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
321
+ """Write settings to a JSON file atomically with permission preservation.
322
+
323
+ Creates a temporary sibling file, writes content, then atomically
324
+ replaces the target. Cleans up the temporary file in a finally block.
325
+
326
+ Args:
327
+ settings_path: Path to the target settings JSON file.
328
+ all_settings: The settings dictionary to serialize and save.
329
+ """
136
330
  unique_temporary_suffix_byte_length = UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH
137
331
  settings_path.parent.mkdir(parents=True, exist_ok=True)
138
332
  serialized_settings = serialize_settings_to_json_text(all_settings)
@@ -163,6 +357,15 @@ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
163
357
 
164
358
 
165
359
  def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
360
+ """Add a value to a list when it is not already present.
361
+
362
+ Args:
363
+ all_target_list: The list to potentially append to.
364
+ new_value: The string value to add when missing.
365
+
366
+ Returns:
367
+ True when the value was appended, False when it already existed.
368
+ """
166
369
  if new_value in all_target_list:
167
370
  return False
168
371
  all_target_list.append(new_value)
@@ -172,12 +375,19 @@ def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
172
375
  def ensure_dict_section(
173
376
  all_settings: dict[str, object], section_name: str
174
377
  ) -> dict[str, object]:
175
- """Return an existing dict section or create an empty one if absent.
378
+ """Return an existing dict section or create an empty one when absent.
176
379
 
177
380
  A missing key and an explicit JSON null are treated identically: both
178
381
  produce a fresh empty dict stored back into settings. Any other non-dict
179
382
  value (string, list, number, bool) calls exit_with_error to avoid
180
383
  overwriting user data.
384
+
385
+ Args:
386
+ all_settings: The parsed settings dictionary.
387
+ section_name: Key name of the section to retrieve or create.
388
+
389
+ Returns:
390
+ The existing or newly created section dictionary.
181
391
  """
182
392
  existing_section = all_settings.get(section_name)
183
393
  if existing_section is None:
@@ -194,12 +404,19 @@ def ensure_dict_section(
194
404
 
195
405
 
196
406
  def ensure_list_entry(all_section: dict[str, object], entry_name: str) -> list[object]:
197
- """Return an existing list entry or create an empty one if absent.
407
+ """Return an existing list entry or create an empty one when absent.
198
408
 
199
409
  A missing key and an explicit JSON null are treated identically: both
200
410
  produce a fresh empty list stored back into the section. Any other
201
411
  non-list value (string, dict, number, bool) calls exit_with_error to
202
412
  avoid overwriting user data.
413
+
414
+ Args:
415
+ all_section: The parent dictionary section.
416
+ entry_name: Key name of the list entry to retrieve or create.
417
+
418
+ Returns:
419
+ The existing or newly created list entry.
203
420
  """
204
421
  existing_entry = all_section.get(entry_name)
205
422
  if existing_entry is None:
@@ -224,6 +441,13 @@ def is_valid_project_root(candidate_path: Path) -> bool:
224
441
  def prune_empty_list_then_empty_section(
225
442
  all_settings: dict[str, object], section_key: str, list_key: str
226
443
  ) -> None:
444
+ """Remove an empty list key and its parent section when both are empty.
445
+
446
+ Args:
447
+ all_settings: The parsed settings dictionary to prune in place.
448
+ section_key: Key of the parent section to check.
449
+ list_key: Key of the list entry within the section.
450
+ """
227
451
  section = all_settings.get(section_key)
228
452
  if not isinstance(section, dict):
229
453
  return