claude-dev-env 1.38.1 → 1.40.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 (282) hide show
  1. package/CLAUDE.md +10 -36
  2. package/_shared/pr-loop/audit-reply-template.md +147 -0
  3. package/_shared/pr-loop/fix-protocol.md +25 -4
  4. package/_shared/pr-loop/gh-payloads.md +37 -50
  5. package/_shared/pr-loop/scripts/code_rules_gate.py +0 -60
  6. package/_shared/pr-loop/scripts/config/post_audit_thread_constants.py +199 -0
  7. package/_shared/pr-loop/scripts/config/reviews_disabled_constants.py +8 -0
  8. package/_shared/pr-loop/scripts/post_audit_thread.py +1242 -0
  9. package/_shared/pr-loop/scripts/preflight.py +129 -2
  10. package/_shared/pr-loop/scripts/reviews_disabled.py +59 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  12. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +1116 -0
  13. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  14. package/_shared/pr-loop/scripts/tests/test_preflight.py +41 -0
  15. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +36 -0
  16. package/_shared/pr-loop/state-schema.md +1 -1
  17. package/agents/clean-coder.md +2 -2
  18. package/agents/pr-description-writer.md +150 -52
  19. package/bin/install.mjs +6 -7
  20. package/bin/install.test.mjs +8 -0
  21. package/commands/doc-gist.md +16 -0
  22. package/commands/plan.md +0 -2
  23. package/commands/review-plan.md +1 -1
  24. package/docs/CODE_RULES.md +122 -2
  25. package/docs/PR_DESCRIPTION_GUIDE.md +127 -64
  26. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  27. package/hooks/blocking/code_rules_enforcer.py +1143 -129
  28. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  29. package/hooks/blocking/destructive_command_blocker.py +74 -0
  30. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  31. package/hooks/blocking/md_to_html_blocker.py +119 -0
  32. package/hooks/blocking/pr_description_enforcer.py +57 -22
  33. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  34. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  35. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  37. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  38. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  39. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  40. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  41. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  42. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  44. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  45. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  46. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  47. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  48. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  49. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  50. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  51. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  52. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  53. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  54. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  55. package/hooks/blocking/test_pr_description_enforcer.py +69 -8
  56. package/hooks/config/any_type_config.py +7 -0
  57. package/hooks/config/banned_identifiers_constants.py +11 -0
  58. package/hooks/config/blocking_check_limits.py +38 -0
  59. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  60. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  61. package/hooks/config/convergence_branch_constants.py +9 -0
  62. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  63. package/hooks/config/html_companion_constants.py +20 -0
  64. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  65. package/hooks/config/pr_description_enforcer_constants.py +14 -0
  66. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  67. package/hooks/hooks.json +28 -20
  68. package/hooks/pyproject.toml +69 -0
  69. package/hooks/validators/mypy_integration.py +47 -1
  70. package/hooks/validators/run_all_validators.py +3 -3
  71. package/hooks/validators/test_mypy_integration.py +50 -1
  72. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  73. package/hooks/workflow/md_to_html_companion.py +365 -0
  74. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  75. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  76. package/package.json +1 -1
  77. package/rules/gh-body-file.md +2 -0
  78. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  79. package/scripts/check.ps1 +106 -0
  80. package/scripts/config/timing.py +11 -0
  81. package/scripts/sweep_empty_dirs.py +138 -0
  82. package/scripts/sync_to_cursor/rules.py +1 -1
  83. package/scripts/test_sweep_empty_dirs.py +183 -0
  84. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  85. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  86. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  87. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  88. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  89. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  90. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  91. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  92. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  93. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  94. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  95. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  96. package/skills/bugteam/CONSTRAINTS.md +21 -22
  97. package/skills/bugteam/EXAMPLES.md +3 -3
  98. package/skills/bugteam/PROMPTS.md +227 -67
  99. package/skills/bugteam/SKILL.md +132 -455
  100. package/skills/bugteam/reference/README.md +1 -1
  101. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  102. package/skills/bugteam/reference/audit-contract.md +4 -22
  103. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  104. package/skills/bugteam/reference/design-rationale.md +2 -2
  105. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  106. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  107. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  108. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  109. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  113. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  114. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  115. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  116. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  117. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  118. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  119. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  120. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  121. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  122. package/skills/bugteam/reference/team-setup.md +111 -9
  123. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  124. package/skills/bugteam/scripts/README.md +60 -0
  125. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  126. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  127. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  128. package/skills/bugteam/scripts/bugteam_preflight.py +328 -0
  129. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  130. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  131. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  132. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  133. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  134. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  135. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  136. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  137. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  138. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  139. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  140. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  141. package/skills/bugteam/scripts/test_bugteam_preflight.py +309 -0
  142. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  143. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  144. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  145. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  146. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  147. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  148. package/skills/bugteam/test_skill_additions.py +1 -11
  149. package/skills/code/SKILL.md +176 -0
  150. package/skills/copilot-review/SKILL.md +16 -0
  151. package/skills/doc-gist/SKILL.md +99 -0
  152. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  153. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  154. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  155. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  156. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  157. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  158. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  159. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  160. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  161. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  162. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  163. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  164. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  165. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  166. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  167. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  168. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  169. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  170. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  171. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  172. package/skills/doc-gist/references/examples/README.md +5 -0
  173. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  174. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  175. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  176. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  177. package/skills/findbugs/SKILL.md +96 -2
  178. package/skills/monitor-open-prs/SKILL.md +14 -32
  179. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  180. package/skills/pr-consistency-audit/SKILL.md +112 -0
  181. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  182. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  183. package/skills/pr-converge/SKILL.md +229 -23
  184. package/skills/pr-converge/config/__init__.py +0 -0
  185. package/skills/pr-converge/config/constants.py +63 -0
  186. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  187. package/skills/pr-converge/reference/examples.md +43 -11
  188. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  189. package/skills/pr-converge/reference/ground-rules.md +5 -3
  190. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  191. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  192. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  193. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  194. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  195. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  196. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  197. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  198. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  199. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  200. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  201. package/skills/pr-converge/reference/per-tick.md +107 -31
  202. package/skills/pr-converge/reference/state-schema.md +22 -1
  203. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  204. package/skills/pr-converge/scripts/README.md +34 -46
  205. package/skills/pr-converge/scripts/check_bugbot_ci.py +279 -0
  206. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  207. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  208. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  209. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  210. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  211. package/skills/pr-converge/scripts/test_check_bugbot_ci.py +312 -0
  212. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  213. package/skills/qbug/SKILL.md +157 -27
  214. package/skills/session-log/SKILL.md +216 -114
  215. package/skills/session-tidy/SKILL.md +1 -1
  216. package/skills/skill-builder/SKILL.md +138 -56
  217. package/skills/skill-builder/references/delegation-map.md +72 -113
  218. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  219. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  220. package/skills/skill-builder/references/skill-types.md +228 -0
  221. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  222. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  223. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  224. package/skills/skill-builder/workflows/new-skill.md +80 -168
  225. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  226. package/skills/structure-prompt/SKILL.md +50 -0
  227. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  228. package/skills/structure-prompt/reference/block-classification.md +27 -0
  229. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  230. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  231. package/skills/structure-prompt/reference/cleanup.md +33 -0
  232. package/skills/structure-prompt/reference/constraints.md +33 -0
  233. package/skills/structure-prompt/reference/directives.md +37 -0
  234. package/skills/structure-prompt/reference/examples.md +72 -0
  235. package/skills/structure-prompt/reference/instantiation.md +51 -0
  236. package/skills/structure-prompt/reference/output-contract.md +72 -0
  237. package/skills/structure-prompt/reference/per-category.md +23 -0
  238. package/skills/structure-prompt/reference/persona.md +38 -0
  239. package/skills/structure-prompt/reference/research.md +33 -0
  240. package/skills/structure-prompt/reference/structure.md +28 -0
  241. package/agents/code-standards-agent.md +0 -93
  242. package/agents/groq-coder.md +0 -113
  243. package/agents/plan-executor.md +0 -226
  244. package/agents/project-docs-analyzer.md +0 -53
  245. package/agents/project-structure-organizer-agent.md +0 -72
  246. package/agents/skill-to-agent-converter.md +0 -370
  247. package/agents/skill-writer-agent.md +0 -470
  248. package/agents/user-docs-writer.md +0 -67
  249. package/agents/workflow-visual-documenter.md +0 -82
  250. package/commands/readability-review.md +0 -20
  251. package/hooks/mypy.ini +0 -2
  252. package/hooks/notification/attention_needed_notify.py +0 -71
  253. package/hooks/notification/claude_notification_handler.py +0 -67
  254. package/hooks/notification/notification_utils.py +0 -267
  255. package/hooks/notification/subagent_complete_notify.py +0 -381
  256. package/hooks/notification/test_attention_needed_notify.py +0 -47
  257. package/hooks/notification/test_claude_notification_handler.py +0 -54
  258. package/hooks/notification/test_notification_utils.py +0 -91
  259. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  260. package/scripts/config/groq_bugteam_config.py +0 -230
  261. package/scripts/config/test_groq_bugteam_config.py +0 -83
  262. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  263. package/scripts/groq_bugteam.README.md +0 -131
  264. package/scripts/groq_bugteam.py +0 -647
  265. package/scripts/groq_bugteam_dotenv.py +0 -40
  266. package/scripts/groq_bugteam_spec.py +0 -226
  267. package/scripts/test_groq_bugteam.py +0 -529
  268. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  269. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  270. package/scripts/test_groq_bugteam_spec.py +0 -338
  271. package/skills/bugteam/SKILL_EVALS.md +0 -309
  272. package/skills/dream/SKILL.md +0 -118
  273. package/skills/ingest/SKILL.md +0 -40
  274. package/skills/npm-creator/SKILL.md +0 -187
  275. package/skills/readability-review/SKILL.md +0 -127
  276. package/skills/resume-review/SKILL.md +0 -261
  277. package/skills/rule-audit/SKILL.md +0 -307
  278. package/skills/rule-creator/SKILL.md +0 -150
  279. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  280. package/skills/skill-writer/REFERENCE.md +0 -284
  281. package/skills/skill-writer/SKILL.md +0 -222
  282. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,358 @@
1
+ """Shared helpers for grant_project_claude_permissions and revoke_project_claude_permissions.
2
+
3
+ Writes to ~/.claude/settings.json are atomic and permission-preserving: the
4
+ target file's existing POSIX mode is captured, a sibling temp file is
5
+ created via os.open with O_CREAT | O_EXCL and the preserved mode, content
6
+ is written, then os.replace swaps it into place. Output is serialized with
7
+ sort_keys=True for a stable on-disk layout; the first run on a hand-ordered
8
+ settings file produces a one-time re-sort diff, subsequent writes are stable.
9
+ """
10
+
11
+ import json
12
+ import os
13
+ import stat
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import NoReturn
17
+
18
+ _previously_cached_config = {}
19
+ for each_cached_module_name in [
20
+ each_module_key
21
+ for each_module_key in list(sys.modules)
22
+ if each_module_key == "config" or each_module_key.startswith("config.")
23
+ ]:
24
+ _previously_cached_config[each_cached_module_name] = sys.modules.pop(
25
+ each_cached_module_name
26
+ )
27
+
28
+ from config.claude_permissions_common_constants import (
29
+ ATOMIC_WRITE_TEMPORARY_SUFFIX,
30
+ DEFAULT_SETTINGS_FILE_MODE,
31
+ TEXT_FILE_ENCODING,
32
+ )
33
+
34
+ sys.modules.update(_previously_cached_config)
35
+
36
+
37
+ def exit_with_error(message: str) -> NoReturn:
38
+ print(f"Error: {message}", file=sys.stderr)
39
+ raise SystemExit(1)
40
+
41
+
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
+ """
51
+ glob_metacharacters_in_path: tuple[str, ...] = (
52
+ "*",
53
+ "?",
54
+ "[",
55
+ "]",
56
+ "(",
57
+ ")",
58
+ "{",
59
+ "}",
60
+ ",",
61
+ )
62
+ return any(
63
+ each_character in candidate_path
64
+ for each_character in glob_metacharacters_in_path
65
+ )
66
+
67
+
68
+ def get_current_project_path() -> str:
69
+ """Return the normalized current working directory path.
70
+
71
+ Returns:
72
+ The cwd as a POSIX-style path string.
73
+
74
+ Raises:
75
+ ValueError: When the path contains glob metacharacters.
76
+ """
77
+ normalized_project_path = str(Path.cwd()).replace("\\", "/")
78
+ if path_contains_glob_metacharacters(normalized_project_path):
79
+ raise ValueError(
80
+ f"Current directory path contains glob metacharacters and cannot "
81
+ f"be used to build permission rules safely: {normalized_project_path}"
82
+ )
83
+ return normalized_project_path
84
+
85
+
86
+ def build_permission_rule(tool_name: str, project_path: str) -> str:
87
+ return f"{tool_name}({project_path}/.claude/**)"
88
+
89
+
90
+ def build_permission_rules(
91
+ project_path: str, all_permission_allow_tools: tuple[str, ...]
92
+ ) -> list[str]:
93
+ """Construct permission rule strings for each tool.
94
+
95
+ Args:
96
+ project_path: The POSIX-style project root path.
97
+ all_permission_allow_tools: Tool names to build rules for.
98
+
99
+ Returns:
100
+ List of permission rule strings for the given project path.
101
+ """
102
+ return [
103
+ build_permission_rule(each_tool, project_path)
104
+ for each_tool in all_permission_allow_tools
105
+ ]
106
+
107
+
108
+ def load_settings(settings_path: Path) -> dict[str, object]:
109
+ """Read and parse a JSON settings file from disk.
110
+
111
+ Args:
112
+ settings_path: Path to the JSON settings file.
113
+
114
+ Returns:
115
+ Parsed settings dictionary. Returns an empty dict when the file
116
+ does not exist.
117
+
118
+ Raises:
119
+ SystemExit: When the file exists but is not valid JSON, or when
120
+ its root value is not a JSON object.
121
+ """
122
+ if not settings_path.exists():
123
+ return {}
124
+ parsed_settings: dict[str, object] = {}
125
+ try:
126
+ raw_text = settings_path.read_text(encoding=TEXT_FILE_ENCODING)
127
+ except OSError as read_error:
128
+ exit_with_error(f"Failed to read {settings_path}: {read_error}")
129
+ try:
130
+ parsed_settings = json.loads(raw_text)
131
+ except json.JSONDecodeError as decode_error:
132
+ exit_with_error(
133
+ f"Refusing to modify {settings_path}: existing file is not valid JSON "
134
+ f"({decode_error}). Fix or back up the file manually, then re-run."
135
+ )
136
+ if not isinstance(parsed_settings, dict):
137
+ exit_with_error(
138
+ f"Refusing to modify {settings_path}: existing file's root is "
139
+ f"{type(parsed_settings).__name__}, not a JSON object. Fix or back up "
140
+ f"the file manually, then re-run."
141
+ )
142
+ return parsed_settings
143
+
144
+
145
+ def serialize_settings_to_json_text(all_settings: dict[str, object]) -> str:
146
+ """Serialize a settings dictionary to JSON text with stable formatting.
147
+
148
+ Args:
149
+ all_settings: The settings dictionary to serialize.
150
+
151
+ Returns:
152
+ Pretty-printed JSON string with sorted keys.
153
+ """
154
+ json_indent_width_columns: int = len(" ")
155
+ return json.dumps(
156
+ all_settings,
157
+ indent=json_indent_width_columns,
158
+ sort_keys=True,
159
+ )
160
+
161
+
162
+ def get_mode_to_preserve(settings_path: Path) -> int:
163
+ """Return the file permission bits from an existing settings file.
164
+
165
+ Args:
166
+ settings_path: Path to the target settings file to stat.
167
+
168
+ Returns:
169
+ The permission bits from the file mode (lower portion).
170
+ Returns DEFAULT_SETTINGS_FILE_MODE when the file does not exist.
171
+ """
172
+ try:
173
+ stat_result = os.stat(settings_path)
174
+ except FileNotFoundError:
175
+ return DEFAULT_SETTINGS_FILE_MODE
176
+ except OSError as stat_error:
177
+ exit_with_error(f"Failed to stat {settings_path}: {stat_error}")
178
+ return stat.S_IMODE(stat_result.st_mode)
179
+
180
+
181
+ def write_atomically_with_mode(
182
+ temporary_path: Path, serialized_content: str, file_mode: int
183
+ ) -> None:
184
+ """Create and write to a temporary file with the given mode.
185
+
186
+ Uses os.open with O_CREAT | O_EXCL to create the file securely,
187
+ then writes the serialized content. The caller is responsible for
188
+ replacing the target file with os.replace afterward.
189
+
190
+ Args:
191
+ temporary_path: Path for the temporary file (sibling of target).
192
+ serialized_content: The content to write to the temporary file.
193
+ file_mode: Unix permission bits for the new file (e.g., 0o600).
194
+
195
+ Raises:
196
+ OSError: When os.open or os.fdopen fails. The raw file descriptor
197
+ is closed before re-raising so the FD does not leak.
198
+ MemoryError: When os.fdopen runs out of buffer memory; the FD is
199
+ closed before re-raising.
200
+ """
201
+ file_descriptor = os.open(
202
+ str(temporary_path),
203
+ os.O_WRONLY | os.O_CREAT | os.O_EXCL,
204
+ file_mode,
205
+ )
206
+ try:
207
+ writer = os.fdopen(file_descriptor, "w", encoding=TEXT_FILE_ENCODING)
208
+ except (OSError, MemoryError):
209
+ os.close(file_descriptor)
210
+ try:
211
+ os.unlink(str(temporary_path))
212
+ except OSError:
213
+ pass
214
+ raise
215
+ with writer:
216
+ writer.write(serialized_content)
217
+
218
+
219
+ def save_settings(settings_path: Path, all_settings: dict[str, object]) -> None:
220
+ """Write settings to a JSON file atomically with permission preservation.
221
+
222
+ Creates a temporary sibling file, writes content, then atomically
223
+ replaces the target. Cleans up the temporary file in a finally block.
224
+
225
+ Args:
226
+ settings_path: Path to the target settings JSON file.
227
+ all_settings: The settings dictionary to serialize and save.
228
+ """
229
+ settings_path.parent.mkdir(parents=True, exist_ok=True)
230
+ serialized_settings = serialize_settings_to_json_text(all_settings)
231
+ process_keyed_temporary_suffix = (
232
+ f"{ATOMIC_WRITE_TEMPORARY_SUFFIX}.{os.getpid()}"
233
+ )
234
+ temporary_path = settings_path.with_suffix(
235
+ settings_path.suffix + process_keyed_temporary_suffix
236
+ )
237
+ mode_to_preserve = get_mode_to_preserve(settings_path)
238
+ is_temp_owned_by_this_invocation = False
239
+ try:
240
+ try:
241
+ write_atomically_with_mode(
242
+ temporary_path, serialized_settings, mode_to_preserve
243
+ )
244
+ is_temp_owned_by_this_invocation = True
245
+ os.replace(str(temporary_path), str(settings_path))
246
+ is_temp_owned_by_this_invocation = False
247
+ except OSError as os_error:
248
+ exit_with_error(
249
+ f"Failed to write settings atomically to {settings_path}: {os_error}"
250
+ )
251
+ finally:
252
+ if is_temp_owned_by_this_invocation and temporary_path.exists():
253
+ try:
254
+ temporary_path.unlink()
255
+ except OSError as unlink_error:
256
+ print(
257
+ f"Warning: could not remove temp file {temporary_path}: "
258
+ f"{type(unlink_error).__name__}: {unlink_error}",
259
+ file=sys.stderr,
260
+ )
261
+
262
+
263
+ def append_if_missing(all_target_list: list[object], new_value: str) -> bool:
264
+ """Add a value to a list if it is not already present.
265
+
266
+ Args:
267
+ all_target_list: The list to potentially append to.
268
+ new_value: The string value to add if missing.
269
+
270
+ Returns:
271
+ True when the value was appended, False when it already existed.
272
+ """
273
+ if new_value in all_target_list:
274
+ return False
275
+ all_target_list.append(new_value)
276
+ return True
277
+
278
+
279
+ def ensure_dict_section(
280
+ all_settings: dict[str, object], section_name: str
281
+ ) -> dict[str, object]:
282
+ """Return an existing dict section or create an empty one if absent.
283
+
284
+ A missing key and an explicit JSON null are treated identically: both
285
+ produce a fresh empty dict stored back into settings. Any other non-dict
286
+ value (string, list, number, bool) calls exit_with_error to avoid
287
+ overwriting user data.
288
+
289
+ Args:
290
+ all_settings: The parsed settings dictionary.
291
+ section_name: Key name of the section to retrieve or create.
292
+
293
+ Returns:
294
+ The existing or newly created section dictionary.
295
+ """
296
+ existing_section = all_settings.get(section_name)
297
+ if existing_section is None:
298
+ replacement_section: dict[str, object] = {}
299
+ all_settings[section_name] = replacement_section
300
+ return replacement_section
301
+ if not isinstance(existing_section, dict):
302
+ exit_with_error(
303
+ f"Refusing to modify settings key {section_name!r}: existing value "
304
+ f"is {type(existing_section).__name__}, not a JSON object. Fix or "
305
+ f"remove the key manually, then re-run."
306
+ )
307
+ return existing_section
308
+
309
+
310
+ def ensure_list_entry(
311
+ all_section: dict[str, object], entry_name: str
312
+ ) -> list[object]:
313
+ """Return an existing list entry or create an empty one if absent.
314
+
315
+ A missing key and an explicit JSON null are treated identically: both
316
+ produce a fresh empty list stored back into the section. Any other
317
+ non-list value (string, dict, number, bool) calls exit_with_error to
318
+ avoid overwriting user data.
319
+
320
+ Args:
321
+ all_section: The parent dictionary section.
322
+ entry_name: Key name of the list entry to retrieve or create.
323
+
324
+ Returns:
325
+ The existing or newly created list entry.
326
+ """
327
+ existing_entry = all_section.get(entry_name)
328
+ if existing_entry is None:
329
+ replacement_entry: list[object] = []
330
+ all_section[entry_name] = replacement_entry
331
+ return replacement_entry
332
+ if not isinstance(existing_entry, list):
333
+ exit_with_error(
334
+ f"Refusing to modify settings entry {entry_name!r}: existing value "
335
+ f"is {type(existing_entry).__name__}, not a JSON array. Fix or "
336
+ f"remove the entry manually, then re-run."
337
+ )
338
+ return existing_entry
339
+
340
+
341
+ def prune_empty_list_then_empty_section(
342
+ all_settings: dict[str, object], section_key: str, list_key: str
343
+ ) -> None:
344
+ """Remove an empty list key and its parent section if both are empty.
345
+
346
+ Args:
347
+ all_settings: The parsed settings dictionary to prune in place.
348
+ section_key: Key of the parent section to check.
349
+ list_key: Key of the list entry within the section.
350
+ """
351
+ section = all_settings.get(section_key)
352
+ if not isinstance(section, dict):
353
+ return
354
+ list_entry = section.get(list_key)
355
+ if isinstance(list_entry, list) and len(list_entry) == 0:
356
+ del section[list_key]
357
+ if len(section) == 0:
358
+ del all_settings[section_key]