claude-dev-env 1.38.0 → 1.39.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 (271) 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 +189 -0
  7. package/_shared/pr-loop/scripts/post_audit_thread.py +947 -0
  8. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +0 -19
  9. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +923 -0
  10. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +127 -0
  11. package/_shared/pr-loop/state-schema.md +1 -1
  12. package/agents/clean-coder.md +2 -2
  13. package/bin/install.mjs +6 -7
  14. package/bin/install.test.mjs +8 -0
  15. package/commands/doc-gist.md +16 -0
  16. package/commands/plan.md +0 -2
  17. package/commands/review-plan.md +1 -1
  18. package/docs/CODE_RULES.md +122 -2
  19. package/hooks/blocking/bot_mention_comment_blocker.py +75 -0
  20. package/hooks/blocking/code_rules_enforcer.py +1236 -161
  21. package/hooks/blocking/convergence_gate_blocker.py +130 -0
  22. package/hooks/blocking/destructive_command_blocker.py +74 -0
  23. package/hooks/blocking/gh_body_arg_blocker.py +30 -0
  24. package/hooks/blocking/md_to_html_blocker.py +119 -0
  25. package/hooks/blocking/test_bot_mention_comment_blocker.py +131 -0
  26. package/hooks/blocking/test_code_rules_enforcer.py +21 -0
  27. package/hooks/blocking/test_code_rules_enforcer_any_exempt_files.py +70 -0
  28. package/hooks/blocking/test_code_rules_enforcer_any_imports_and_cast.py +92 -0
  29. package/hooks/blocking/test_code_rules_enforcer_banned_import_alias.py +143 -0
  30. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +152 -0
  31. package/hooks/blocking/test_code_rules_enforcer_bare_except.py +120 -0
  32. package/hooks/blocking/test_code_rules_enforcer_boundary_types.py +175 -0
  33. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  34. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +50 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_format.py +255 -0
  36. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +130 -0
  37. package/hooks/blocking/test_code_rules_enforcer_stub_implementations.py +141 -0
  38. package/hooks/blocking/test_code_rules_enforcer_test_branching.py +143 -0
  39. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +169 -0
  40. package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +99 -0
  41. package/hooks/blocking/test_code_rules_enforcer_typed_dict_pairs.py +141 -0
  42. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +158 -0
  43. package/hooks/blocking/test_convergence_gate_blocker.py +63 -0
  44. package/hooks/blocking/test_destructive_command_blocker.py +146 -0
  45. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +102 -0
  46. package/hooks/blocking/test_gh_body_arg_blocker.py +45 -0
  47. package/hooks/blocking/test_md_to_html_blocker.py +317 -0
  48. package/hooks/config/any_type_config.py +7 -0
  49. package/hooks/config/banned_identifiers_constants.py +11 -0
  50. package/hooks/config/blocking_check_limits.py +38 -0
  51. package/hooks/config/bot_mention_comment_blocker_constants.py +20 -0
  52. package/hooks/config/code_rules_enforcer_constants.py +53 -0
  53. package/hooks/config/convergence_branch_constants.py +9 -0
  54. package/hooks/config/doc_gist_auto_publish_constants.py +18 -0
  55. package/hooks/config/html_companion_constants.py +20 -0
  56. package/hooks/config/inline_tuple_string_magic_constants.py +22 -0
  57. package/hooks/config/test_banned_identifiers_constants.py +17 -0
  58. package/hooks/hooks.json +28 -20
  59. package/hooks/pyproject.toml +69 -0
  60. package/hooks/validators/mypy_integration.py +47 -1
  61. package/hooks/validators/run_all_validators.py +3 -3
  62. package/hooks/validators/test_mypy_integration.py +50 -1
  63. package/hooks/workflow/doc_gist_auto_publish.py +144 -0
  64. package/hooks/workflow/md_to_html_companion.py +365 -0
  65. package/hooks/workflow/test_doc_gist_auto_publish.py +117 -0
  66. package/hooks/workflow/test_md_to_html_companion.py +452 -0
  67. package/package.json +1 -1
  68. package/rules/gh-body-file.md +2 -0
  69. package/scripts/Install-SweepEmptyDirs.ps1 +111 -0
  70. package/scripts/check.ps1 +106 -0
  71. package/scripts/config/timing.py +11 -0
  72. package/scripts/sweep_empty_dirs.py +138 -0
  73. package/scripts/sync_to_cursor/rules.py +1 -1
  74. package/scripts/test_sweep_empty_dirs.py +183 -0
  75. package/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +323 -0
  76. package/skills/_shared/pr-loop/scripts/_cli_utils.py +22 -0
  77. package/skills/_shared/pr-loop/scripts/_path_resolver.py +165 -0
  78. package/skills/_shared/pr-loop/scripts/_xml_utils.py +20 -0
  79. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +182 -0
  80. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +185 -0
  81. package/skills/_shared/pr-loop/scripts/config/__init__.py +0 -0
  82. package/skills/_shared/pr-loop/scripts/config/path_resolver_constants.py +78 -0
  83. package/skills/_shared/pr-loop/scripts/init_loop_state.py +135 -0
  84. package/skills/_shared/pr-loop/scripts/teardown_worktrees.py +175 -0
  85. package/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +182 -0
  86. package/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +206 -0
  87. package/skills/bugteam/CONSTRAINTS.md +21 -22
  88. package/skills/bugteam/EXAMPLES.md +3 -3
  89. package/skills/bugteam/PROMPTS.md +227 -67
  90. package/skills/bugteam/SKILL.md +114 -455
  91. package/skills/bugteam/reference/README.md +1 -1
  92. package/skills/bugteam/reference/audit-and-teammates.md +112 -39
  93. package/skills/bugteam/reference/audit-contract.md +4 -22
  94. package/skills/bugteam/reference/copilot-gap-analysis.md +8 -5
  95. package/skills/bugteam/reference/design-rationale.md +2 -2
  96. package/skills/bugteam/reference/github-pr-reviews.md +50 -57
  97. package/skills/bugteam/reference/obstacles/audit-assign-ids.md +13 -0
  98. package/skills/bugteam/reference/obstacles/audit-capture-excerpts.md +13 -0
  99. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +13 -0
  100. package/skills/bugteam/reference/obstacles/audit-write-xml.md +13 -0
  101. package/skills/bugteam/reference/obstacles/fix-append-summary.md +13 -0
  102. package/skills/bugteam/reference/obstacles/fix-apply-fixes.md +13 -0
  103. package/skills/bugteam/reference/obstacles/fix-git-add-commit.md +13 -0
  104. package/skills/bugteam/reference/obstacles/fix-git-push.md +13 -0
  105. package/skills/bugteam/reference/obstacles/fix-post-reply.md +13 -0
  106. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +13 -0
  107. package/skills/bugteam/reference/obstacles/fix-py-compile.md +13 -0
  108. package/skills/bugteam/reference/obstacles/fix-read-files.md +13 -0
  109. package/skills/bugteam/reference/obstacles/fix-resolve-thread.md +13 -0
  110. package/skills/bugteam/reference/obstacles/fix-test-suite.md +13 -0
  111. package/skills/bugteam/reference/obstacles/fix-violation-count.md +13 -0
  112. package/skills/bugteam/reference/obstacles/fix-write-xml.md +13 -0
  113. package/skills/bugteam/reference/team-setup.md +106 -9
  114. package/skills/bugteam/reference/teardown-publish-permissions.md +39 -8
  115. package/skills/bugteam/scripts/README.md +60 -0
  116. package/skills/bugteam/scripts/_claude_permissions_common.py +358 -0
  117. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +976 -0
  118. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +375 -0
  119. package/skills/bugteam/scripts/bugteam_preflight.py +294 -0
  120. package/skills/bugteam/scripts/config/bugteam_code_rules_gate_constants.py +25 -0
  121. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +26 -0
  122. package/skills/bugteam/scripts/config/bugteam_preflight_constants.py +35 -0
  123. package/skills/bugteam/scripts/config/claude_permissions_common_constants.py +20 -0
  124. package/skills/bugteam/scripts/config/probe_code_rules_enforcer_check_constants.py +12 -0
  125. package/skills/bugteam/scripts/config/windows_safe_rmtree_constants.py +7 -0
  126. package/skills/bugteam/scripts/grant_project_claude_permissions.py +175 -0
  127. package/skills/bugteam/scripts/probe_code_rules_enforcer_check.py +107 -0
  128. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +220 -0
  129. package/skills/bugteam/scripts/test__claude_permissions_common.py +112 -0
  130. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +400 -0
  131. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +384 -0
  132. package/skills/bugteam/scripts/test_bugteam_preflight.py +268 -0
  133. package/skills/bugteam/scripts/test_claude_permissions_common.py +195 -0
  134. package/skills/bugteam/scripts/test_grant_project_claude_permissions.py +55 -0
  135. package/skills/bugteam/scripts/test_probe_code_rules_enforcer_check.py +76 -0
  136. package/skills/bugteam/scripts/test_revoke_project_claude_permissions.py +55 -0
  137. package/skills/bugteam/scripts/test_windows_safe_rmtree.py +108 -0
  138. package/skills/bugteam/scripts/windows_safe_rmtree.py +100 -0
  139. package/skills/bugteam/test_skill_additions.py +1 -11
  140. package/skills/code/SKILL.md +176 -0
  141. package/skills/doc-gist/SKILL.md +99 -0
  142. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +453 -0
  143. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +515 -0
  144. package/skills/doc-gist/references/examples/03-code-review-pr.html +638 -0
  145. package/skills/doc-gist/references/examples/04-code-understanding.html +491 -0
  146. package/skills/doc-gist/references/examples/05-design-system.html +629 -0
  147. package/skills/doc-gist/references/examples/06-component-variants.html +605 -0
  148. package/skills/doc-gist/references/examples/07-prototype-animation.html +455 -0
  149. package/skills/doc-gist/references/examples/08-prototype-interaction.html +396 -0
  150. package/skills/doc-gist/references/examples/09-slide-deck.html +592 -0
  151. package/skills/doc-gist/references/examples/10-svg-illustrations.html +492 -0
  152. package/skills/doc-gist/references/examples/11-status-report.html +528 -0
  153. package/skills/doc-gist/references/examples/12-incident-report.html +596 -0
  154. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +395 -0
  155. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +381 -0
  156. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +368 -0
  157. package/skills/doc-gist/references/examples/16-implementation-plan.html +702 -0
  158. package/skills/doc-gist/references/examples/17-pr-writeup.html +595 -0
  159. package/skills/doc-gist/references/examples/18-editor-triage-board.html +573 -0
  160. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +663 -0
  161. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +722 -0
  162. package/skills/doc-gist/references/examples/README.md +5 -0
  163. package/skills/doc-gist/scripts/config/__init__.py +0 -0
  164. package/skills/doc-gist/scripts/config/gist_upload_constants.py +16 -0
  165. package/skills/doc-gist/scripts/gist_upload.py +177 -0
  166. package/skills/doc-gist/scripts/test_gist_upload.py +51 -0
  167. package/skills/findbugs/SKILL.md +68 -2
  168. package/skills/monitor-open-prs/SKILL.md +13 -32
  169. package/skills/monitor-open-prs/test_skill_contract.py +0 -11
  170. package/skills/pr-consistency-audit/SKILL.md +112 -0
  171. package/skills/pr-consistency-audit/reference/detection-rules.md +96 -0
  172. package/skills/pr-consistency-audit/reference/illustrations.md +78 -0
  173. package/skills/pr-converge/SKILL.md +227 -23
  174. package/skills/pr-converge/config/__init__.py +0 -0
  175. package/skills/pr-converge/config/constants.py +62 -0
  176. package/skills/pr-converge/reference/convergence-gates.md +138 -44
  177. package/skills/pr-converge/reference/examples.md +43 -11
  178. package/skills/pr-converge/reference/fix-protocol.md +6 -5
  179. package/skills/pr-converge/reference/ground-rules.md +5 -3
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +44 -19
  181. package/skills/pr-converge/reference/obstacles/fix-post-replies.md +13 -0
  182. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +13 -0
  183. package/skills/pr-converge/reference/obstacles/fix-push.md +13 -0
  184. package/skills/pr-converge/reference/obstacles/fix-read-filelines.md +13 -0
  185. package/skills/pr-converge/reference/obstacles/fix-reset-state.md +13 -0
  186. package/skills/pr-converge/reference/obstacles/fix-resolve-threads.md +13 -0
  187. package/skills/pr-converge/reference/obstacles/fix-spawn-clean-coder.md +13 -0
  188. package/skills/pr-converge/reference/obstacles/fix-stage-commit.md +13 -0
  189. package/skills/pr-converge/reference/obstacles/fix-trigger-bugbot.md +13 -0
  190. package/skills/pr-converge/reference/obstacles/fix-write-test.md +13 -0
  191. package/skills/pr-converge/reference/per-tick.md +90 -31
  192. package/skills/pr-converge/reference/state-schema.md +22 -1
  193. package/skills/pr-converge/reference/stop-conditions.md +9 -7
  194. package/skills/pr-converge/scripts/README.md +34 -46
  195. package/skills/pr-converge/scripts/check_bugbot_ci.py +174 -0
  196. package/skills/pr-converge/scripts/check_convergence.py +497 -0
  197. package/skills/pr-converge/scripts/check_pending_reviews.py +154 -0
  198. package/skills/pr-converge/scripts/config/pr_converge_constants.py +118 -0
  199. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +134 -0
  200. package/skills/pr-converge/scripts/post_fix_reply.py +168 -0
  201. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -12
  202. package/skills/qbug/SKILL.md +132 -27
  203. package/skills/session-log/SKILL.md +216 -114
  204. package/skills/session-tidy/SKILL.md +1 -1
  205. package/skills/skill-builder/SKILL.md +138 -56
  206. package/skills/skill-builder/references/delegation-map.md +72 -113
  207. package/skills/skill-builder/references/progressive-disclosure.md +122 -0
  208. package/skills/skill-builder/references/self-audit-checklist.md +92 -0
  209. package/skills/skill-builder/references/skill-types.md +228 -0
  210. package/skills/skill-builder/references/thariq-x-post-skills.json +33 -0
  211. package/skills/skill-builder/templates/gap-analysis.md +15 -8
  212. package/skills/skill-builder/workflows/improve-skill.md +86 -57
  213. package/skills/skill-builder/workflows/new-skill.md +80 -168
  214. package/skills/skill-builder/workflows/polish-skill.md +78 -54
  215. package/skills/structure-prompt/SKILL.md +50 -0
  216. package/skills/structure-prompt/reference/adversarial-tuning.md +62 -0
  217. package/skills/structure-prompt/reference/block-classification.md +27 -0
  218. package/skills/structure-prompt/reference/canonical-case.md +48 -0
  219. package/skills/structure-prompt/reference/citation-depth.md +70 -0
  220. package/skills/structure-prompt/reference/cleanup.md +33 -0
  221. package/skills/structure-prompt/reference/constraints.md +33 -0
  222. package/skills/structure-prompt/reference/directives.md +37 -0
  223. package/skills/structure-prompt/reference/examples.md +72 -0
  224. package/skills/structure-prompt/reference/instantiation.md +51 -0
  225. package/skills/structure-prompt/reference/output-contract.md +72 -0
  226. package/skills/structure-prompt/reference/per-category.md +23 -0
  227. package/skills/structure-prompt/reference/persona.md +38 -0
  228. package/skills/structure-prompt/reference/research.md +33 -0
  229. package/skills/structure-prompt/reference/structure.md +28 -0
  230. package/agents/code-standards-agent.md +0 -93
  231. package/agents/groq-coder.md +0 -113
  232. package/agents/plan-executor.md +0 -226
  233. package/agents/project-docs-analyzer.md +0 -53
  234. package/agents/project-structure-organizer-agent.md +0 -72
  235. package/agents/skill-to-agent-converter.md +0 -370
  236. package/agents/skill-writer-agent.md +0 -470
  237. package/agents/user-docs-writer.md +0 -67
  238. package/agents/workflow-visual-documenter.md +0 -82
  239. package/commands/readability-review.md +0 -20
  240. package/hooks/mypy.ini +0 -2
  241. package/hooks/notification/attention_needed_notify.py +0 -71
  242. package/hooks/notification/claude_notification_handler.py +0 -67
  243. package/hooks/notification/notification_utils.py +0 -267
  244. package/hooks/notification/subagent_complete_notify.py +0 -381
  245. package/hooks/notification/test_attention_needed_notify.py +0 -47
  246. package/hooks/notification/test_claude_notification_handler.py +0 -54
  247. package/hooks/notification/test_notification_utils.py +0 -91
  248. package/hooks/notification/test_subagent_complete_notify.py +0 -79
  249. package/scripts/config/groq_bugteam_config.py +0 -230
  250. package/scripts/config/test_groq_bugteam_config.py +0 -83
  251. package/scripts/config/test_spec_implementer_prompt.py +0 -32
  252. package/scripts/groq_bugteam.README.md +0 -131
  253. package/scripts/groq_bugteam.py +0 -647
  254. package/scripts/groq_bugteam_dotenv.py +0 -40
  255. package/scripts/groq_bugteam_spec.py +0 -226
  256. package/scripts/test_groq_bugteam.py +0 -529
  257. package/scripts/test_groq_bugteam_apply_fix_from_spec.py +0 -426
  258. package/scripts/test_groq_bugteam_dotenv.py +0 -66
  259. package/scripts/test_groq_bugteam_spec.py +0 -338
  260. package/skills/bugteam/SKILL_EVALS.md +0 -309
  261. package/skills/dream/SKILL.md +0 -118
  262. package/skills/ingest/SKILL.md +0 -40
  263. package/skills/npm-creator/SKILL.md +0 -187
  264. package/skills/readability-review/SKILL.md +0 -127
  265. package/skills/resume-review/SKILL.md +0 -261
  266. package/skills/rule-audit/SKILL.md +0 -307
  267. package/skills/rule-creator/SKILL.md +0 -150
  268. package/skills/searching-obsidian-vault/SKILL.md +0 -131
  269. package/skills/skill-writer/REFERENCE.md +0 -284
  270. package/skills/skill-writer/SKILL.md +0 -222
  271. package/skills/tdd-team/SKILL.md +0 -128
@@ -0,0 +1,195 @@
1
+ import importlib
2
+ import sys
3
+ from pathlib import Path
4
+ from types import ModuleType
5
+ from unittest.mock import patch
6
+
7
+ import pytest
8
+
9
+ _script_directory = str(Path(__file__).resolve().parent)
10
+ if _script_directory not in sys.path:
11
+ sys.path.insert(0, _script_directory)
12
+
13
+ import _claude_permissions_common as common_module
14
+ from _claude_permissions_common import (
15
+ build_permission_rule,
16
+ get_current_project_path,
17
+ path_contains_glob_metacharacters,
18
+ save_settings,
19
+ )
20
+ from config.claude_permissions_common_constants import DEFAULT_SETTINGS_FILE_MODE
21
+ import grant_project_claude_permissions as grant_module
22
+ import revoke_project_claude_permissions as revoke_module
23
+
24
+
25
+ def test_return_normalized_path_when_cwd_contains_spaces(
26
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
27
+ ) -> None:
28
+ directory_with_spaces = tmp_path / "dir with spaces"
29
+ directory_with_spaces.mkdir()
30
+ monkeypatch.chdir(directory_with_spaces)
31
+ returned_project_path = get_current_project_path()
32
+ expected_suffix = "/dir with spaces"
33
+ assert returned_project_path.endswith(expected_suffix)
34
+ assert "\\" not in returned_project_path
35
+ built_rule = build_permission_rule("Edit", returned_project_path)
36
+ assert built_rule.startswith("Edit(")
37
+ assert built_rule.endswith("/.claude/**)")
38
+ assert "dir with spaces" in built_rule
39
+
40
+
41
+ def test_raise_when_cwd_contains_glob_metacharacters(
42
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
43
+ ) -> None:
44
+ directory_with_star = tmp_path / "weird[dir]"
45
+ directory_with_star.mkdir()
46
+ monkeypatch.chdir(directory_with_star)
47
+ with pytest.raises(ValueError, match="glob metacharacters"):
48
+ get_current_project_path()
49
+
50
+
51
+ def test_flag_glob_metacharacters_in_any_position() -> None:
52
+ assert path_contains_glob_metacharacters("/home/user/[dir]/project")
53
+ assert path_contains_glob_metacharacters("/home/user/project*")
54
+ assert not path_contains_glob_metacharacters("/home/user/dir with spaces")
55
+
56
+
57
+ def test_save_settings_logs_when_temp_unlink_fails(
58
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
59
+ ) -> None:
60
+ """A swallowed unlink in the finally block must surface to stderr.
61
+
62
+ Forces a write success followed by os.replace failure so the temp file
63
+ survives into the finally branch, then makes Path.unlink raise.
64
+ """
65
+ settings_path = tmp_path / "settings.json"
66
+ settings_path.write_text('{"existing": true}\n', encoding="utf-8")
67
+
68
+ def failing_replace(*_args: object, **_kwargs: object) -> None:
69
+ raise OSError("replace blocked by AV")
70
+
71
+ def failing_unlink(self: Path, *args: object, **kwargs: object) -> None:
72
+ raise PermissionError("temp file held by AV")
73
+
74
+ with patch.object(common_module.os, "replace", failing_replace):
75
+ with patch.object(Path, "unlink", failing_unlink):
76
+ with pytest.raises(SystemExit):
77
+ save_settings(settings_path, {"new_key": "value"})
78
+ captured = capsys.readouterr()
79
+ assert ".tmp" in captured.err
80
+ assert "PermissionError" in captured.err or "held by AV" in captured.err
81
+
82
+
83
+ def test_save_settings_finally_skips_unlink_when_no_temp_was_created(
84
+ tmp_path: Path,
85
+ ) -> None:
86
+ """When this invocation never created the temp file, finally must not unlink it."""
87
+ settings_path = tmp_path / "settings.json"
88
+ settings_path.write_text('{"existing": true}\n', encoding="utf-8")
89
+
90
+ unlink_call_paths: list[Path] = []
91
+ original_unlink = Path.unlink
92
+
93
+ def recording_unlink(self: Path, *args: object, **kwargs: object) -> None:
94
+ unlink_call_paths.append(self)
95
+ original_unlink(self, *args, **kwargs)
96
+
97
+ def write_raises(*_args: object, **_kwargs: object) -> None:
98
+ raise FileExistsError("another writer's temp")
99
+
100
+ with patch.object(common_module, "write_atomically_with_mode", write_raises):
101
+ with patch.object(Path, "unlink", recording_unlink):
102
+ with pytest.raises(SystemExit):
103
+ common_module.save_settings(settings_path, {"new_key": "value"})
104
+ assert all(
105
+ each_path.suffix != ".tmp"
106
+ for each_path in unlink_call_paths
107
+ ), (
108
+ "finally must not unlink a temp file this invocation never created"
109
+ )
110
+
111
+
112
+ def test_default_settings_file_mode_used_when_settings_file_missing(
113
+ tmp_path: Path,
114
+ ) -> None:
115
+ """get_mode_to_preserve must fall back to DEFAULT_SETTINGS_FILE_MODE."""
116
+ missing_settings_path = tmp_path / "no_such_file.json"
117
+ returned_mode = common_module.get_mode_to_preserve(missing_settings_path)
118
+ assert returned_mode == DEFAULT_SETTINGS_FILE_MODE
119
+
120
+
121
+ def test_is_valid_project_root_helper_is_not_orphaned_in_common_module() -> None:
122
+ """The orphan helper in the common module must be removed.
123
+
124
+ Both grant and revoke keep their own local copies and consume them from
125
+ module scope; the common-module copy was dead code with zero call sites.
126
+ """
127
+ assert not hasattr(common_module, "is_valid_project_root"), (
128
+ "is_valid_project_root must not live in _claude_permissions_common — "
129
+ "neither grant nor revoke imports it from there"
130
+ )
131
+ assert callable(grant_module.is_valid_project_root)
132
+ assert callable(revoke_module.is_valid_project_root)
133
+
134
+
135
+ def _reload_with_stale_config_cache(module_name: str) -> ModuleType:
136
+ fake_submodule_name = "config.claude_permissions_common_constants"
137
+ fake_parent_name = "config"
138
+ sentinel_module_a = ModuleType(fake_parent_name)
139
+ sentinel_module_b = ModuleType(fake_submodule_name)
140
+ sys.modules[fake_parent_name] = sentinel_module_a
141
+ sys.modules[fake_submodule_name] = sentinel_module_b
142
+ try:
143
+ target_module = sys.modules.get(module_name)
144
+ if target_module is None:
145
+ target_module = importlib.import_module(module_name)
146
+ else:
147
+ target_module = importlib.reload(target_module)
148
+ finally:
149
+ sys.modules.pop(fake_parent_name, None)
150
+ sys.modules.pop(fake_submodule_name, None)
151
+ return target_module
152
+
153
+
154
+ def test_grant_module_import_evicts_cached_config_submodules(
155
+ tmp_path: Path,
156
+ ) -> None:
157
+ """grant_project_claude_permissions must evict cached `config.*` on import.
158
+
159
+ Regression for loop1-2: without a defensive cache pop above sys.path.insert,
160
+ a cached `config` package shadows scripts/config/ and the from-import raises.
161
+ Calls the rebound `is_valid_project_root` to confirm the real implementation
162
+ survived the cache eviction (a stale shadow would either raise on import or
163
+ bind a placeholder that returns the wrong value).
164
+ """
165
+ reloaded_module = _reload_with_stale_config_cache(
166
+ "grant_project_claude_permissions"
167
+ )
168
+ real_project_root = tmp_path / "project_root"
169
+ (real_project_root / ".claude").mkdir(parents=True)
170
+ bare_directory = tmp_path / "no_claude_marker"
171
+ bare_directory.mkdir()
172
+ assert reloaded_module.is_valid_project_root(real_project_root) is True
173
+ assert reloaded_module.is_valid_project_root(bare_directory) is False
174
+
175
+
176
+ def test_revoke_module_import_evicts_cached_config_submodules(
177
+ tmp_path: Path,
178
+ ) -> None:
179
+ """revoke_project_claude_permissions must evict cached `config.*` on import.
180
+
181
+ Regression for loop1-3: without a defensive cache pop above sys.path.insert,
182
+ a cached `config` package shadows scripts/config/ and the from-import raises.
183
+ Calls the rebound `is_valid_project_root` to confirm the real implementation
184
+ survived the cache eviction (a stale shadow would either raise on import or
185
+ bind a placeholder that returns the wrong value).
186
+ """
187
+ reloaded_module = _reload_with_stale_config_cache(
188
+ "revoke_project_claude_permissions"
189
+ )
190
+ real_project_root = tmp_path / "project_root"
191
+ (real_project_root / ".claude").mkdir(parents=True)
192
+ bare_directory = tmp_path / "no_claude_marker"
193
+ bare_directory.mkdir()
194
+ assert reloaded_module.is_valid_project_root(real_project_root) is True
195
+ assert reloaded_module.is_valid_project_root(bare_directory) is False
@@ -0,0 +1,55 @@
1
+ """Regression tests for grant_project_claude_permissions module-import behavior.
2
+
3
+ Pins the loop1-2 fix: a defensive cache pop above sys.path.insert evicts every
4
+ cached `config` and `config.<submodule>` entry so the from-import binds against
5
+ scripts/config/ rather than a stale parent package shadowing it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import sys
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ _script_directory = str(Path(__file__).resolve().parent)
17
+ if _script_directory not in sys.path:
18
+ sys.path.insert(0, _script_directory)
19
+
20
+
21
+ def _reload_grant_with_stale_config_cache() -> ModuleType:
22
+ fake_submodule_name = "config.claude_permissions_common_constants"
23
+ fake_parent_name = "config"
24
+ sentinel_module_a = ModuleType(fake_parent_name)
25
+ sentinel_module_b = ModuleType(fake_submodule_name)
26
+ sys.modules[fake_parent_name] = sentinel_module_a
27
+ sys.modules[fake_submodule_name] = sentinel_module_b
28
+ try:
29
+ target_module = sys.modules.get("grant_project_claude_permissions")
30
+ if target_module is None:
31
+ target_module = importlib.import_module("grant_project_claude_permissions")
32
+ else:
33
+ target_module = importlib.reload(target_module)
34
+ finally:
35
+ sys.modules.pop(fake_parent_name, None)
36
+ sys.modules.pop(fake_submodule_name, None)
37
+ return target_module
38
+
39
+
40
+ def test_grant_module_imports_when_config_is_already_cached(tmp_path: Path) -> None:
41
+ """Module import must succeed even when sys.modules carries a stale `config`.
42
+
43
+ Regression for loop1-2 — invokes is_valid_project_root after reload to
44
+ prove the binding came from scripts/config/ rather than the sentinel.
45
+ """
46
+ reloaded_module = _reload_grant_with_stale_config_cache()
47
+ not_a_project_root = tmp_path / "empty_dir"
48
+ not_a_project_root.mkdir()
49
+ assert reloaded_module.is_valid_project_root(not_a_project_root) is False, (
50
+ "is_valid_project_root must run normally after the reload — proof that "
51
+ "the from-import bound real constants, not the stale cached ones"
52
+ )
53
+ a_git_project_root = tmp_path / "git_project"
54
+ (a_git_project_root / ".git").mkdir(parents=True)
55
+ assert reloaded_module.is_valid_project_root(a_git_project_root) is True
@@ -0,0 +1,76 @@
1
+ """Tests for probe_code_rules_enforcer_check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import pytest
10
+
11
+ _script_directory = str(Path(__file__).resolve().parent)
12
+ if _script_directory not in sys.path:
13
+ sys.path.insert(0, _script_directory)
14
+
15
+ import probe_code_rules_enforcer_check as probe_module
16
+ from probe_code_rules_enforcer_check import main, run_probe
17
+
18
+
19
+ class _FakeEnforcerModule:
20
+ @staticmethod
21
+ def check_dummy(content: str, reported_path: str) -> list[str]:
22
+ if "trigger" in content:
23
+ return [f"{reported_path}: trigger detected"]
24
+ return []
25
+
26
+
27
+ def _install_fake_loader(
28
+ monkeypatch: pytest.MonkeyPatch,
29
+ fake_module: Any,
30
+ ) -> None:
31
+ monkeypatch.setattr(probe_module, "_load_enforcer_module", lambda: fake_module)
32
+
33
+
34
+ def test_run_probe_returns_check_function_output(
35
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
36
+ ) -> None:
37
+ _install_fake_loader(monkeypatch, _FakeEnforcerModule())
38
+ fixture = tmp_path / "fixture.py"
39
+ fixture.write_text("trigger me", encoding="utf-8")
40
+ issues = run_probe("check_dummy", str(fixture), "reported.py")
41
+ assert issues == ["reported.py: trigger detected"]
42
+
43
+
44
+ def test_run_probe_raises_when_check_function_missing(
45
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
46
+ ) -> None:
47
+ _install_fake_loader(monkeypatch, _FakeEnforcerModule())
48
+ fixture = tmp_path / "fixture.py"
49
+ fixture.write_text("no-op", encoding="utf-8")
50
+ with pytest.raises(AttributeError):
51
+ run_probe("does_not_exist", str(fixture), "reported.py")
52
+
53
+
54
+ def test_main_prints_each_issue(
55
+ tmp_path: Path,
56
+ monkeypatch: pytest.MonkeyPatch,
57
+ capsys: pytest.CaptureFixture[str],
58
+ ) -> None:
59
+ _install_fake_loader(monkeypatch, _FakeEnforcerModule())
60
+ fixture = tmp_path / "fixture.py"
61
+ fixture.write_text("trigger me", encoding="utf-8")
62
+ exit_code = main(
63
+ ["probe_code_rules_enforcer_check.py", "check_dummy", str(fixture)]
64
+ )
65
+ captured = capsys.readouterr()
66
+ assert exit_code == 0
67
+ assert "trigger detected" in captured.out
68
+
69
+
70
+ def test_main_returns_usage_exit_code_when_argv_count_wrong(
71
+ capsys: pytest.CaptureFixture[str],
72
+ ) -> None:
73
+ exit_code = main(["probe_code_rules_enforcer_check.py"])
74
+ captured = capsys.readouterr()
75
+ assert exit_code != 0
76
+ assert "usage" in captured.err.lower()
@@ -0,0 +1,55 @@
1
+ """Regression tests for revoke_project_claude_permissions module-import behavior.
2
+
3
+ Pins the loop1-3 fix: a defensive cache pop above sys.path.insert evicts every
4
+ cached `config` and `config.<submodule>` entry so the from-import binds against
5
+ scripts/config/ rather than a stale parent package shadowing it.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import sys
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ _script_directory = str(Path(__file__).resolve().parent)
17
+ if _script_directory not in sys.path:
18
+ sys.path.insert(0, _script_directory)
19
+
20
+
21
+ def _reload_revoke_with_stale_config_cache() -> ModuleType:
22
+ fake_submodule_name = "config.claude_permissions_common_constants"
23
+ fake_parent_name = "config"
24
+ sentinel_module_a = ModuleType(fake_parent_name)
25
+ sentinel_module_b = ModuleType(fake_submodule_name)
26
+ sys.modules[fake_parent_name] = sentinel_module_a
27
+ sys.modules[fake_submodule_name] = sentinel_module_b
28
+ try:
29
+ target_module = sys.modules.get("revoke_project_claude_permissions")
30
+ if target_module is None:
31
+ target_module = importlib.import_module("revoke_project_claude_permissions")
32
+ else:
33
+ target_module = importlib.reload(target_module)
34
+ finally:
35
+ sys.modules.pop(fake_parent_name, None)
36
+ sys.modules.pop(fake_submodule_name, None)
37
+ return target_module
38
+
39
+
40
+ def test_revoke_module_imports_when_config_is_already_cached(tmp_path: Path) -> None:
41
+ """Module import must succeed even when sys.modules carries a stale `config`.
42
+
43
+ Regression for loop1-3 — invokes is_valid_project_root after reload to
44
+ prove the binding came from scripts/config/ rather than the sentinel.
45
+ """
46
+ reloaded_module = _reload_revoke_with_stale_config_cache()
47
+ not_a_project_root = tmp_path / "empty_dir"
48
+ not_a_project_root.mkdir()
49
+ assert reloaded_module.is_valid_project_root(not_a_project_root) is False, (
50
+ "is_valid_project_root must run normally after the reload — proof that "
51
+ "the from-import bound real constants, not the stale cached ones"
52
+ )
53
+ a_claude_project_root = tmp_path / "claude_project"
54
+ (a_claude_project_root / ".claude").mkdir(parents=True)
55
+ assert reloaded_module.is_valid_project_root(a_claude_project_root) is True
@@ -0,0 +1,108 @@
1
+ """Tests for windows_safe_rmtree."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import stat
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _script_directory = str(Path(__file__).resolve().parent)
13
+ if _script_directory not in sys.path:
14
+ sys.path.insert(0, _script_directory)
15
+
16
+ from unittest.mock import patch
17
+
18
+ from windows_safe_rmtree import _strip_read_only_and_retry, main, remove_tree
19
+
20
+
21
+ def test_strip_read_only_and_retry_logs_when_retry_still_fails(
22
+ capsys: pytest.CaptureFixture[str],
23
+ tmp_path: Path,
24
+ ) -> None:
25
+ """The chmod-then-retry handler must surface residual failures to stderr."""
26
+ target_path = tmp_path / "locked_file.txt"
27
+ target_path.write_text("payload", encoding="utf-8")
28
+
29
+ def always_fails(_path: str) -> None:
30
+ raise PermissionError("file held by another process")
31
+
32
+ _strip_read_only_and_retry(always_fails, str(target_path), None, None, None)
33
+ captured = capsys.readouterr()
34
+ assert str(target_path) in captured.err
35
+ assert "PermissionError" in captured.err or "held by another process" in captured.err
36
+
37
+
38
+ def test_remove_tree_returns_nonzero_when_residual_oserror(
39
+ capsys: pytest.CaptureFixture[str],
40
+ tmp_path: Path,
41
+ ) -> None:
42
+ """When shutil.rmtree raises after handler retries, remove_tree must signal."""
43
+ target_path = tmp_path / "ghost"
44
+ target_path.mkdir()
45
+ with patch(
46
+ "shutil.rmtree",
47
+ side_effect=PermissionError("residual lock"),
48
+ ):
49
+ exit_code = remove_tree(str(target_path))
50
+ captured = capsys.readouterr()
51
+ assert exit_code != 0, "remove_tree must report failure when rmtree raises"
52
+ assert str(target_path) in captured.err
53
+ assert "residual lock" in captured.err or "PermissionError" in captured.err
54
+
55
+
56
+ def test_main_propagates_remove_tree_failure(
57
+ capsys: pytest.CaptureFixture[str],
58
+ tmp_path: Path,
59
+ ) -> None:
60
+ """main must return non-zero when remove_tree could not finish cleanup."""
61
+ target_path = tmp_path / "stubborn"
62
+ target_path.mkdir()
63
+ with patch(
64
+ "shutil.rmtree",
65
+ side_effect=PermissionError("still locked"),
66
+ ):
67
+ exit_code = main(["windows_safe_rmtree.py", str(target_path)])
68
+ assert exit_code != 0
69
+
70
+
71
+ def test_remove_tree_deletes_plain_directory(tmp_path: Path) -> None:
72
+ target = tmp_path / "victim"
73
+ target.mkdir()
74
+ (target / "file.txt").write_text("payload", encoding="utf-8")
75
+ remove_tree(str(target))
76
+ assert not target.exists()
77
+
78
+
79
+ def test_remove_tree_handles_read_only_file(tmp_path: Path) -> None:
80
+ target = tmp_path / "victim"
81
+ target.mkdir()
82
+ locked_file = target / "locked.txt"
83
+ locked_file.write_text("payload", encoding="utf-8")
84
+ os.chmod(locked_file, stat.S_IREAD)
85
+ remove_tree(str(target))
86
+ assert not target.exists()
87
+
88
+
89
+ def test_remove_tree_swallows_missing_path(tmp_path: Path) -> None:
90
+ missing_path = tmp_path / "does-not-exist"
91
+ remove_tree(str(missing_path))
92
+
93
+
94
+ def test_main_returns_zero_on_success(tmp_path: Path) -> None:
95
+ target = tmp_path / "victim"
96
+ target.mkdir()
97
+ exit_code = main(["windows_safe_rmtree.py", str(target)])
98
+ assert exit_code == 0
99
+ assert not target.exists()
100
+
101
+
102
+ def test_main_returns_usage_exit_code_when_argv_count_wrong(
103
+ capsys: pytest.CaptureFixture[str],
104
+ ) -> None:
105
+ exit_code = main(["windows_safe_rmtree.py"])
106
+ captured = capsys.readouterr()
107
+ assert exit_code != 0
108
+ assert "usage" in captured.err.lower()
@@ -0,0 +1,100 @@
1
+ """Recursively remove a directory tree, stripping Windows ReadOnly attributes.
2
+
3
+ Required by ~/.claude/rules/windows-filesystem-safe.md so bugteam teardown does
4
+ not silently swallow Windows ReadOnly-attribute failures the way the unsafe
5
+ shutil ignore-errors flag does.
6
+
7
+ Usage:
8
+ python windows_safe_rmtree.py <absolute-path>
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import shutil
15
+ import stat
16
+ import sys
17
+ from collections.abc import Callable
18
+
19
+ from config.windows_safe_rmtree_constants import (
20
+ EXIT_CODE_REMOVE_TREE_FAILURE,
21
+ EXIT_CODE_USAGE_ERROR,
22
+ EXPECTED_ARGUMENT_COUNT,
23
+ ONEXC_PYTHON_MAJOR_VERSION,
24
+ ONEXC_PYTHON_MINOR_VERSION,
25
+ )
26
+
27
+
28
+ def _strip_read_only_and_retry(
29
+ removal_function: Callable[[str], None],
30
+ target_path: str,
31
+ *_exc_info: object,
32
+ ) -> None:
33
+ try:
34
+ os.chmod(target_path, os.stat(target_path).st_mode | stat.S_IWRITE)
35
+ removal_function(target_path)
36
+ except OSError as residual_error:
37
+ sys.stderr.write(
38
+ f"windows_safe_rmtree: chmod-and-retry could not remove {target_path}: "
39
+ f"{type(residual_error).__name__}: {residual_error}\n"
40
+ )
41
+
42
+
43
+ def _select_handler_keyword() -> dict[str, Callable[..., None]]:
44
+ onexc_required_version = (
45
+ ONEXC_PYTHON_MAJOR_VERSION,
46
+ ONEXC_PYTHON_MINOR_VERSION,
47
+ )
48
+ if sys.version_info >= onexc_required_version:
49
+ return {"onexc": _strip_read_only_and_retry}
50
+ return {"onerror": _strip_read_only_and_retry}
51
+
52
+
53
+ def remove_tree(target_path: str) -> int:
54
+ """Recursively remove a directory tree, handling Windows ReadOnly attributes.
55
+
56
+ Args:
57
+ target_path: Absolute path to the directory tree to remove.
58
+
59
+ Returns:
60
+ Zero when the tree was removed (or never existed). Non-zero when
61
+ the chmod-and-retry handler could not finish cleanup; callers must
62
+ treat a non-zero return as "tree may still be present".
63
+ """
64
+ if not os.path.exists(target_path):
65
+ return 0
66
+ handler_keyword = _select_handler_keyword()
67
+ try:
68
+ shutil.rmtree(target_path, **handler_keyword)
69
+ except OSError as residual_error:
70
+ sys.stderr.write(
71
+ f"windows_safe_rmtree: residual failure removing {target_path}: "
72
+ f"{type(residual_error).__name__}: {residual_error}\n"
73
+ )
74
+ return EXIT_CODE_REMOVE_TREE_FAILURE
75
+ return 0
76
+
77
+
78
+ def _print_usage_to_stderr() -> None:
79
+ sys.stderr.write("usage: python windows_safe_rmtree.py <absolute-path>\n")
80
+
81
+
82
+ def main(all_arguments: list[str]) -> int:
83
+ """Parse command-line arguments and invoke remove_tree.
84
+
85
+ Args:
86
+ all_arguments: Command-line arguments including script name
87
+ and the target directory path.
88
+
89
+ Returns:
90
+ Exit code 0 on success, EXIT_CODE_USAGE_ERROR on invalid usage,
91
+ EXIT_CODE_REMOVE_TREE_FAILURE when remove_tree could not finish.
92
+ """
93
+ if len(all_arguments) != EXPECTED_ARGUMENT_COUNT:
94
+ _print_usage_to_stderr()
95
+ return EXIT_CODE_USAGE_ERROR
96
+ return remove_tree(all_arguments[1])
97
+
98
+
99
+ if __name__ == "__main__":
100
+ sys.exit(main(sys.argv))
@@ -10,21 +10,11 @@ def _read_skill_text() -> str:
10
10
  return skill_path.read_text(encoding="utf-8")
11
11
 
12
12
 
13
- def test_skill_references_fix_implementer_env_var():
14
- skill_text = _read_skill_text()
15
- assert "BUGTEAM_FIX_IMPLEMENTER" in skill_text
16
-
17
-
18
- def test_skill_names_default_implementer_subagent_type():
13
+ def test_skill_names_implementer_subagent_type():
19
14
  skill_text = _read_skill_text()
20
15
  assert "clean-coder" in skill_text
21
16
 
22
17
 
23
- def test_skill_names_optional_groq_implementer_subagent_type():
24
- skill_text = _read_skill_text()
25
- assert "groq-coder" in skill_text
26
-
27
-
28
18
  def test_skill_documents_bugbot_retrigger_flag():
29
19
  skill_text = _read_skill_text()
30
20
  assert "--bugbot-retrigger" in skill_text