claude-dev-env 7.0.0 → 7.2.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 (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +213 -0
  6. package/.agents/skills/session-continuity/SKILL.md +125 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +447 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +88 -0
  20. package/bin/install-session-continuity.test.mjs +55 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -1,382 +0,0 @@
1
- """Tests for package_inventory_stale_blocker hook."""
2
-
3
- import json
4
- import os
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
-
9
- sys.path.insert(0, str(Path(__file__).resolve().parent))
10
-
11
- from package_inventory_stale_blocker import (
12
- find_stale_inventory,
13
- inventory_named_basenames,
14
- is_inventoried_production_file,
15
- )
16
-
17
- from hooks_constants.package_inventory_stale_blocker_constants import (
18
- STALE_INVENTORY_SYSTEM_MESSAGE,
19
- )
20
-
21
- HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "package_inventory_stale_blocker.py")
22
-
23
- README_LISTING_TWO_FILES = (
24
- "# Pipeline\n\n"
25
- "| Path | Role |\n"
26
- "|---|---|\n"
27
- "| `pipeline/dialer_compose.py` | Composes a dialer strip. |\n"
28
- "| `compose_dialer_cli.py` | CLI for the dialer strip. |\n"
29
- )
30
-
31
- CLAUDE_MD_BULLET_LIST = (
32
- "# package\n\n"
33
- "## Key files\n\n"
34
- "- `compose_dialer_cli.py` — composes one dialer strip.\n"
35
- "- `compose_aod_cli.py` — composes the AOD image.\n"
36
- )
37
-
38
- README_LISTING_ONE_FILE = "# package\n\nSome prose mentioning `single_module.py` once.\n"
39
-
40
- README_LISTING_ONLY_GLOB_TOKENS = (
41
- "# package\n\n"
42
- "This directory holds files matching `*.py` and `*.mjs`.\n"
43
- )
44
-
45
- README_LISTING_ONLY_FENCED_FILENAMES = (
46
- "# package\n\n"
47
- "Example inventory table:\n\n"
48
- "```\n"
49
- "| Path | Role |\n"
50
- "|---|---|\n"
51
- "| `example_alpha.py` | Sample row. |\n"
52
- "| `example_beta.py` | Sample row. |\n"
53
- "```\n"
54
- )
55
-
56
- README_LISTING_ONLY_COMMAND_EXAMPLES = (
57
- "# package\n\n"
58
- "Run `parent:node_modules package.json` to find the manifest, then "
59
- "`python <file>.py` and `psql $DATABASE_URL -f <query>.sql`. Import via "
60
- "`from git_hooks_constants import VALUE`.\n"
61
- )
62
-
63
- README_PROSE_NAMES_NON_SIBLING_FILES = (
64
- "# package\n\n"
65
- "This directory works alongside `install.mjs` and is documented in "
66
- "`source-material-section-types.md`.\n"
67
- )
68
-
69
-
70
- class _RunHook:
71
- """Helper to test the hook via subprocess, mirroring the sibling test style."""
72
-
73
- def __call__(self, tool_name: str, tool_input: dict) -> subprocess.CompletedProcess:
74
- payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
75
- return subprocess.run(
76
- [sys.executable, HOOK_SCRIPT_PATH],
77
- input=payload,
78
- capture_output=True,
79
- text=True,
80
- check=False,
81
- )
82
-
83
-
84
- _run_hook = _RunHook()
85
-
86
-
87
- def _package_directory_with_readme(tmp_path: Path, readme_content: str) -> Path:
88
- """Return a fresh package directory holding a README.md with the given content."""
89
- package_directory = tmp_path / "package_directory"
90
- package_directory.mkdir()
91
- (package_directory / "README.md").write_text(readme_content, encoding="utf-8")
92
- return package_directory
93
-
94
-
95
- def _write_sibling_files(package_directory: Path, all_basenames: list[str]) -> None:
96
- """Create each named file on disk inside *package_directory*."""
97
- for each_basename in all_basenames:
98
- (package_directory / each_basename).write_text("x = 1\n", encoding="utf-8")
99
-
100
-
101
- def test_inventory_named_basenames_strips_path_to_basename():
102
- named_basenames = inventory_named_basenames(README_LISTING_TWO_FILES)
103
- assert named_basenames == {"dialer_compose.py", "compose_dialer_cli.py"}
104
-
105
-
106
- def test_inventory_named_basenames_reads_bullet_list():
107
- named_basenames = inventory_named_basenames(CLAUDE_MD_BULLET_LIST)
108
- assert named_basenames == {"compose_dialer_cli.py", "compose_aod_cli.py"}
109
-
110
-
111
- def test_inventory_named_basenames_rejects_glob_tokens():
112
- named_basenames = inventory_named_basenames(README_LISTING_ONLY_GLOB_TOKENS)
113
- assert named_basenames == set()
114
-
115
-
116
- def test_inventory_named_basenames_skips_fenced_code_block():
117
- named_basenames = inventory_named_basenames(README_LISTING_ONLY_FENCED_FILENAMES)
118
- assert named_basenames == set()
119
-
120
-
121
- def test_inventory_named_basenames_rejects_command_example_spans():
122
- named_basenames = inventory_named_basenames(README_LISTING_ONLY_COMMAND_EXAMPLES)
123
- assert named_basenames == set()
124
-
125
-
126
- def test_blocks_new_production_file_absent_from_readme(tmp_path: Path):
127
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
128
- _write_sibling_files(package_directory, ["dialer_compose.py", "compose_dialer_cli.py"])
129
- new_file_path = package_directory / "check_dialer_seam_cli.py"
130
- result = _run_hook(
131
- "Write",
132
- {"file_path": str(new_file_path), "content": "x = 1\n"},
133
- )
134
- assert result.returncode == 0
135
- payload = json.loads(result.stdout)
136
- assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
137
- assert "check_dialer_seam_cli.py" in payload["hookSpecificOutput"]["permissionDecisionReason"]
138
- assert payload["systemMessage"] == STALE_INVENTORY_SYSTEM_MESSAGE
139
-
140
-
141
- def test_blocks_new_file_absent_from_claude_md_bullet_list(tmp_path: Path):
142
- package_directory = tmp_path / "package_directory"
143
- package_directory.mkdir()
144
- (package_directory / "CLAUDE.md").write_text(CLAUDE_MD_BULLET_LIST, encoding="utf-8")
145
- _write_sibling_files(package_directory, ["compose_dialer_cli.py", "compose_aod_cli.py"])
146
- new_file_path = package_directory / "build_dialer_aod_roster_cli.py"
147
- result = _run_hook(
148
- "Write",
149
- {"file_path": str(new_file_path), "content": "x = 1\n"},
150
- )
151
- assert result.returncode == 0
152
- payload = json.loads(result.stdout)
153
- assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
154
-
155
-
156
- def test_allows_new_file_already_named_in_readme(tmp_path: Path):
157
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
158
- new_file_path = package_directory / "compose_dialer_cli.py"
159
- result = _run_hook(
160
- "Write",
161
- {"file_path": str(new_file_path), "content": "x = 1\n"},
162
- )
163
- assert result.returncode == 0
164
- assert result.stdout.strip() == ""
165
-
166
-
167
- def test_allows_new_file_named_by_path_form_in_readme(tmp_path: Path):
168
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
169
- pipeline_directory = package_directory / "pipeline"
170
- pipeline_directory.mkdir()
171
- new_file_path = package_directory / "dialer_compose.py"
172
- result = _run_hook(
173
- "Write",
174
- {"file_path": str(new_file_path), "content": "x = 1\n"},
175
- )
176
- assert result.returncode == 0
177
- assert result.stdout.strip() == ""
178
-
179
-
180
- def test_allows_directory_with_no_inventory(tmp_path: Path):
181
- package_directory = tmp_path / "package_directory"
182
- package_directory.mkdir()
183
- new_file_path = package_directory / "lonely_module.py"
184
- result = _run_hook(
185
- "Write",
186
- {"file_path": str(new_file_path), "content": "x = 1\n"},
187
- )
188
- assert result.returncode == 0
189
- assert result.stdout.strip() == ""
190
-
191
-
192
- def test_allows_directory_whose_readme_names_too_few_files(tmp_path: Path):
193
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_ONE_FILE)
194
- new_file_path = package_directory / "another_module.py"
195
- result = _run_hook(
196
- "Write",
197
- {"file_path": str(new_file_path), "content": "x = 1\n"},
198
- )
199
- assert result.returncode == 0
200
- assert result.stdout.strip() == ""
201
-
202
-
203
- def test_allows_new_file_when_inventory_holds_only_glob_tokens(tmp_path: Path):
204
- package_directory = _package_directory_with_readme(
205
- tmp_path, README_LISTING_ONLY_GLOB_TOKENS
206
- )
207
- new_file_path = package_directory / "new_sibling_module.py"
208
- result = _run_hook(
209
- "Write",
210
- {"file_path": str(new_file_path), "content": "x = 1\n"},
211
- )
212
- assert result.returncode == 0
213
- assert result.stdout.strip() == ""
214
-
215
-
216
- def test_allows_new_file_when_inventory_filenames_live_in_code_fence(tmp_path: Path):
217
- package_directory = _package_directory_with_readme(
218
- tmp_path, README_LISTING_ONLY_FENCED_FILENAMES
219
- )
220
- new_file_path = package_directory / "new_sibling_module.py"
221
- result = _run_hook(
222
- "Write",
223
- {"file_path": str(new_file_path), "content": "x = 1\n"},
224
- )
225
- assert result.returncode == 0
226
- assert result.stdout.strip() == ""
227
-
228
-
229
- def test_allows_new_file_when_inventory_names_only_non_sibling_files(tmp_path: Path):
230
- package_directory = _package_directory_with_readme(
231
- tmp_path, README_PROSE_NAMES_NON_SIBLING_FILES
232
- )
233
- new_file_path = package_directory / "new_helper.py"
234
- result = _run_hook(
235
- "Write",
236
- {"file_path": str(new_file_path), "content": "x = 1\n"},
237
- )
238
- assert result.returncode == 0
239
- assert result.stdout.strip() == ""
240
-
241
-
242
- def test_blocks_new_file_when_inventory_names_sibling_files_on_disk(tmp_path: Path):
243
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
244
- (package_directory / "dialer_compose.py").write_text("x = 1\n", encoding="utf-8")
245
- (package_directory / "compose_dialer_cli.py").write_text("x = 1\n", encoding="utf-8")
246
- new_file_path = package_directory / "check_dialer_seam_cli.py"
247
- result = _run_hook(
248
- "Write",
249
- {"file_path": str(new_file_path), "content": "x = 1\n"},
250
- )
251
- assert result.returncode == 0
252
- payload = json.loads(result.stdout)
253
- assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
254
- assert "check_dialer_seam_cli.py" in payload["hookSpecificOutput"]["permissionDecisionReason"]
255
-
256
-
257
- def test_allows_test_file_absent_from_inventory(tmp_path: Path):
258
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
259
- new_file_path = package_directory / "test_check_dialer_seam_cli.py"
260
- result = _run_hook(
261
- "Write",
262
- {"file_path": str(new_file_path), "content": "x = 1\n"},
263
- )
264
- assert result.returncode == 0
265
- assert result.stdout.strip() == ""
266
-
267
-
268
- def test_allows_init_file_absent_from_inventory(tmp_path: Path):
269
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
270
- new_file_path = package_directory / "__init__.py"
271
- result = _run_hook(
272
- "Write",
273
- {"file_path": str(new_file_path), "content": "x = 1\n"},
274
- )
275
- assert result.returncode == 0
276
- assert result.stdout.strip() == ""
277
-
278
-
279
- def test_allows_non_code_file_absent_from_inventory(tmp_path: Path):
280
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
281
- new_file_path = package_directory / "notes.txt"
282
- result = _run_hook(
283
- "Write",
284
- {"file_path": str(new_file_path), "content": "x = 1\n"},
285
- )
286
- assert result.returncode == 0
287
- assert result.stdout.strip() == ""
288
-
289
-
290
- def test_allows_edit_of_existing_file(tmp_path: Path):
291
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
292
- existing_file_path = package_directory / "seam_continuity.py"
293
- existing_file_path.write_text("x = 1\n", encoding="utf-8")
294
- result = _run_hook(
295
- "Write",
296
- {"file_path": str(existing_file_path), "content": "x = 2\n"},
297
- )
298
- assert result.returncode == 0
299
- assert result.stdout.strip() == ""
300
-
301
-
302
- def test_is_inventoried_production_file_rejects_config_directory(tmp_path: Path):
303
- config_directory = tmp_path / "config"
304
- config_directory.mkdir()
305
- config_file_path = config_directory / "constants.py"
306
- assert is_inventoried_production_file(str(config_file_path)) is False
307
-
308
-
309
- def test_is_inventoried_production_file_accepts_production_file(tmp_path: Path):
310
- production_file_path = tmp_path / "dialer_compose.py"
311
- assert is_inventoried_production_file(str(production_file_path)) is True
312
-
313
-
314
- SKILL_MD_SCRIPTS_LAYOUT = (
315
- "# base-theme-match\n\n"
316
- "## Layout\n\n"
317
- "| Path | Role |\n"
318
- "|---|---|\n"
319
- "| `scripts/discover_candidates.py` | Discovers candidates. |\n"
320
- "| `scripts/resolve_stp.py` | Resolves the STP filename. |\n"
321
- "| `scripts/select_stp.py` | Selects the STP filename. |\n"
322
- )
323
-
324
-
325
- def _skill_scripts_directory(tmp_path: Path) -> Path:
326
- """Return a skill package's scripts/ directory under a SKILL.md Layout table."""
327
- skill_directory = tmp_path / "base-theme-match"
328
- skill_directory.mkdir()
329
- (skill_directory / "SKILL.md").write_text(SKILL_MD_SCRIPTS_LAYOUT, encoding="utf-8")
330
- scripts_directory = skill_directory / "scripts"
331
- scripts_directory.mkdir()
332
- _write_sibling_files(scripts_directory, ["discover_candidates.py", "resolve_stp.py"])
333
- return scripts_directory
334
-
335
-
336
- def test_blocks_new_script_absent_from_parent_skill_layout(tmp_path: Path):
337
- scripts_directory = _skill_scripts_directory(tmp_path)
338
- new_file_path = scripts_directory / "stp_selection.py"
339
- result = _run_hook(
340
- "Write",
341
- {"file_path": str(new_file_path), "content": "x = 1\n"},
342
- )
343
- assert result.returncode == 0
344
- payload = json.loads(result.stdout)
345
- assert payload["hookSpecificOutput"]["permissionDecision"] == "deny"
346
- assert "stp_selection.py" in payload["hookSpecificOutput"]["permissionDecisionReason"]
347
-
348
-
349
- def test_allows_new_script_named_in_parent_skill_layout(tmp_path: Path):
350
- scripts_directory = _skill_scripts_directory(tmp_path)
351
- new_file_path = scripts_directory / "select_stp.py"
352
- result = _run_hook(
353
- "Write",
354
- {"file_path": str(new_file_path), "content": "x = 1\n"},
355
- )
356
- assert result.returncode == 0
357
- assert result.stdout.strip() == ""
358
-
359
-
360
- def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
361
- package_directory = _package_directory_with_readme(tmp_path, README_LISTING_TWO_FILES)
362
- _write_sibling_files(package_directory, ["dialer_compose.py", "compose_dialer_cli.py"])
363
- new_file_path = package_directory / "seam_continuity.py"
364
- survey = find_stale_inventory(str(new_file_path))
365
- assert survey is not None
366
- assert survey.present_inventory_names == ["README.md"]
367
- assert survey.named_basenames == {"dialer_compose.py", "compose_dialer_cli.py"}
368
-
369
-
370
- def test_find_stale_inventory_skips_directory_below_minimum_named_entries(
371
- tmp_path: Path,
372
- ) -> None:
373
- """Skip a package inventory that names fewer than the minimum on-disk siblings."""
374
- package_directory = tmp_path / "prose_package"
375
- package_directory.mkdir()
376
- (package_directory / "CLAUDE.md").write_text(
377
- "| File | Role |\n|---|---|\n| `only_one.py` | sole entry |\n",
378
- encoding="utf-8",
379
- )
380
- (package_directory / "only_one.py").write_text("FLAG = 1\n", encoding="utf-8")
381
- new_file_path = package_directory / "new_helper.py"
382
- assert find_stale_inventory(str(new_file_path)) is None