claude-dev-env 2.11.0 → 2.13.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 (203) hide show
  1. package/_shared/advisor/reference/sol-rung.md +5 -2
  2. package/_shared/advisor/reference/warm-up.md +1 -0
  3. package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
  4. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
  5. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  8. package/agents/CLAUDE.md +1 -2
  9. package/agents/pr-description-writer.md +11 -81
  10. package/bin/install.mjs +48 -9
  11. package/bin/install.prune.test.mjs +26 -0
  12. package/bin/install.settings-defaults.test.mjs +60 -33
  13. package/bin/install.test.mjs +2 -4
  14. package/commands/CLAUDE.md +1 -0
  15. package/commands/sr-loop.md +52 -0
  16. package/docs/CLAUDE.md +0 -1
  17. package/docs/references/CLAUDE.md +0 -1
  18. package/hooks/CLAUDE.md +1 -1
  19. package/hooks/blocking/CLAUDE.md +2 -27
  20. package/hooks/blocking/config/CLAUDE.md +1 -13
  21. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  22. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  23. package/hooks/blocking/stop_dispatcher.py +4 -4
  24. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  26. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  27. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  28. package/hooks/diagnostic/CLAUDE.md +3 -3
  29. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  30. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  31. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  32. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  33. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  34. package/hooks/git-hooks/pre_push.py +3 -85
  35. package/hooks/git-hooks/test_pre_push.py +0 -130
  36. package/hooks/hooks.json +0 -35
  37. package/hooks/hooks_constants/CLAUDE.md +0 -9
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  39. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  40. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  41. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  42. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  43. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  46. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  47. package/hooks/lifecycle/CLAUDE.md +1 -3
  48. package/package.json +1 -1
  49. package/rules/CLAUDE.md +0 -2
  50. package/rules/gh-cli-conventions.md +0 -1
  51. package/rules/git-workflow.md +1 -3
  52. package/scripts/CLAUDE.md +0 -1
  53. package/scripts/_code_review_test_support.py +0 -95
  54. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  55. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  56. package/scripts/invoke_code_review.py +8 -451
  57. package/scripts/test_invoke_code_review.py +5 -61
  58. package/scripts/test_invoke_code_review_cli.py +1 -45
  59. package/scripts/test_invoke_code_review_contract.py +1 -118
  60. package/settings.json +1 -10
  61. package/skills/CLAUDE.md +8 -0
  62. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  63. package/skills/autoconverge/reference/convergence.md +7 -7
  64. package/skills/autoconverge/reference/gotchas.md +3 -10
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  66. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  67. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  68. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  69. package/skills/autoconverge/workflow/converge.mjs +201 -238
  70. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  71. package/skills/bugteam/CONSTRAINTS.md +1 -1
  72. package/skills/bugteam/SKILL.md +9 -0
  73. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  74. package/skills/bugteam/test_skill_additions.py +9 -0
  75. package/skills/comments/SKILL.md +65 -0
  76. package/skills/descriptions/SKILL.md +51 -0
  77. package/skills/emergencies/SKILL.md +42 -0
  78. package/skills/imagegen/SKILL.md +21 -0
  79. package/skills/imagegen/scripts/config/__init__.py +1 -0
  80. package/skills/imagegen/scripts/config/constants.py +28 -0
  81. package/skills/imagegen/scripts/imagegen.py +45 -0
  82. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  83. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  84. package/skills/orchestrator/SKILL.md +2 -4
  85. package/skills/plan-to-pr/SKILL.md +6 -7
  86. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  87. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  88. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  89. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  90. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  91. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  92. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  93. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  94. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  95. package/skills/pr-converge/SKILL.md +13 -16
  96. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  97. package/skills/pr-converge/reference/state-schema.md +1 -27
  98. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  99. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  100. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  101. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  102. package/skills/prototype/SKILL.md +4 -4
  103. package/skills/prototype/reference/honest-limitations.md +4 -4
  104. package/skills/prototype/reference/promotion-tasks.md +2 -2
  105. package/skills/prototype/workflows/promotion.md +4 -6
  106. package/skills/reviews/SKILL.md +85 -0
  107. package/skills/small-cl/SKILL.md +52 -0
  108. package/skills/usage-pause/SKILL.md +1 -1
  109. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  110. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  111. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
  112. package/agents/code-verifier.md +0 -74
  113. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  114. package/docs/references/code-review-enforcement.md +0 -132
  115. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  116. package/hooks/blocking/code_review_gate_deny.py +0 -74
  117. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  118. package/hooks/blocking/code_review_push_gate.py +0 -145
  119. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  120. package/hooks/blocking/code_review_stamp_store.py +0 -233
  121. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  122. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  123. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  124. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  125. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  126. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  127. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  128. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  129. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  130. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  131. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  132. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  133. package/hooks/blocking/conftest.py +0 -123
  134. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  135. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  136. package/hooks/blocking/pr_description_body_audit.py +0 -148
  137. package/hooks/blocking/pr_description_command_parser.py +0 -237
  138. package/hooks/blocking/pr_description_enforcer.py +0 -227
  139. package/hooks/blocking/pr_description_pr_number.py +0 -155
  140. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  141. package/hooks/blocking/pr_description_readability.py +0 -364
  142. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  143. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  144. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  145. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  146. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  147. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  148. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  149. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  150. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  151. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  152. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  153. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  154. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  155. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  156. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  157. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  158. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  159. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  160. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  161. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  162. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  163. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  164. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  165. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  166. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  167. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  168. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  169. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  170. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  171. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  172. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  173. package/hooks/blocking/verification_verdict_store.py +0 -1039
  174. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  175. package/hooks/blocking/verified_commit_gate.py +0 -113
  176. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  177. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  178. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  179. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  180. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  181. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  182. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  183. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  184. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  185. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  186. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  187. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  188. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  189. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  190. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  191. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  192. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  193. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  194. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  195. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  196. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  197. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  198. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  199. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  200. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  201. package/rules/proof-of-work-pr-comments.md +0 -22
  202. package/rules/verified-commit-gate-skip.md +0 -4
  203. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,217 +0,0 @@
1
- """Find every gated ``git commit``/``git push`` and the directory it targets.
2
-
3
- ::
4
-
5
- git commit -m x -> gated in the session directory
6
- cd subdir && git commit -m x -> gated in session_directory/subdir
7
- git -C /repo commit -m x -> gated in /repo
8
- git stash push -> not gated (push is an argument)
9
-
10
- Walks the ``git`` words and directory-change verbs in a command, in the order
11
- they appear, so a commit after a ``cd`` gates against the directory the shell
12
- is actually in when it runs.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import re
18
-
19
- from config.verified_commit_constants import (
20
- DIRECTORY_CHANGE_PATTERN_PREFIX,
21
- DIRECTORY_CHANGE_PATTERN_SUFFIX,
22
- DIRECTORY_CHANGE_VERBS,
23
- GATED_GIT_SUBCOMMANDS,
24
- OPTION_WITH_VALUE_STEP,
25
- REPO_DIRECTORY_OPTION,
26
- VALUE_TAKING_GIT_OPTIONS,
27
- WORK_TREE_OPTION,
28
- )
29
- from config.verified_commit_gate_output_constants import REGEX_ALTERNATION_SEPARATOR
30
- from verified_commit_gate_parts.command_tokenization import (
31
- collapse_line_continuations,
32
- git_word_match_gates,
33
- is_inside_quoted_region,
34
- quoted_spans,
35
- strip_token_quotes,
36
- )
37
- from verified_commit_gate_parts.directory_resolution import (
38
- directory_change_target,
39
- expand_home_prefix,
40
- resolve_against,
41
- split_option_value,
42
- value_after_option,
43
- )
44
-
45
-
46
- def _record_directory_option(
47
- option_name: str,
48
- option_value: str | None,
49
- repo_directory: str | None,
50
- work_tree_directory: str | None,
51
- ) -> tuple[str | None, str | None]:
52
- """Fold one value-taking git option into the tracked target directories."""
53
- if option_value is None:
54
- return repo_directory, work_tree_directory
55
- if option_name == REPO_DIRECTORY_OPTION:
56
- return expand_home_prefix(option_value), work_tree_directory
57
- if option_name == WORK_TREE_OPTION:
58
- return repo_directory, expand_home_prefix(option_value)
59
- return repo_directory, work_tree_directory
60
-
61
-
62
- def _resolved_option_value(
63
- attached_value: str | None, all_following_tokens: list[str], option_index: int
64
- ) -> str | None:
65
- """Read a value-taking option's value: attached (``--opt=v``) or the next token.
66
-
67
- An attached value is used as given, even when it is the empty string
68
- (``--work-tree=``), so an empty value never falls through to read the
69
- following subcommand token as the value.
70
-
71
- Args:
72
- attached_value: The ``=``-attached value, or None when none was attached.
73
- all_following_tokens: Quote-stripped tokens after the ``git`` word.
74
- option_index: Index of the value-taking option token.
75
-
76
- Returns:
77
- The attached value when present, otherwise the separate value token.
78
- """
79
- if attached_value is not None:
80
- return attached_value
81
- return value_after_option(all_following_tokens, option_index)
82
-
83
-
84
- def gated_invocation_directory(
85
- all_following_tokens: list[str],
86
- all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
87
- ) -> tuple[bool, str | None]:
88
- """Walk the quote-stripped tokens after a ``git`` word to its subcommand.
89
-
90
- Args:
91
- all_following_tokens: Quote-stripped tokens after the ``git`` word.
92
- all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
93
- """
94
- repo_directory: str | None = None
95
- work_tree_directory: str | None = None
96
- token_index = 0
97
- while token_index < len(all_following_tokens):
98
- each_token = all_following_tokens[token_index]
99
- option_name, attached_value = split_option_value(each_token)
100
- if option_name in VALUE_TAKING_GIT_OPTIONS:
101
- option_value = _resolved_option_value(
102
- attached_value, all_following_tokens, token_index
103
- )
104
- repo_directory, work_tree_directory = _record_directory_option(
105
- option_name, option_value, repo_directory, work_tree_directory
106
- )
107
- token_index += 1 if attached_value is not None else OPTION_WITH_VALUE_STEP
108
- continue
109
- if each_token.startswith("-"):
110
- token_index += 1
111
- continue
112
- return each_token.lower() in all_gated_subcommands, repo_directory or work_tree_directory
113
- return False, repo_directory or work_tree_directory
114
-
115
-
116
- def _directory_change_verb_pattern() -> re.Pattern[str]:
117
- """Build the regex matching any directory-change verb spelling."""
118
- verb_alternation = REGEX_ALTERNATION_SEPARATOR.join(
119
- re.escape(each_verb) for each_verb in sorted(DIRECTORY_CHANGE_VERBS)
120
- )
121
- return re.compile(
122
- DIRECTORY_CHANGE_PATTERN_PREFIX + verb_alternation + DIRECTORY_CHANGE_PATTERN_SUFFIX,
123
- re.IGNORECASE,
124
- )
125
-
126
-
127
- def _ordered_gate_matches(command_text: str) -> list[re.Match[str]]:
128
- """Collect every ``git`` word and directory-change verb match, in order."""
129
- git_word_pattern = re.compile(
130
- r"(?:^|(?<=[\s;&|(\"'/\\]))git(?:\.exe)?(?:[\"'](?=\s|$)|(?=\s|$))",
131
- re.IGNORECASE,
132
- )
133
- all_quoted_spans = quoted_spans(command_text)
134
- all_directory_change_matches = [
135
- each_match
136
- for each_match in _directory_change_verb_pattern().finditer(command_text)
137
- if not is_inside_quoted_region(each_match.start(), all_quoted_spans)
138
- ]
139
- all_git_word_matches = [
140
- each_match
141
- for each_match in git_word_pattern.finditer(command_text)
142
- if git_word_match_gates(each_match, command_text, all_quoted_spans)
143
- ]
144
- return sorted(
145
- all_git_word_matches + all_directory_change_matches,
146
- key=lambda each_match: each_match.start(),
147
- )
148
-
149
-
150
- def _following_tokens(command_text: str, match_end: int) -> list[str]:
151
- """Quote-strip the tokens following a ``git`` word match."""
152
- command_token_pattern = re.compile(r"\"[^\"]*\"|'[^']*'|\S+")
153
- following_text = command_text[match_end:]
154
- return [
155
- strip_token_quotes(each_token)
156
- for each_token in command_token_pattern.findall(following_text)
157
- ]
158
-
159
-
160
- def _apply_directory_change(
161
- command_text: str, directory_change_match: re.Match[str], active_directory: str
162
- ) -> str:
163
- """Resolve the active directory after one ``cd``/``pushd`` match."""
164
- changed_directory = directory_change_target(command_text, directory_change_match.end())
165
- if changed_directory is None:
166
- return active_directory
167
- return resolve_against(active_directory, changed_directory)
168
-
169
-
170
- def _target_directory_for_match(
171
- command_text: str,
172
- git_word_match: re.Match[str],
173
- active_directory: str,
174
- all_gated_subcommands: frozenset[str],
175
- ) -> str | None:
176
- """Resolve the gated directory for one ``git`` word match, or None."""
177
- all_following_tokens = _following_tokens(command_text, git_word_match.end())
178
- is_gated, flagged_directory = gated_invocation_directory(
179
- all_following_tokens, all_gated_subcommands
180
- )
181
- if not is_gated:
182
- return None
183
- if flagged_directory is None:
184
- return active_directory
185
- return resolve_against(active_directory, flagged_directory)
186
-
187
-
188
- def gated_repo_directories(
189
- command_text: str,
190
- fallback_directory: str,
191
- all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
192
- ) -> list[str]:
193
- """Collect the directories of every gated git call found in a command.
194
-
195
- Args:
196
- command_text: The raw command string from the tool payload.
197
- fallback_directory: The session working directory, the active directory
198
- until a directory-change verb or a ``-C`` flag overrides it.
199
- all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
200
-
201
- Returns:
202
- One directory per detected gated invocation, in order; empty when the
203
- command carries no gated git verb.
204
- """
205
- command_text = collapse_line_continuations(command_text)
206
- active_directory = fallback_directory
207
- target_directories: list[str] = []
208
- for each_match in _ordered_gate_matches(command_text):
209
- if each_match.group().lower().strip("\"'") in DIRECTORY_CHANGE_VERBS:
210
- active_directory = _apply_directory_change(command_text, each_match, active_directory)
211
- continue
212
- target_directory = _target_directory_for_match(
213
- command_text, each_match, active_directory, all_gated_subcommands
214
- )
215
- if target_directory is not None:
216
- target_directories.append(target_directory)
217
- return target_directories
@@ -1,10 +0,0 @@
1
- """Put the blocking and hooks directories on sys.path for the parts tests."""
2
-
3
- import sys
4
- from pathlib import Path
5
-
6
- _blocking_directory = str(Path(__file__).resolve().parents[2])
7
- _hooks_directory = str(Path(__file__).resolve().parents[3])
8
- for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
9
- if each_bootstrap_directory not in sys.path:
10
- sys.path.insert(0, each_bootstrap_directory)
@@ -1,94 +0,0 @@
1
- """Behavioral tests for the verified-commit gate's command tokenization."""
2
-
3
- import re
4
-
5
- from verified_commit_gate_parts.command_tokenization import (
6
- collapse_line_continuations,
7
- command_carries_trailing_marker,
8
- containing_quoted_span,
9
- git_word_match_gates,
10
- is_inside_quoted_region,
11
- quoted_spans,
12
- strip_token_quotes,
13
- )
14
-
15
- VERIFY_SKIP_MARKER = "# verify-skip"
16
-
17
-
18
- def test_command_carries_trailing_marker_true_for_a_real_trailing_comment() -> None:
19
- command_text = f'git commit -m "wire up" {VERIFY_SKIP_MARKER}'
20
- assert command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
21
-
22
-
23
- def test_command_carries_trailing_marker_false_when_marker_sits_inside_quotes() -> None:
24
- command_text = f'git commit -m "explain the {VERIFY_SKIP_MARKER} hatch"'
25
- assert not command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
26
-
27
-
28
- def test_command_carries_trailing_marker_true_with_trailing_words_after_marker() -> None:
29
- command_text = f"git commit -m x {VERIFY_SKIP_MARKER} please"
30
- assert command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
31
-
32
-
33
- def test_command_carries_trailing_marker_false_when_hash_abuts_a_prior_hash() -> None:
34
- command_text = f"git commit -m x #{VERIFY_SKIP_MARKER}"
35
- assert not command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
36
-
37
-
38
- def test_command_carries_trailing_marker_false_when_hash_abuts_a_closing_quote() -> None:
39
- command_text = f'git commit -m "msg"{VERIFY_SKIP_MARKER}'
40
- assert not command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
41
-
42
-
43
- def test_command_carries_trailing_marker_true_when_real_comment_follows_quoted_mention() -> None:
44
- command_text = f'git commit -m "see {VERIFY_SKIP_MARKER}" {VERIFY_SKIP_MARKER}'
45
- assert command_carries_trailing_marker(command_text, VERIFY_SKIP_MARKER)
46
-
47
-
48
- def test_collapse_line_continuations_joins_a_split_git_word() -> None:
49
- assert collapse_line_continuations("git \\\ncommit -m x") == "git commit -m x"
50
-
51
-
52
- def test_quoted_spans_finds_each_quoted_region_in_order() -> None:
53
- all_spans = quoted_spans("git commit -m \"first\" -m 'second'")
54
- assert len(all_spans) == 2
55
- assert all_spans[0][0] < all_spans[1][0]
56
-
57
-
58
- def test_is_inside_quoted_region_true_for_a_position_inside_a_span() -> None:
59
- command_text = 'echo "Next: git commit"'
60
- all_spans = quoted_spans(command_text)
61
- assert is_inside_quoted_region(command_text.index("git"), all_spans)
62
-
63
-
64
- def test_containing_quoted_span_returns_none_outside_any_quotes() -> None:
65
- command_text = 'git commit -m "msg"'
66
- all_spans = quoted_spans(command_text)
67
- assert containing_quoted_span(command_text.index("git"), all_spans) is None
68
-
69
-
70
- def test_strip_token_quotes_removes_unpaired_edge_quote() -> None:
71
- assert strip_token_quotes('push"') == "push"
72
-
73
-
74
- def test_git_word_match_gates_true_for_a_bare_git_word() -> None:
75
- command_text = "git commit -m x"
76
- git_word_match = re.search(r"git", command_text)
77
- assert git_word_match is not None
78
- assert git_word_match_gates(git_word_match, command_text, quoted_spans(command_text))
79
-
80
-
81
- def test_git_word_match_gates_false_for_git_inside_prose() -> None:
82
- command_text = 'echo "Next: git commit"'
83
- all_spans = quoted_spans(command_text)
84
- git_word_match = re.search(r"git", command_text)
85
- assert git_word_match is not None
86
- assert not git_word_match_gates(git_word_match, command_text, all_spans)
87
-
88
-
89
- def test_git_word_match_gates_true_for_a_quoted_git_binary_path() -> None:
90
- command_text = "& 'C:/x/git.exe' commit -m x"
91
- all_spans = quoted_spans(command_text)
92
- git_word_match = re.search(r"git", command_text)
93
- assert git_word_match is not None
94
- assert git_word_match_gates(git_word_match, command_text, all_spans)
@@ -1,17 +0,0 @@
1
- """Behavioral tests for the verified-commit gate's deny-payload shape."""
2
-
3
- from verified_commit_gate_parts.deny_payload import build_deny_payload
4
-
5
-
6
- def test_build_deny_payload_carries_the_deny_decision_and_reason() -> None:
7
- deny_payload = build_deny_payload("BLOCKED: needs a verdict")
8
- hook_specific_output = deny_payload["hookSpecificOutput"]
9
- assert hook_specific_output["hookEventName"] == "PreToolUse"
10
- assert hook_specific_output["permissionDecision"] == "deny"
11
- assert hook_specific_output["permissionDecisionReason"] == "BLOCKED: needs a verdict"
12
-
13
-
14
- def test_build_deny_payload_carries_the_verify_skip_additional_context() -> None:
15
- deny_payload = build_deny_payload("BLOCKED: needs a verdict")
16
- additional_context = deny_payload["hookSpecificOutput"]["additionalContext"]
17
- assert "# verify-skip" in additional_context
@@ -1,38 +0,0 @@
1
- """Behavioral tests for the verified-commit gate's deny-reason resolution."""
2
-
3
- import subprocess
4
- from pathlib import Path
5
-
6
- from verified_commit_gate_parts.deny_reason import deny_reason_for_directory
7
-
8
-
9
- def _run_git(work_dir: Path, *git_arguments: str) -> None:
10
- subprocess.run(
11
- ["git", "-C", str(work_dir), *git_arguments],
12
- check=True,
13
- capture_output=True,
14
- text=True,
15
- )
16
-
17
-
18
- def _make_repo_with_no_upstream(tmp_path: Path) -> Path:
19
- work_dir = tmp_path / "work"
20
- work_dir.mkdir()
21
- _run_git(work_dir, "init", "--initial-branch=main")
22
- _run_git(work_dir, "config", "user.email", "tests@example.com")
23
- _run_git(work_dir, "config", "user.name", "Gate Tests")
24
- (work_dir / "app.py").write_text(
25
- "def add(left: int, right: int) -> int:\n return left + right\n", encoding="utf-8"
26
- )
27
- _run_git(work_dir, "add", "-A")
28
- _run_git(work_dir, "commit", "-m", "base")
29
- return work_dir
30
-
31
-
32
- def test_deny_reason_for_directory_none_when_not_a_git_repo(tmp_path: Path) -> None:
33
- assert deny_reason_for_directory(str(tmp_path), "") is None
34
-
35
-
36
- def test_deny_reason_for_directory_none_with_no_resolvable_upstream(tmp_path: Path) -> None:
37
- work_dir = _make_repo_with_no_upstream(tmp_path)
38
- assert deny_reason_for_directory(str(work_dir), "") is None
@@ -1,71 +0,0 @@
1
- """Behavioral tests for the verified-commit gate's directory-change resolution."""
2
-
3
- import os
4
-
5
- import pytest
6
-
7
- from verified_commit_gate_parts.directory_resolution import (
8
- argument_tokens_after_verb,
9
- directory_change_target,
10
- expand_home_prefix,
11
- is_absolute_directory,
12
- resolve_against,
13
- split_option_value,
14
- value_after_option,
15
- )
16
-
17
-
18
- def test_split_option_value_splits_an_equals_form_option() -> None:
19
- assert split_option_value("--work-tree=/repo") == ("--work-tree", "/repo")
20
-
21
-
22
- def test_split_option_value_returns_none_value_for_a_bare_option() -> None:
23
- assert split_option_value("-C") == ("-C", None)
24
-
25
-
26
- def test_value_after_option_reads_the_next_token() -> None:
27
- assert value_after_option(["-C", "/repo", "commit"], 0) == "/repo"
28
-
29
-
30
- def test_value_after_option_returns_none_at_the_end() -> None:
31
- assert value_after_option(["-C"], 0) is None
32
-
33
-
34
- def test_expand_home_prefix_expands_a_leading_tilde(monkeypatch: pytest.MonkeyPatch) -> None:
35
- monkeypatch.setenv("HOME", "C:/fake-home")
36
- monkeypatch.setenv("USERPROFILE", "C:/fake-home")
37
- assert expand_home_prefix("~/repo") == os.path.expanduser("~/repo")
38
-
39
-
40
- def test_is_absolute_directory_true_for_a_drive_path() -> None:
41
- assert is_absolute_directory("C:/repo")
42
-
43
-
44
- def test_is_absolute_directory_false_for_a_relative_path() -> None:
45
- assert not is_absolute_directory("subdir")
46
-
47
-
48
- def test_resolve_against_joins_a_relative_target() -> None:
49
- assert resolve_against("/session", "subdir") == os.path.join("/session", "subdir")
50
-
51
-
52
- def test_resolve_against_replaces_with_an_absolute_target() -> None:
53
- assert resolve_against("/session", "/repo") == "/repo"
54
-
55
-
56
- def test_argument_tokens_after_verb_stops_at_a_separator() -> None:
57
- command_text = "cd subdir; git commit"
58
- match_end = command_text.index("cd") + len("cd")
59
- assert argument_tokens_after_verb(command_text, match_end) == ["subdir"]
60
-
61
-
62
- def test_directory_change_target_reads_the_destination() -> None:
63
- command_text = "cd /repo"
64
- match_end = command_text.index("cd") + len("cd")
65
- assert directory_change_target(command_text, match_end) == "/repo"
66
-
67
-
68
- def test_directory_change_target_none_for_a_bare_change() -> None:
69
- command_text = "cd && git commit"
70
- match_end = command_text.index("cd") + len("cd")
71
- assert directory_change_target(command_text, match_end) is None
@@ -1,96 +0,0 @@
1
- """Behavioral tests for gated git-invocation directory resolution."""
2
-
3
- import os
4
-
5
- from verified_commit_gate_parts.gated_invocations import (
6
- gated_invocation_directory,
7
- gated_repo_directories,
8
- )
9
-
10
-
11
- def test_gated_invocation_directory_true_for_commit_in_subcommand_position() -> None:
12
- is_gated, repo_directory = gated_invocation_directory(["commit", "-m", "x"])
13
- assert is_gated
14
- assert repo_directory is None
15
-
16
-
17
- def test_gated_invocation_directory_false_for_commit_as_an_argument() -> None:
18
- is_gated, _ = gated_invocation_directory(["stash", "push"])
19
- assert not is_gated
20
-
21
-
22
- def test_gated_invocation_directory_reads_the_dash_c_directory() -> None:
23
- is_gated, repo_directory = gated_invocation_directory(["-C", "/repo", "commit"])
24
- assert is_gated
25
- assert repo_directory == "/repo"
26
-
27
-
28
- def test_gated_repo_directories_finds_a_bare_commit() -> None:
29
- assert gated_repo_directories("git commit -m x", "/session") == ["/session"]
30
-
31
-
32
- def test_gated_repo_directories_ignores_a_quoted_prose_mention() -> None:
33
- assert gated_repo_directories('echo "Next: git commit"', "/session") == []
34
-
35
-
36
- def test_gated_repo_directories_follows_a_preceding_cd() -> None:
37
- assert gated_repo_directories("cd subdir && git commit -m x", "/session") == [
38
- os.path.join("/session", "subdir")
39
- ]
40
-
41
-
42
- def test_gated_repo_directories_honors_a_dash_c_flag() -> None:
43
- assert gated_repo_directories("git -C /repo commit -m x", "/session") == ["/repo"]
44
-
45
-
46
- def test_gated_repo_directories_empty_for_a_non_gated_subcommand() -> None:
47
- assert gated_repo_directories("git status", "/session") == []
48
-
49
-
50
- def test_gated_invocation_directory_reads_an_empty_work_tree_value_not_the_subcommand() -> None:
51
- is_gated, work_tree_directory = gated_invocation_directory(
52
- ["--work-tree=", "commit", "-m", "x"]
53
- )
54
- assert is_gated
55
- assert work_tree_directory == ""
56
-
57
-
58
- def test_gated_repo_directories_gates_an_empty_work_tree_against_the_session() -> None:
59
- assert gated_repo_directories("git --work-tree= commit -m x", "/session") == [
60
- os.path.join("/session", "")
61
- ]
62
-
63
-
64
- def test_default_gated_subcommands_gates_both_commit_and_push() -> None:
65
- assert gated_repo_directories("git commit -m x", "/session") == ["/session"]
66
- assert gated_repo_directories("git push", "/session") == ["/session"]
67
-
68
-
69
- def test_push_only_subcommands_gate_push_but_not_commit() -> None:
70
- push_only = frozenset({"push"})
71
- assert gated_invocation_directory(["push"], push_only)[0] is True
72
- assert gated_invocation_directory(["commit", "-m", "x"], push_only)[0] is False
73
-
74
-
75
- def test_commit_only_subcommands_gate_commit_but_not_push() -> None:
76
- commit_only = frozenset({"commit"})
77
- assert gated_invocation_directory(["commit", "-m", "x"], commit_only)[0] is True
78
- assert gated_invocation_directory(["push"], commit_only)[0] is False
79
-
80
-
81
- def test_gated_repo_directories_threads_push_only_subcommands() -> None:
82
- push_only = frozenset({"push"})
83
- assert gated_repo_directories("git push", "/session", push_only) == ["/session"]
84
- assert gated_repo_directories("git commit -m x", "/session", push_only) == []
85
-
86
-
87
- def test_gated_repo_directories_accepts_the_all_gated_subcommands_keyword() -> None:
88
- push_only = frozenset({"push"})
89
- assert gated_repo_directories(
90
- "git push", "/session", all_gated_subcommands=push_only
91
- ) == ["/session"]
92
-
93
-
94
- def test_gated_invocation_directory_accepts_the_all_gated_subcommands_keyword() -> None:
95
- push_only = frozenset({"push"})
96
- assert gated_invocation_directory(["push"], all_gated_subcommands=push_only)[0] is True