claude-dev-env 1.83.0 → 1.85.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 (186) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
  6. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  7. package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
  8. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  9. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  13. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
  14. package/agents/clean-coder.md +3 -0
  15. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  16. package/bin/install.mjs +16 -8
  17. package/bin/install.test.mjs +17 -6
  18. package/commands/CLAUDE.md +0 -1
  19. package/docs/CODE_RULES.md +1 -1
  20. package/hooks/blocking/CLAUDE.md +0 -4
  21. package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
  22. package/hooks/blocking/code_rules_constants_config.py +5 -0
  23. package/hooks/blocking/code_rules_docstrings.py +133 -19
  24. package/hooks/blocking/code_rules_enforcer.py +4 -0
  25. package/hooks/blocking/code_rules_imports_logging.py +10 -6
  26. package/hooks/blocking/code_rules_magic_values.py +5 -0
  27. package/hooks/blocking/code_rules_naming_collection.py +15 -8
  28. package/hooks/blocking/code_rules_paired_test.py +3 -2
  29. package/hooks/blocking/code_rules_shared.py +34 -0
  30. package/hooks/blocking/code_rules_string_magic.py +1 -1
  31. package/hooks/blocking/code_rules_unused_imports.py +2 -2
  32. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  33. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
  37. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  38. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  39. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  40. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  41. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  42. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  43. package/hooks/hooks_constants/CLAUDE.md +0 -3
  44. package/hooks/hooks_constants/blocking_check_limits.py +4 -0
  45. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  46. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  47. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  48. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  49. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  50. package/hooks/workflow/CLAUDE.md +2 -8
  51. package/package.json +1 -1
  52. package/rules/CLAUDE.md +1 -1
  53. package/rules/docstring-prose-matches-implementation.md +1 -1
  54. package/rules/env-var-table-code-drift.md +1 -1
  55. package/rules/package-inventory-stale-entry.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/plain-illustrative-docstrings.md +35 -1
  58. package/skills/CLAUDE.md +6 -1
  59. package/skills/_shared/pr-loop/CLAUDE.md +1 -0
  60. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  61. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  62. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
  63. package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
  64. package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
  65. package/skills/autoconverge/SKILL.md +82 -58
  66. package/skills/autoconverge/reference/closing-report.md +6 -6
  67. package/skills/autoconverge/reference/convergence.md +17 -15
  68. package/skills/autoconverge/reference/gotchas.md +6 -3
  69. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  70. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  71. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  72. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  73. package/skills/autoconverge/workflow/converge.mjs +71 -74
  74. package/skills/autoconverge/workflow/render_report.py +7 -11
  75. package/skills/bugteam/CLAUDE.md +1 -1
  76. package/skills/bugteam/PROMPTS.md +7 -6
  77. package/skills/bugteam/SKILL.md +25 -72
  78. package/skills/bugteam/reference/CLAUDE.md +1 -3
  79. package/skills/bugteam/reference/README.md +1 -1
  80. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  81. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  82. package/skills/bugteam/reference/team-setup.md +8 -7
  83. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  84. package/skills/bugteam/scripts/README.md +0 -4
  85. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  86. package/skills/code/SKILL.md +2 -0
  87. package/skills/copilot-review/CLAUDE.md +1 -1
  88. package/skills/copilot-review/SKILL.md +25 -23
  89. package/skills/findbugs/CLAUDE.md +2 -2
  90. package/skills/findbugs/SKILL.md +22 -83
  91. package/skills/fixbugs/SKILL.md +2 -4
  92. package/skills/log-audit/CLAUDE.md +20 -0
  93. package/skills/log-audit/SKILL.md +68 -0
  94. package/skills/log-audit/reference/CLAUDE.md +9 -0
  95. package/skills/log-audit/reference/charter.md +52 -0
  96. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  97. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  98. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  99. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  100. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  101. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  102. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  103. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  104. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  105. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  106. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  107. package/skills/monitor-open-prs/SKILL.md +2 -2
  108. package/skills/post-audit-findings/SKILL.md +84 -0
  109. package/skills/pr-converge/CLAUDE.md +2 -0
  110. package/skills/pr-converge/SKILL.md +99 -60
  111. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  112. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  113. package/skills/pr-converge/reference/examples.md +5 -5
  114. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  115. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  116. package/skills/pr-converge/reference/per-tick.md +24 -45
  117. package/skills/pr-converge/reference/state-schema.md +27 -0
  118. package/skills/pr-converge/scripts/README.md +3 -5
  119. package/skills/pr-fix-protocol/SKILL.md +70 -0
  120. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  121. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  122. package/skills/pr-scope-resolve/SKILL.md +48 -0
  123. package/skills/qbug/CLAUDE.md +4 -4
  124. package/skills/qbug/SKILL.md +46 -144
  125. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  126. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  127. package/skills/reviewer-gates/SKILL.md +96 -0
  128. package/skills/session-log/CLAUDE.md +7 -7
  129. package/skills/session-log/SKILL.md +27 -44
  130. package/skills/test_markdown_link_integrity.py +103 -0
  131. package/commands/doc-gist.md +0 -16
  132. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  133. package/hooks/blocking/conftest.py +0 -30
  134. package/hooks/blocking/md_path_exemptions.py +0 -224
  135. package/hooks/blocking/md_to_html_blocker.py +0 -155
  136. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  137. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  138. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  139. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  140. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  141. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  142. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  143. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  144. package/hooks/workflow/md_to_html_companion.py +0 -358
  145. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  146. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  147. package/skills/bugteam/reference/audit-contract.md +0 -163
  148. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  149. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  150. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  151. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  152. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  153. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  154. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  155. package/skills/doc-gist/CLAUDE.md +0 -25
  156. package/skills/doc-gist/SKILL.md +0 -97
  157. package/skills/doc-gist/references/CLAUDE.md +0 -9
  158. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  159. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  160. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  161. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  162. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  163. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  164. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  165. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  166. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  167. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  168. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  169. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  170. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  171. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  172. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  173. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  174. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  175. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  176. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  177. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  178. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  179. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  180. package/skills/doc-gist/references/examples/README.md +0 -5
  181. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  182. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  183. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  184. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  185. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  186. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -1,160 +0,0 @@
1
- """TDD-pair tests for the underscore-prefixed _bugteam_permissions_common module.
2
-
3
- The TDD enforcer matches a production filename ``X.py`` to ``test_X.py``;
4
- ``_bugteam_permissions_common.py`` carries a leading underscore that the
5
- enforcer treats as part of the name. This file's tests are the canonical
6
- match. The broader behavioral suite continues to live alongside, in
7
- ``test_bugteam_permissions_common.py``.
8
- """
9
- from __future__ import annotations
10
-
11
- import sys
12
- from pathlib import Path
13
- from unittest.mock import patch
14
-
15
- import pytest
16
-
17
- _script_directory = str(Path(__file__).resolve().parent)
18
- if _script_directory not in sys.path:
19
- sys.path.insert(0, _script_directory)
20
-
21
- import _bugteam_permissions_common as common_module
22
- import grant_project_claude_permissions as grant_module
23
- import revoke_project_claude_permissions as revoke_module
24
-
25
-
26
- def test_is_valid_project_root_not_defined_on_common_module() -> None:
27
- """The common module must not expose ``is_valid_project_root``.
28
-
29
- Both grant and revoke define their own local copy. A shared copy on
30
- the common module would be a third parallel definition with no
31
- production caller, so its absence is the asserted contract.
32
- """
33
- assert not hasattr(common_module, "is_valid_project_root")
34
-
35
-
36
- def test_grant_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
37
- git_project_root = tmp_path / "git_project"
38
- (git_project_root / ".git").mkdir(parents=True)
39
- assert grant_module.is_valid_project_root(git_project_root) is True
40
-
41
-
42
- def test_grant_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
43
- claude_project_root = tmp_path / "claude_project"
44
- (claude_project_root / ".claude").mkdir(parents=True)
45
- assert grant_module.is_valid_project_root(claude_project_root) is True
46
-
47
-
48
- def test_grant_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
49
- unmarked_directory = tmp_path / "no_marker"
50
- unmarked_directory.mkdir()
51
- assert grant_module.is_valid_project_root(unmarked_directory) is False
52
-
53
-
54
- def test_revoke_is_valid_project_root_detects_git_marker(tmp_path: Path) -> None:
55
- git_project_root = tmp_path / "git_project"
56
- (git_project_root / ".git").mkdir(parents=True)
57
- assert revoke_module.is_valid_project_root(git_project_root) is True
58
-
59
-
60
- def test_revoke_is_valid_project_root_detects_claude_marker(tmp_path: Path) -> None:
61
- claude_project_root = tmp_path / "claude_project"
62
- (claude_project_root / ".claude").mkdir(parents=True)
63
- assert revoke_module.is_valid_project_root(claude_project_root) is True
64
-
65
-
66
- def test_revoke_is_valid_project_root_rejects_unmarked_directory(tmp_path: Path) -> None:
67
- unmarked_directory = tmp_path / "no_marker"
68
- unmarked_directory.mkdir()
69
- assert revoke_module.is_valid_project_root(unmarked_directory) is False
70
-
71
-
72
- def test_write_atomically_with_mode_releases_fd_when_fdopen_raises(
73
- tmp_path: Path,
74
- ) -> None:
75
- """A failure inside os.fdopen must close the raw file descriptor."""
76
- target_path = tmp_path / "settings.json.tmp"
77
- with patch.object(
78
- common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
79
- ):
80
- with pytest.raises(MemoryError):
81
- common_module.write_atomically_with_mode(
82
- target_path, "payload", file_mode=0o600
83
- )
84
-
85
-
86
- def test_get_mode_to_preserve_returns_existing_file_mode(
87
- tmp_path: Path,
88
- ) -> None:
89
- """When the file exists, the actual filesystem mode must be returned (not the default)."""
90
- target_path = tmp_path / "settings.json"
91
- target_path.write_text("{}", encoding="utf-8")
92
- actual_filesystem_mode = target_path.stat().st_mode & 0o777
93
- returned_mode = common_module.get_mode_to_preserve(target_path)
94
- assert returned_mode == actual_filesystem_mode
95
-
96
-
97
- def test_write_atomically_with_mode_raises_oserror_when_open_fails(
98
- tmp_path: Path,
99
- ) -> None:
100
- """OSError from os.open must propagate (no fd leak path to test here)."""
101
- target_path = tmp_path / "subdirectory" / "missing" / "settings.json.tmp"
102
- with pytest.raises(OSError):
103
- common_module.write_atomically_with_mode(
104
- target_path, "payload", file_mode=0o600
105
- )
106
-
107
-
108
- def test_write_atomically_unlinks_temp_when_fdopen_raises(
109
- tmp_path: Path,
110
- ) -> None:
111
- """A failure inside os.fdopen must remove the on-disk temp file.
112
-
113
- Regression for loop1-9: the file existed the moment os.open returned,
114
- but the OSError/MemoryError handler only closed the raw FD — leaving an
115
- empty .tmp sibling on disk after the exception propagated up to
116
- save_settings, where the unlink in the finally block was skipped because
117
- `is_temp_owned_by_this_invocation` had not yet been set.
118
- """
119
- target_path = tmp_path / "settings.json.tmp"
120
- with patch.object(
121
- common_module.os, "fdopen", side_effect=MemoryError("fdopen failure")
122
- ):
123
- with pytest.raises(MemoryError):
124
- common_module.write_atomically_with_mode(
125
- target_path, "payload", file_mode=0o600
126
- )
127
- assert not target_path.exists(), (
128
- "the temp file created by os.open must be unlinked before re-raising"
129
- )
130
-
131
-
132
- def test_save_settings_uses_pid_keyed_temp_suffix(tmp_path: Path) -> None:
133
- """Concurrent save_settings invocations must not race on a deterministic temp name.
134
-
135
- Regression for loop1-10: building the temp path as `settings.json.tmp` is
136
- deterministic and unkeyed by PID, so two parallel /bugteam invocations
137
- racing on Step 0 (grant) or Step 5 (revoke) collide on O_CREAT|O_EXCL —
138
- the second caller hits FileExistsError and the OSError handler hard-exits,
139
- silently dropping that PR's permission grant.
140
- """
141
- settings_path = tmp_path / "settings.json"
142
- settings_path.write_text("{}", encoding="utf-8")
143
- captured_temp_paths: list[str] = []
144
- real_open = common_module.os.open
145
-
146
- def capturing_open(target: str, *args: object, **kwargs: object) -> int:
147
- captured_temp_paths.append(target)
148
- return real_open(target, *args, **kwargs)
149
-
150
- with patch.object(common_module.os, "open", side_effect=capturing_open):
151
- common_module.save_settings(settings_path, {"first": True})
152
- common_module.save_settings(settings_path, {"second": True})
153
- assert len(captured_temp_paths) == 2
154
- assert all(
155
- str(common_module.os.getpid()) in each_temp_path
156
- for each_temp_path in captured_temp_paths
157
- ), (
158
- "temp filename must include os.getpid() so concurrent runs do not "
159
- f"collide on a deterministic name; saw: {captured_temp_paths!r}"
160
- )
@@ -1,356 +0,0 @@
1
- """Behavior tests for the agent-config carve-out and stale-trust-entry purge.
2
-
3
- Covers two Bugbot findings on PR #467:
4
- - Deny rules must be written to permissions.deny so agent-config edits
5
- require explicit per-edit user approval.
6
- - Trust entries in autoMode.environment must be purged on grant
7
- (preventing accumulation across template revisions) and removed on
8
- revoke regardless of the exact template wording.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import json
14
- import sys
15
- from pathlib import Path
16
- from typing import Any
17
-
18
- import pytest
19
-
20
- _script_directory = str(Path(__file__).resolve().parent)
21
- if _script_directory not in sys.path:
22
- sys.path.insert(0, _script_directory)
23
-
24
- import _bugteam_permissions_common as common_module
25
- import grant_project_claude_permissions as grant_module
26
- import revoke_project_claude_permissions as revoke_module
27
- from bugteam_scripts_constants.claude_permissions_common_constants import (
28
- ALL_AGENT_CONFIG_DENY_TOOLS,
29
- ALL_AGENT_CONFIG_PATH_PATTERNS,
30
- ALL_PERMISSION_ALLOW_TOOLS,
31
- AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE,
32
- )
33
-
34
-
35
- def _make_fake_project(tmp_path: Path) -> Path:
36
- fake_project_root = tmp_path / "fake_project"
37
- (fake_project_root / ".claude").mkdir(parents=True)
38
- return fake_project_root
39
-
40
-
41
- def _project_path_as_posix(fake_project_root: Path) -> str:
42
- return str(fake_project_root).replace("\\", "/")
43
-
44
-
45
- def _redirect_settings_to_fake_path(
46
- fake_settings_path: Path, monkeypatch: pytest.MonkeyPatch
47
- ) -> None:
48
- fake_home_directory = fake_settings_path.parent.parent
49
- monkeypatch.setattr(Path, "home", classmethod(lambda _cls: fake_home_directory))
50
-
51
-
52
- def _prepare_fake_home(tmp_path: Path) -> Path:
53
- fake_home_directory = tmp_path / "fake_home"
54
- claude_settings_directory = fake_home_directory / ".claude"
55
- claude_settings_directory.mkdir(parents=True)
56
- return claude_settings_directory / "settings.json"
57
-
58
-
59
- def _seed_grant_then_run(
60
- fake_settings_path: Path,
61
- fake_project_root: Path,
62
- monkeypatch: pytest.MonkeyPatch,
63
- pre_existing_settings: dict[str, Any],
64
- ) -> None:
65
- fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
66
- _redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
67
- monkeypatch.chdir(fake_project_root)
68
- grant_module.grant_permissions_for_current_directory()
69
-
70
-
71
- def _seed_revoke_then_run(
72
- fake_settings_path: Path,
73
- fake_project_root: Path,
74
- monkeypatch: pytest.MonkeyPatch,
75
- pre_existing_settings: dict[str, Any],
76
- ) -> None:
77
- fake_settings_path.write_text(json.dumps(pre_existing_settings), encoding="utf-8")
78
- _redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
79
- monkeypatch.chdir(fake_project_root)
80
- revoke_module.revoke_permissions_for_current_directory()
81
-
82
-
83
- def test_grant_writes_deny_rules_for_every_tool_and_pattern(
84
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
85
- ) -> None:
86
- fake_project_root = _make_fake_project(tmp_path)
87
- fake_settings_path = _prepare_fake_home(tmp_path)
88
- _seed_grant_then_run(
89
- fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
90
- )
91
- capsys.readouterr()
92
- written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
93
- deny_list = written_settings["permissions"]["deny"]
94
- project_path_posix = _project_path_as_posix(fake_project_root)
95
- for each_tool in ALL_AGENT_CONFIG_DENY_TOOLS:
96
- for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
97
- expected_rule = f"{each_tool}({project_path_posix}/.claude/{each_pattern})"
98
- assert expected_rule in deny_list, (
99
- f"deny list missing expected rule {expected_rule!r}"
100
- )
101
-
102
-
103
- def test_grant_writes_glob_deny_rules_for_every_agent_config_pattern(
104
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
105
- ) -> None:
106
- """Glob must be in the deny tuple so agent-config paths require approval.
107
-
108
- The AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE promises Edit/Write/Read/Glob
109
- trust EXCEPT for agent-config files. Glob deny rules are how the EXCEPT
110
- clause is honored for the Glob tool.
111
- """
112
- fake_project_root = _make_fake_project(tmp_path)
113
- fake_settings_path = _prepare_fake_home(tmp_path)
114
- _seed_grant_then_run(
115
- fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
116
- )
117
- capsys.readouterr()
118
- written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
119
- deny_list = written_settings["permissions"]["deny"]
120
- project_path_posix = _project_path_as_posix(fake_project_root)
121
- assert "Glob" in ALL_AGENT_CONFIG_DENY_TOOLS
122
- assert "Glob" not in ALL_PERMISSION_ALLOW_TOOLS
123
- for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
124
- expected_glob_rule = f"Glob({project_path_posix}/.claude/{each_pattern})"
125
- assert expected_glob_rule in deny_list, (
126
- f"deny list missing expected Glob rule {expected_glob_rule!r}"
127
- )
128
-
129
-
130
- def test_grant_purges_stale_trust_entries_then_writes_current_template(
131
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
132
- ) -> None:
133
- fake_project_root = _make_fake_project(tmp_path)
134
- fake_settings_path = _prepare_fake_home(tmp_path)
135
- project_path_posix = _project_path_as_posix(fake_project_root)
136
- stale_entry_a = (
137
- f"Trusted local workspace: {project_path_posix}/.claude/** old wording form A"
138
- )
139
- stale_entry_b = (
140
- f"Trusted local workspace: {project_path_posix}/.claude/** "
141
- f"different earlier wording"
142
- )
143
- unrelated_entry = "Some unrelated environment hint"
144
- pre_existing_settings: dict[str, Any] = {
145
- "autoMode": {
146
- "environment": [stale_entry_a, stale_entry_b, unrelated_entry],
147
- },
148
- }
149
- _seed_grant_then_run(
150
- fake_settings_path,
151
- fake_project_root,
152
- monkeypatch,
153
- pre_existing_settings=pre_existing_settings,
154
- )
155
- captured = capsys.readouterr()
156
- written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
157
- environment_list = written_settings["autoMode"]["environment"]
158
- assert stale_entry_a not in environment_list
159
- assert stale_entry_b not in environment_list
160
- assert unrelated_entry in environment_list
161
- matching_trust_entries = [
162
- each_entry
163
- for each_entry in environment_list
164
- if isinstance(each_entry, str)
165
- and each_entry.startswith("Trusted local workspace:")
166
- and f"{project_path_posix}/.claude/**" in each_entry
167
- ]
168
- assert len(matching_trust_entries) == 1
169
- assert "Stale auto-mode environment entries purged" in captured.out
170
-
171
-
172
- def test_revoke_removes_deny_rules(
173
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
174
- ) -> None:
175
- fake_project_root = _make_fake_project(tmp_path)
176
- fake_settings_path = _prepare_fake_home(tmp_path)
177
- project_path_posix = _project_path_as_posix(fake_project_root)
178
- all_deny_rules = common_module.build_agent_config_deny_rules(
179
- project_path_posix,
180
- ALL_AGENT_CONFIG_DENY_TOOLS,
181
- ALL_AGENT_CONFIG_PATH_PATTERNS,
182
- )
183
- pre_existing_settings: dict[str, Any] = {
184
- "permissions": {
185
- "deny": list(all_deny_rules),
186
- },
187
- }
188
- _seed_revoke_then_run(
189
- fake_settings_path,
190
- fake_project_root,
191
- monkeypatch,
192
- pre_existing_settings=pre_existing_settings,
193
- )
194
- capsys.readouterr()
195
- written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
196
- permissions_section = written_settings.get("permissions", {})
197
- remaining_deny_list = permissions_section.get("deny", [])
198
- for each_rule in all_deny_rules:
199
- assert each_rule not in remaining_deny_list
200
-
201
-
202
- def test_revoke_removes_every_legacy_trust_entry_for_project(
203
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
204
- ) -> None:
205
- fake_project_root = _make_fake_project(tmp_path)
206
- fake_settings_path = _prepare_fake_home(tmp_path)
207
- project_path_posix = _project_path_as_posix(fake_project_root)
208
- legacy_entry_a = (
209
- f"Trusted local workspace: {project_path_posix}/.claude/** template revision A"
210
- )
211
- legacy_entry_b = (
212
- f"Trusted local workspace: {project_path_posix}/.claude/** template revision B"
213
- )
214
- unrelated_other_project_entry = (
215
- "Trusted local workspace: /some/other/project/.claude/** still valid"
216
- )
217
- pre_existing_settings: dict[str, Any] = {
218
- "autoMode": {
219
- "environment": [
220
- legacy_entry_a,
221
- legacy_entry_b,
222
- unrelated_other_project_entry,
223
- ],
224
- },
225
- }
226
- _seed_revoke_then_run(
227
- fake_settings_path,
228
- fake_project_root,
229
- monkeypatch,
230
- pre_existing_settings=pre_existing_settings,
231
- )
232
- written_settings = json.loads(fake_settings_path.read_text(encoding="utf-8"))
233
- environment_list = written_settings.get("autoMode", {}).get("environment", [])
234
- assert legacy_entry_a not in environment_list
235
- assert legacy_entry_b not in environment_list
236
- assert unrelated_other_project_entry in environment_list
237
-
238
-
239
- def test_template_constant_documents_agent_config_carveout() -> None:
240
- assert "agent-config files always require explicit per-edit user approval" in (
241
- AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
242
- )
243
-
244
-
245
- def test_is_trust_entry_for_project_predicate_filters_by_prefix_and_project_path() -> (
246
- None
247
- ):
248
- project_path_posix = "/fake/proj"
249
- trust_prefix = "Trusted local workspace:"
250
- non_string_value: object = 42
251
- assert (
252
- common_module.is_trust_entry_for_project(
253
- non_string_value, project_path_posix, trust_prefix
254
- )
255
- is False
256
- )
257
- wrong_prefix_entry = (
258
- f"Something else: {project_path_posix}/.claude/** with marker token"
259
- )
260
- assert (
261
- common_module.is_trust_entry_for_project(
262
- wrong_prefix_entry, project_path_posix, trust_prefix
263
- )
264
- is False
265
- )
266
- different_project_entry = (
267
- "Trusted local workspace: /other/project/.claude/** unrelated"
268
- )
269
- assert (
270
- common_module.is_trust_entry_for_project(
271
- different_project_entry, project_path_posix, trust_prefix
272
- )
273
- is False
274
- )
275
- matching_entry = (
276
- f"Trusted local workspace: {project_path_posix}/.claude/** any wording form"
277
- )
278
- assert (
279
- common_module.is_trust_entry_for_project(
280
- matching_entry, project_path_posix, trust_prefix
281
- )
282
- is True
283
- )
284
-
285
-
286
- def test_is_trust_entry_rejects_cross_project_path_suffix_collision() -> None:
287
- """When the project_path is a path suffix of an unrelated entry's path,
288
- the predicate must reject the unrelated entry (the boundary anchor case)."""
289
- short_project_path = "/projects/foo"
290
- trust_prefix = "Trusted local workspace:"
291
- longer_unrelated_path_entry = (
292
- "Trusted local workspace: /Users/jon/projects/foo/.claude/** unrelated path"
293
- )
294
- assert (
295
- common_module.is_trust_entry_for_project(
296
- longer_unrelated_path_entry, short_project_path, trust_prefix
297
- )
298
- is False
299
- )
300
- quoted_matching_entry = (
301
- f'Trusted local workspace: "{short_project_path}/.claude/**" quoted form'
302
- )
303
- assert (
304
- common_module.is_trust_entry_for_project(
305
- quoted_matching_entry, short_project_path, trust_prefix
306
- )
307
- is True
308
- )
309
-
310
-
311
- def test_second_grant_is_idempotent_when_no_other_settings_changed(
312
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
313
- ) -> None:
314
- """Running grant twice in a row must perform zero changes the second time.
315
-
316
- On the second call the existing trust entry is byte-identical to the
317
- freshly-formatted current entry, so purge_stale_trust_entries treats it as
318
- protected and does not remove it; add_auto_mode_environment_entry then
319
- no-ops because the entry is already present.
320
- """
321
- fake_project_root = _make_fake_project(tmp_path)
322
- fake_settings_path = _prepare_fake_home(tmp_path)
323
- _seed_grant_then_run(
324
- fake_settings_path, fake_project_root, monkeypatch, pre_existing_settings={}
325
- )
326
- first_run_output = capsys.readouterr()
327
- assert "No changes needed" not in first_run_output.out
328
- _redirect_settings_to_fake_path(fake_settings_path, monkeypatch)
329
- monkeypatch.chdir(fake_project_root)
330
- grant_module.grant_permissions_for_current_directory()
331
- second_run_output = capsys.readouterr()
332
- assert "No changes needed; settings file left untouched." in second_run_output.out
333
- assert "Stale auto-mode environment entries purged" not in second_run_output.out
334
-
335
-
336
- def test_template_derives_human_readable_pattern_list_from_pattern_tuple() -> None:
337
- """Every pattern in ALL_AGENT_CONFIG_PATH_PATTERNS must surface in the
338
- rendered template through its derived human-readable form, and the
339
- template must still expose the {project_path} placeholder for .format()
340
- substitution at runtime."""
341
- template_text: str = AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE
342
- assert "{project_path}" in template_text
343
- for each_pattern in ALL_AGENT_CONFIG_PATH_PATTERNS:
344
- if each_pattern.endswith("/**"):
345
- directory_name = each_pattern[: -len("/**")]
346
- expected_phrase = f"anything under {directory_name}/"
347
- elif each_pattern == "mcp.json":
348
- expected_phrase = "the mcp.json file"
349
- else:
350
- expected_phrase = each_pattern
351
- assert expected_phrase in template_text, (
352
- f"template missing derived phrase for pattern {each_pattern!r}: "
353
- f"expected {expected_phrase!r}"
354
- )
355
- rendered_template_text = template_text.format(project_path="/tmp/x")
356
- assert "/tmp/x" in rendered_template_text
@@ -1,140 +0,0 @@
1
- import sys
2
- from pathlib import Path
3
- from unittest.mock import patch
4
-
5
- import pytest
6
-
7
- _script_directory = str(Path(__file__).resolve().parent)
8
- if _script_directory not in sys.path:
9
- sys.path.insert(0, _script_directory)
10
-
11
- import _bugteam_permissions_common as common_module
12
- from _bugteam_permissions_common import (
13
- build_permission_rule,
14
- get_current_project_path,
15
- path_contains_glob_metacharacters,
16
- save_settings,
17
- )
18
- from bugteam_scripts_constants.claude_permissions_common_constants import DEFAULT_SETTINGS_FILE_MODE
19
- import grant_project_claude_permissions as grant_module
20
- import revoke_project_claude_permissions as revoke_module
21
-
22
-
23
- def test_return_normalized_path_when_cwd_contains_spaces(
24
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
25
- ) -> None:
26
- directory_with_spaces = tmp_path / "dir with spaces"
27
- directory_with_spaces.mkdir()
28
- monkeypatch.chdir(directory_with_spaces)
29
- returned_project_path = get_current_project_path()
30
- expected_suffix = "/dir with spaces"
31
- assert returned_project_path.endswith(expected_suffix)
32
- assert "\\" not in returned_project_path
33
- built_rule = build_permission_rule("Edit", returned_project_path)
34
- assert built_rule.startswith("Edit(")
35
- assert built_rule.endswith("/.claude/**)")
36
- assert "dir with spaces" in built_rule
37
-
38
-
39
- def test_raise_when_cwd_contains_glob_metacharacters(
40
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
41
- ) -> None:
42
- directory_with_star = tmp_path / "weird[dir]"
43
- directory_with_star.mkdir()
44
- monkeypatch.chdir(directory_with_star)
45
- with pytest.raises(ValueError, match="glob metacharacters"):
46
- get_current_project_path()
47
-
48
-
49
- def test_flag_glob_metacharacters_in_any_position() -> None:
50
- assert path_contains_glob_metacharacters("/home/user/[dir]/project")
51
- assert path_contains_glob_metacharacters("/home/user/project*")
52
- assert not path_contains_glob_metacharacters("/home/user/dir with spaces")
53
-
54
-
55
- def test_save_settings_logs_when_temp_unlink_fails(
56
- tmp_path: Path, capsys: pytest.CaptureFixture[str]
57
- ) -> None:
58
- """A swallowed unlink in the finally block must surface to stderr.
59
-
60
- Forces a write success followed by os.replace failure so the temp file
61
- survives into the finally branch, then makes Path.unlink raise.
62
- """
63
- settings_path = tmp_path / "settings.json"
64
- settings_path.write_text('{"existing": true}\n', encoding="utf-8")
65
-
66
- def failing_replace(*_args: object, **_kwargs: object) -> None:
67
- raise OSError("replace blocked by AV")
68
-
69
- def failing_unlink(self: Path, *args: object, **kwargs: object) -> None:
70
- raise PermissionError("temp file held by AV")
71
-
72
- with patch.object(common_module.os, "replace", failing_replace):
73
- with patch.object(Path, "unlink", failing_unlink):
74
- with pytest.raises(SystemExit):
75
- save_settings(settings_path, {"new_key": "value"})
76
- captured = capsys.readouterr()
77
- assert ".tmp" in captured.err
78
- assert "PermissionError" in captured.err or "held by AV" in captured.err
79
-
80
-
81
- def test_save_settings_finally_skips_unlink_when_no_temp_was_created(
82
- tmp_path: Path,
83
- ) -> None:
84
- """When this invocation never created the temp file, finally must not unlink it."""
85
- settings_path = tmp_path / "settings.json"
86
- settings_path.write_text('{"existing": true}\n', encoding="utf-8")
87
-
88
- unlink_call_paths: list[Path] = []
89
- original_unlink = Path.unlink
90
-
91
- def recording_unlink(self: Path, *args: object, **kwargs: object) -> None:
92
- unlink_call_paths.append(self)
93
- original_unlink(self, *args, **kwargs)
94
-
95
- def write_raises(*_args: object, **_kwargs: object) -> None:
96
- raise FileExistsError("another writer's temp")
97
-
98
- with patch.object(common_module, "write_atomically_with_mode", write_raises):
99
- with patch.object(Path, "unlink", recording_unlink):
100
- with pytest.raises(SystemExit):
101
- common_module.save_settings(settings_path, {"new_key": "value"})
102
- assert all(
103
- each_path.suffix != ".tmp"
104
- for each_path in unlink_call_paths
105
- ), (
106
- "finally must not unlink a temp file this invocation never created"
107
- )
108
-
109
-
110
- def test_default_settings_file_mode_used_when_settings_file_missing(
111
- tmp_path: Path,
112
- ) -> None:
113
- """get_mode_to_preserve must fall back to DEFAULT_SETTINGS_FILE_MODE."""
114
- missing_settings_path = tmp_path / "no_such_file.json"
115
- returned_mode = common_module.get_mode_to_preserve(missing_settings_path)
116
- assert returned_mode == DEFAULT_SETTINGS_FILE_MODE
117
-
118
-
119
- def test_is_valid_project_root_exported_from_consumer_modules(
120
- tmp_path: Path,
121
- ) -> None:
122
- """is_valid_project_root behaviour matches across both consumers.
123
-
124
- Grant and revoke each define their own local copy of the helper, so
125
- both copies must agree on the .git / .claude marker contract.
126
- """
127
- git_marker_project_root = tmp_path / "git_project"
128
- (git_marker_project_root / ".git").mkdir(parents=True)
129
- claude_marker_project_root = tmp_path / "claude_project"
130
- (claude_marker_project_root / ".claude").mkdir(parents=True)
131
- bare_directory = tmp_path / "no_marker"
132
- bare_directory.mkdir()
133
- assert grant_module.is_valid_project_root(git_marker_project_root) is True
134
- assert grant_module.is_valid_project_root(claude_marker_project_root) is True
135
- assert grant_module.is_valid_project_root(bare_directory) is False
136
- assert revoke_module.is_valid_project_root(git_marker_project_root) is True
137
- assert revoke_module.is_valid_project_root(claude_marker_project_root) is True
138
- assert revoke_module.is_valid_project_root(bare_directory) is False
139
-
140
-
@@ -1,25 +0,0 @@
1
- # doc-gist
2
-
3
- Designs fresh HTML artifacts and publishes them as secret GitHub gists with a shareable htmlpreview URL. Triggered by `/doc-gist`, `publish this`, `share as a gist`, `open this as a webpage`, `make me a writeup`, or any request ending in a shareable HTML preview URL.
4
-
5
- ## Purpose
6
-
7
- The skill ships transport, not templates. For each request the model designs a fresh HTML artifact suited to the work, marks it with `<!-- @publish-as-gist -->`, and writes it to disk. A PostToolUse hook (`hooks/workflow/doc_gist_auto_publish.py`) spots the marker, runs `packages/claude-dev-env/skills/doc-gist/scripts/gist_upload.py`, and prints the gist and htmlpreview URLs into the tool output for the user to click.
8
-
9
- ## Key files
10
-
11
- | File | Purpose |
12
- |---|---|
13
- | `SKILL.md` | Full instructions: the auto-publish hook flow, the transport script CLI, a gallery-to-artifact-type mapping table, and gotchas (`gh` auth, marker syntax, self-contained HTML only, secret-not-private note). |
14
-
15
- ## Subdirectories
16
-
17
- | Directory | Role |
18
- |---|---|
19
- | `references/` | Reference material — the example HTML gallery and related docs. |
20
- | `scripts/` | The `gist_upload.py` transport script and its constants package. |
21
-
22
- ## Auto-publish vs manual
23
-
24
- - **Auto:** write HTML with `<!-- @publish-as-gist -->` anywhere in the file; the hook handles the rest.
25
- - **Manual:** run `python scripts/gist_upload.py --input <path>` for an existing file or when the marker route does not apply.