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,336 +0,0 @@
1
- """Tests for md_to_html_blocker path resolution, denial payload, and config.
2
-
3
- Covers home-relative and tilde path canonicalization, the OS temp-directory
4
- exemption, cwd-relative resolution against repo and plugin roots, the structure
5
- of the denial payload (system message, additional context, redirect reason),
6
- and the module-introspection contracts that pin centralized constants.
7
- """
8
-
9
- import importlib
10
- import json
11
- import os
12
- import subprocess
13
- import sys
14
-
15
- _BLOCKING_DIRECTORY = os.path.dirname(__file__)
16
-
17
- if _BLOCKING_DIRECTORY not in sys.path:
18
- sys.path.insert(0, _BLOCKING_DIRECTORY)
19
-
20
- from _md_to_html_blocker_test_support import ( # noqa: E402
21
- HOOK_SCRIPT_PATH,
22
- _get_sandbox_parent_directory,
23
- _run_hook,
24
- )
25
-
26
- _ALL_HOME_ENVIRONMENT_VARIABLE_NAMES = ("USERPROFILE", "HOME")
27
- _ALL_TEMP_ENVIRONMENT_VARIABLE_NAMES = ("TMPDIR", "TEMP", "TMP")
28
-
29
-
30
- def _redirect_home_to(monkeypatch, fake_home_directory):
31
- """Point every home-directory env read at *fake_home_directory*.
32
-
33
- The hook runs as a subprocess that inherits os.environ, so setting the
34
- home env vars here propagates to the hook process. Windows ntpath reads
35
- USERPROFILE while POSIX reads HOME, so both are set to keep the test's
36
- expected path and the hook's resolution aligned on either platform.
37
-
38
- Args:
39
- monkeypatch: The pytest monkeypatch fixture used to set env vars.
40
- fake_home_directory: An existing sandbox directory to treat as home.
41
-
42
- Returns:
43
- The canonical (realpath) form of *fake_home_directory*, matching the
44
- canonicalization the exemption resolver applies before comparison.
45
- """
46
- for each_home_variable_name in _ALL_HOME_ENVIRONMENT_VARIABLE_NAMES:
47
- monkeypatch.setenv(each_home_variable_name, fake_home_directory)
48
- return os.path.realpath(fake_home_directory)
49
-
50
-
51
- def _redirect_temp_to(monkeypatch, fake_temp_directory):
52
- """Point every temp-directory env read at *fake_temp_directory*.
53
-
54
- tempfile.gettempdir() consults TMPDIR, then TEMP, then TMP, so all three
55
- are set. The hook subprocess is a fresh process, so its
56
- tempfile.gettempdir() reads these env vars rather than a cached value.
57
-
58
- Args:
59
- monkeypatch: The pytest monkeypatch fixture used to set env vars.
60
- fake_temp_directory: An existing sandbox directory to treat as temp.
61
-
62
- Returns:
63
- The canonical (realpath) form of *fake_temp_directory*, matching the
64
- canonicalization the exemption resolver applies before comparison.
65
- """
66
- for each_temp_variable_name in _ALL_TEMP_ENVIRONMENT_VARIABLE_NAMES:
67
- monkeypatch.setenv(each_temp_variable_name, fake_temp_directory)
68
- return os.path.realpath(fake_temp_directory)
69
-
70
-
71
- def _isolate_home_away_from_temp(monkeypatch, base_directory):
72
- """Redirect home and temp at disjoint subdirectories under *base_directory*.
73
-
74
- Pointing home and temp at separate trees keeps a home-relative test path
75
- from also matching the temp-directory exemption, which the resolver checks
76
- after the home exemption. Both env groups are set so the hook subprocess
77
- and this test process resolve the same fake home and temp.
78
-
79
- Args:
80
- monkeypatch: The pytest monkeypatch fixture used to set env vars.
81
- base_directory: An existing sandbox directory whose `home` and `temp`
82
- subdirectories become the fake home and fake temp.
83
-
84
- Returns:
85
- The canonical (realpath) form of the fake home directory.
86
- """
87
- fake_home_directory = os.path.join(base_directory, "home")
88
- fake_temp_directory = os.path.join(base_directory, "temp")
89
- os.makedirs(fake_home_directory, exist_ok=True)
90
- os.makedirs(fake_temp_directory, exist_ok=True)
91
- _redirect_temp_to(monkeypatch, fake_temp_directory)
92
- return _redirect_home_to(monkeypatch, fake_home_directory)
93
-
94
-
95
- def test_block_messages_mention_claude_dev_env_source_exemptions():
96
- """Block messages must surface the `packages/claude-dev-env/<dir>/` anchored
97
- exemption so contributors aren't misled when a `.md` write is denied
98
- elsewhere. Ensures docs/, rules/, and system-prompts/ source files
99
- render as writable in the user-facing message."""
100
- hook_dir = os.path.dirname(HOOK_SCRIPT_PATH)
101
- if hook_dir not in sys.path:
102
- sys.path.insert(0, hook_dir)
103
- blocker_module = importlib.import_module("md_to_html_blocker")
104
- importlib.reload(blocker_module)
105
-
106
- context_message = blocker_module._block_context()
107
- system_message = blocker_module._block_system_message()
108
- combined_messages = context_message + " " + system_message
109
- assert "claude-dev-env" in combined_messages, (
110
- "Block messages must mention claude-dev-env source-directory exemption; "
111
- f"got context={context_message!r} system={system_message!r}"
112
- )
113
-
114
-
115
- def test_module_imports_path_segments_from_hooks_constants():
116
- """The blocker pulls the two leading path segments (`packages` and
117
- `claude-dev-env`) through the centralised hooks_constants module rather
118
- than inlining them as raw string literals."""
119
- hook_dir = os.path.dirname(HOOK_SCRIPT_PATH)
120
- if hook_dir not in sys.path:
121
- sys.path.insert(0, hook_dir)
122
- blocker_module = importlib.import_module("md_to_html_blocker")
123
- importlib.reload(blocker_module)
124
- assert blocker_module.PACKAGES_TOP_LEVEL_SEGMENT == "packages"
125
- assert blocker_module.CLAUDE_DEV_ENV_REPO_NAME_SEGMENT == "claude-dev-env"
126
-
127
-
128
- def test_module_imports_top_directories_from_hooks_constants():
129
- """The exempt-top-directories set must live in `hooks_constants/` rather
130
- than as a file-global single-use constant in the blocker module. The
131
- blocker imports the centralized constant; a regression that reintroduces
132
- a local module-scope copy would fail this assertion."""
133
- hook_dir = os.path.dirname(HOOK_SCRIPT_PATH)
134
- if hook_dir not in sys.path:
135
- sys.path.insert(0, hook_dir)
136
- blocker_module = importlib.import_module("md_to_html_blocker")
137
- importlib.reload(blocker_module)
138
- assert hasattr(blocker_module, "ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES"), (
139
- "Blocker module must import ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES from "
140
- "hooks_constants/ (file-global single-use rule)."
141
- )
142
- assert not hasattr(blocker_module, "_claude_code_source_top_directories"), (
143
- "Local _claude_code_source_top_directories must not be re-introduced; "
144
- "use the imported constant from hooks_constants/ instead."
145
- )
146
-
147
-
148
- def test_blocks_relative_readme_when_cwd_is_not_repo_root():
149
- sandbox_parent = _get_sandbox_parent_directory()
150
- non_repo_cwd = os.path.join(sandbox_parent, "not-a-repo")
151
- os.makedirs(non_repo_cwd, exist_ok=True)
152
- payload = json.dumps(
153
- {
154
- "tool_name": "Write",
155
- "tool_input": {"file_path": "README.md", "content": "# README"},
156
- }
157
- )
158
- result = subprocess.run(
159
- [sys.executable, HOOK_SCRIPT_PATH],
160
- input=payload,
161
- capture_output=True,
162
- text=True,
163
- check=False,
164
- cwd=non_repo_cwd,
165
- )
166
- assert result.returncode == 0
167
- output = json.loads(result.stdout)
168
- assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
169
-
170
-
171
- def test_denial_has_system_message():
172
- result = _run_hook(
173
- "Write",
174
- {"file_path": "docs/guide.md", "content": "# Hello"},
175
- )
176
- assert result.returncode == 0
177
- output = json.loads(result.stdout)
178
- assert output["suppressOutput"] is True
179
- assert isinstance(output["systemMessage"], str)
180
- assert len(output["systemMessage"]) > 0
181
-
182
-
183
- def test_denial_has_additional_context():
184
- result = _run_hook(
185
- "Write",
186
- {"file_path": "docs/guide.md", "content": "# Hello"},
187
- )
188
- assert result.returncode == 0
189
- output = json.loads(result.stdout)
190
- ctx = output["hookSpecificOutput"].get("additionalContext", "")
191
- assert "HTML" in ctx
192
- assert "thariqs.github.io" in output["hookSpecificOutput"]["permissionDecisionReason"]
193
-
194
-
195
- def test_denial_reason_mentions_html_redirect():
196
- result = _run_hook(
197
- "Write",
198
- {"file_path": "docs/guide.md", "content": "# Hello"},
199
- )
200
- assert result.returncode == 0
201
- output = json.loads(result.stdout)
202
- reason = output["hookSpecificOutput"]["permissionDecisionReason"]
203
- assert ".html" in reason.lower()
204
-
205
-
206
- def test_passes_home_session_log_directory(monkeypatch, tmp_path):
207
- home_directory = _isolate_home_away_from_temp(monkeypatch, str(tmp_path))
208
- session_log_path = os.path.join(home_directory, "SessionLog", "decisions", "note.md")
209
- result = _run_hook(
210
- "Write",
211
- {"file_path": session_log_path, "content": "# Note"},
212
- )
213
- assert result.returncode == 0
214
- assert result.stdout == ""
215
-
216
-
217
- def test_passes_home_claude_plans_directory(monkeypatch, tmp_path):
218
- home_directory = _isolate_home_away_from_temp(monkeypatch, str(tmp_path))
219
- plans_path = os.path.join(home_directory, ".claude", "plans", "plan.md")
220
- result = _run_hook(
221
- "Write",
222
- {"file_path": plans_path, "content": "# Plan"},
223
- )
224
- assert result.returncode == 0
225
- assert result.stdout == ""
226
-
227
-
228
- def test_blocks_home_directory_other_md_file(monkeypatch, tmp_path):
229
- home_directory = _isolate_home_away_from_temp(monkeypatch, str(tmp_path))
230
- other_path = os.path.join(home_directory, "docs", "guide.md")
231
- result = _run_hook(
232
- "Write",
233
- {"file_path": other_path, "content": "# Guide"},
234
- )
235
- assert result.returncode == 0
236
- output = json.loads(result.stdout)
237
- assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
238
-
239
-
240
- def test_passes_tilde_session_log_path():
241
- result = _run_hook(
242
- "Write",
243
- {"file_path": "~/SessionLog/decisions/note.md", "content": "# Note"},
244
- )
245
- assert result.returncode == 0
246
- assert result.stdout == ""
247
-
248
-
249
- def test_passes_tilde_claude_plans_path():
250
- result = _run_hook(
251
- "Write",
252
- {"file_path": "~/.claude/plans/plan.md", "content": "# Plan"},
253
- )
254
- assert result.returncode == 0
255
- assert result.stdout == ""
256
-
257
-
258
- def test_blocks_tilde_other_home_md_file():
259
- result = _run_hook(
260
- "Write",
261
- {"file_path": "~/docs/guide.md", "content": "# Guide"},
262
- )
263
- assert result.returncode == 0
264
- output = json.loads(result.stdout)
265
- assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
266
-
267
-
268
- def test_passes_system_temp_directory(monkeypatch, tmp_path):
269
- temp_directory = _redirect_temp_to(monkeypatch, str(tmp_path))
270
- temp_md_path = os.path.join(temp_directory, "bugteam-scratch", "pr-body.md")
271
- result = _run_hook(
272
- "Write",
273
- {"file_path": temp_md_path, "content": "# Scratch"},
274
- )
275
- assert result.returncode == 0
276
- assert result.stdout == ""
277
-
278
-
279
- def test_passes_relative_path_from_home_cwd(monkeypatch, tmp_path):
280
- home_directory = _isolate_home_away_from_temp(monkeypatch, str(tmp_path))
281
- payload = json.dumps(
282
- {
283
- "tool_name": "Write",
284
- "tool_input": {
285
- "file_path": "SessionLog/decisions/note.md",
286
- "content": "# Note",
287
- },
288
- }
289
- )
290
- result = subprocess.run(
291
- [sys.executable, HOOK_SCRIPT_PATH],
292
- input=payload,
293
- capture_output=True,
294
- text=True,
295
- check=False,
296
- cwd=home_directory,
297
- )
298
- assert result.returncode == 0
299
- assert result.stdout == ""
300
-
301
-
302
- def test_passes_canonicalized_home_path(monkeypatch, tmp_path):
303
- canonical_home = _isolate_home_away_from_temp(monkeypatch, str(tmp_path))
304
- canonical_path = os.path.join(canonical_home, "SessionLog", "canonical-note.md")
305
- result = _run_hook(
306
- "Write",
307
- {"file_path": canonical_path, "content": "# Canonical"},
308
- )
309
- assert result.returncode == 0
310
- assert result.stdout == ""
311
-
312
-
313
- def test_passes_relative_path_under_cwd_plugin_root_marker(tmp_path):
314
- plugin_root = tmp_path / "plugin-cwd-repo"
315
- (plugin_root / ".claude-plugin").mkdir(parents=True)
316
- (plugin_root / "subdir").mkdir(parents=True)
317
-
318
- payload = json.dumps(
319
- {
320
- "tool_name": "Write",
321
- "tool_input": {
322
- "file_path": "subdir/design.md",
323
- "content": "# Design",
324
- },
325
- }
326
- )
327
- result = subprocess.run(
328
- [sys.executable, HOOK_SCRIPT_PATH],
329
- input=payload,
330
- capture_output=True,
331
- text=True,
332
- check=False,
333
- cwd=str(plugin_root),
334
- )
335
- assert result.returncode == 0
336
- assert result.stdout == ""
@@ -1,18 +0,0 @@
1
- """Constants for the doc-gist auto-publish PostToolUse hook.
2
-
3
- PUBLISH_SENTINEL: HTML comment marker. Claude includes it in HTML it intends to share;
4
- absent in HTML that is part of code, tests, or fixtures. Hook is a no-op without it.
5
-
6
- HTML_FILE_EXTENSION: only files ending in this extension are candidates for the hook;
7
- Markdown, source files, etc. are skipped.
8
-
9
- ALL_TARGET_TOOL_NAMES: tool names the hook fires after. The hook is a PostToolUse
10
- listener; only Write and Edit produce a writable file path that the marker check
11
- can be applied to.
12
- """
13
-
14
- PUBLISH_SENTINEL = "<!-- @publish-as-gist -->"
15
- HTML_FILE_EXTENSION = ".html"
16
- ALL_TARGET_TOOL_NAMES = ("Write", "Edit")
17
- HOOK_SUBPROCESS_TIMEOUT_SECONDS = 50
18
- UPLOAD_SCRIPT_RELATIVE_PATH = "skills/doc-gist/scripts/gist_upload.py"
@@ -1,20 +0,0 @@
1
- """CSS theme constants and URL scheme denylist for md-to-html companion hook."""
2
-
3
- CSS_BG_COLOR = "13, 17, 23"
4
- CSS_FG_COLOR = "201, 209, 217"
5
- CSS_BORDER_COLOR = "48, 54, 61"
6
- CSS_ACCENT_COLOR = "88, 166, 255"
7
- CSS_MUTED_COLOR = "139, 148, 158"
8
- CSS_SURFACE_COLOR = "22, 27, 34"
9
- CSS_STRONG_COLOR = "240, 246, 252"
10
- CSS_LINE_HEIGHT = "1.6"
11
- CSS_BODY_PADDING = "2rem"
12
- CSS_MAX_WIDTH = "960px"
13
- CSS_H1_SIZE = "1.6rem"
14
- CSS_H2_SIZE = "1.25rem"
15
- CSS_H3_SIZE = "1.1rem"
16
- CSS_CODE_SIZE = "0.85rem"
17
- CSS_TABLE_WIDTH = "100%"
18
- CSS_TH_WEIGHT = "600"
19
-
20
- BLOCKED_URL_SCHEMES = frozenset({"javascript", "data"})
@@ -1,76 +0,0 @@
1
- """Configuration constants for the md_to_html_blocker PreToolUse hook
2
- and its shared exemption helpers (`md_path_exemptions`)."""
3
-
4
- from __future__ import annotations
5
-
6
- import os
7
- import tempfile
8
-
9
-
10
- ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES: frozenset[str] = frozenset(
11
- {"agents", "docs", "skills", "rules", "system-prompts", "commands"}
12
- )
13
-
14
- PACKAGES_TOP_LEVEL_SEGMENT: str = "packages"
15
- CLAUDE_DEV_ENV_REPO_NAME_SEGMENT: str = "claude-dev-env"
16
-
17
- MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR: int = 4
18
-
19
- ALL_EXEMPT_ANYWHERE_FILENAMES: tuple[str, ...] = ("SKILL.md",)
20
- ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS: tuple[str, ...] = ("agents", "skills", "commands")
21
- ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES: tuple[str, ...] = ("SessionLog",)
22
- ALL_EXEMPT_ROOT_FILENAMES: tuple[str, ...] = ("readme.md", "changelog.md", "claude.md", "agents.md")
23
- REPO_ROOT_MARKER_NAME: str = ".git"
24
- CLAUDE_DIRECTORY_NAME: str = ".claude"
25
- PLUGIN_ROOT_MARKER_DIRECTORY_NAME: str = ".claude-plugin"
26
-
27
- CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX: str = f"{CLAUDE_DIRECTORY_NAME}-"
28
-
29
- ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER: frozenset[str] = frozenset(
30
- each_filename.lower() for each_filename in ALL_EXEMPT_ANYWHERE_FILENAMES
31
- )
32
- ALL_EXEMPT_ROOT_FILENAMES_LOWER: frozenset[str] = frozenset(
33
- each_filename.lower() for each_filename in ALL_EXEMPT_ROOT_FILENAMES
34
- )
35
-
36
-
37
- def _resolve_canonical_directory_lowercase(directory_path: str) -> str:
38
- return (
39
- os.path.realpath(directory_path).replace("\\", "/").rstrip("/").lower()
40
- )
41
-
42
-
43
- RESOLVED_HOME_DIRECTORY_LOWER: str = _resolve_canonical_directory_lowercase(
44
- os.path.expanduser("~")
45
- )
46
- RESOLVED_TEMP_DIRECTORY_LOWER: str = _resolve_canonical_directory_lowercase(
47
- tempfile.gettempdir()
48
- )
49
-
50
- ALL_EXEMPT_HOME_DIRECTORY_PATH_PREFIXES: tuple[str, ...] = tuple(
51
- f"{RESOLVED_HOME_DIRECTORY_LOWER}/{each_relative_directory.lower()}/"
52
- for each_relative_directory in ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES
53
- )
54
- RESOLVED_TEMP_DIRECTORY_PATH_PREFIX: str = f"{RESOLVED_TEMP_DIRECTORY_LOWER}/"
55
-
56
-
57
- __all__ = [
58
- "ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES",
59
- "ALL_EXEMPT_ANYWHERE_FILENAMES",
60
- "ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER",
61
- "ALL_EXEMPT_HOME_DIRECTORY_PATH_PREFIXES",
62
- "ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES",
63
- "ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS",
64
- "ALL_EXEMPT_ROOT_FILENAMES",
65
- "ALL_EXEMPT_ROOT_FILENAMES_LOWER",
66
- "CLAUDE_DEV_ENV_REPO_NAME_SEGMENT",
67
- "CLAUDE_DIRECTORY_NAME",
68
- "CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX",
69
- "MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR",
70
- "PACKAGES_TOP_LEVEL_SEGMENT",
71
- "PLUGIN_ROOT_MARKER_DIRECTORY_NAME",
72
- "REPO_ROOT_MARKER_NAME",
73
- "RESOLVED_HOME_DIRECTORY_LOWER",
74
- "RESOLVED_TEMP_DIRECTORY_LOWER",
75
- "RESOLVED_TEMP_DIRECTORY_PATH_PREFIX",
76
- ]
@@ -1,125 +0,0 @@
1
- """Behavior tests for md_to_html_blocker_constants module."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from pathlib import Path
7
-
8
- _HOOKS_ROOT = Path(__file__).resolve().parent.parent
9
- if str(_HOOKS_ROOT) not in sys.path:
10
- sys.path.insert(0, str(_HOOKS_ROOT))
11
-
12
- from hooks_constants import md_to_html_blocker_constants as constants_module
13
-
14
-
15
- def test_indicator_path_segments_are_named_constants() -> None:
16
- """The two leading segments of the Claude Code source indicator
17
- (`packages/claude-dev-env`) must live as named constants so
18
- `_is_exempt_path` references symbols, not inline literals. Bugbot flagged
19
- these as magic strings even though the third segment was already a named
20
- constant; align all three."""
21
- assert constants_module.PACKAGES_TOP_LEVEL_SEGMENT == "packages"
22
- assert constants_module.CLAUDE_DEV_ENV_REPO_NAME_SEGMENT == "claude-dev-env"
23
- for each_name in ("PACKAGES_TOP_LEVEL_SEGMENT", "CLAUDE_DEV_ENV_REPO_NAME_SEGMENT"):
24
- assert each_name in constants_module.__all__
25
-
26
-
27
- def test_claude_code_source_top_directories_enumerates_six_canonical_dirs() -> None:
28
- """The exempt top-level directory set must match the six canonical Claude
29
- Code source directories (agents, docs, skills, rules, system-prompts,
30
- commands). Drift in either direction silently changes the exemption
31
- surface."""
32
- expected_directories = frozenset(
33
- {"agents", "docs", "skills", "rules", "system-prompts", "commands"}
34
- )
35
- assert constants_module.ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES == expected_directories
36
- assert "ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES" in constants_module.__all__
37
-
38
-
39
- def test_minimum_segment_count_to_match_indicator_is_four() -> None:
40
- """Matching `packages/claude-dev-env/<dir>/<file>` requires at least 4
41
- consecutive path segments from the starting index. The constant documents
42
- that requirement explicitly."""
43
- assert constants_module.MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR == 4
44
- assert "MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR" in constants_module.__all__
45
-
46
-
47
- def test_exempt_anywhere_filenames_include_skill_md() -> None:
48
- """`SKILL.md` files are exempt anywhere in the tree. The constant stores
49
- the display-case spelling; the lookup at use sites lowercases the
50
- candidate basename, so casing here documents the human-facing form."""
51
- assert "SKILL.md" in constants_module.ALL_EXEMPT_ANYWHERE_FILENAMES
52
- assert "ALL_EXEMPT_ANYWHERE_FILENAMES" in constants_module.__all__
53
-
54
-
55
- def test_exempt_plugin_directory_segments_match_claude_code_layout() -> None:
56
- """The three plugin-layout directory names recognized anywhere in a path:
57
- agents/, skills/, commands/. Drift in either direction silently changes
58
- the exemption surface."""
59
- expected_segments = ("agents", "skills", "commands")
60
- assert constants_module.ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS == expected_segments
61
- assert "ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS" in constants_module.__all__
62
-
63
-
64
- def test_exempt_plugin_segments_subset_of_claude_code_source_top_directories() -> None:
65
- """ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS (matched anywhere in the path)
66
- must be a subset of ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES (matched
67
- only at the anchored claude-dev-env source root). If a future change
68
- adds a segment to the anywhere list that is not in the anchored set,
69
- the anywhere rule would let writes through that the anchored rule
70
- would still block — surprising and asymmetric."""
71
- anywhere_segments = set(constants_module.ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS)
72
- anchored_source_directories = set(
73
- constants_module.ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES
74
- )
75
- assert anywhere_segments.issubset(anchored_source_directories)
76
-
77
-
78
- def test_exempt_home_relative_directories_include_session_log() -> None:
79
- """SessionLog/ under the user's home directory is the canonical Obsidian
80
- vault entrypoint and must be writable as .md."""
81
- assert "SessionLog" in constants_module.ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES
82
- assert "ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES" in constants_module.__all__
83
-
84
-
85
- def test_exempt_root_filenames_cover_readme_changelog_claude_and_agents() -> None:
86
- """README.md, CHANGELOG.md, CLAUDE.md, and AGENTS.md at a repo root are
87
- universally exempt; every repo with a `.git` marker satisfies the root
88
- check. CLAUDE.md and AGENTS.md are functional agent-instruction files that
89
- Claude Code loads by name and must stay Markdown."""
90
- assert constants_module.ALL_EXEMPT_ROOT_FILENAMES == (
91
- "readme.md",
92
- "changelog.md",
93
- "claude.md",
94
- "agents.md",
95
- )
96
- assert "ALL_EXEMPT_ROOT_FILENAMES" in constants_module.__all__
97
-
98
-
99
- def test_repo_root_marker_name_is_dot_git() -> None:
100
- """A directory containing `.git` (file or directory) is treated as a repo
101
- root for the README/CHANGELOG exemption."""
102
- assert constants_module.REPO_ROOT_MARKER_NAME == ".git"
103
- assert "REPO_ROOT_MARKER_NAME" in constants_module.__all__
104
-
105
-
106
- def test_claude_directory_name_is_dot_claude() -> None:
107
- """Any path containing a `.claude/` segment bypasses the .md block
108
- (project-level Claude Code infrastructure)."""
109
- assert constants_module.CLAUDE_DIRECTORY_NAME == ".claude"
110
- assert "CLAUDE_DIRECTORY_NAME" in constants_module.__all__
111
-
112
-
113
- def test_plugin_root_marker_directory_name_is_dot_claude_plugin() -> None:
114
- """Any directory whose ancestor contains `.claude-plugin/` is treated as
115
- a plugin repo root and exempted."""
116
- assert constants_module.PLUGIN_ROOT_MARKER_DIRECTORY_NAME == ".claude-plugin"
117
- assert "PLUGIN_ROOT_MARKER_DIRECTORY_NAME" in constants_module.__all__
118
-
119
-
120
- def test_claude_profile_directory_name_prefix_is_dot_claude_hyphen() -> None:
121
- """A directory whose name carries the `.claude-` prefix (profile
122
- directories like `.claude-mel/`, plus `.claude-plugin/`) is Claude
123
- infrastructure; any path inside one bypasses the .md block."""
124
- assert constants_module.CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX == ".claude-"
125
- assert "CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX" in constants_module.__all__