claude-dev-env 7.0.0 → 7.1.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 +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -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 +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -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,204 +0,0 @@
1
- """Production-neutral fixtures for session-edit stage gate tests."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import subprocess
8
- import sys
9
- from pathlib import Path
10
-
11
- _SUPPORT_DIRECTORY = Path(__file__).resolve().parent
12
- _HOOKS_TREE = _SUPPORT_DIRECTORY.parent
13
- if str(_HOOKS_TREE) not in sys.path:
14
- sys.path.insert(0, str(_HOOKS_TREE))
15
-
16
- from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
17
- ALL_EDITED_FILE_PATHS_KEY,
18
- SESSION_EDIT_FILE_PREFIX,
19
- SESSION_EDIT_FILE_SUFFIX,
20
- )
21
-
22
-
23
- def clean_git_environment() -> dict[str, str]:
24
- """Return the environment without inherited repository overrides."""
25
- return {
26
- each_key: each_value
27
- for each_key, each_value in os.environ.items()
28
- if not each_key.startswith("GIT_")
29
- }
30
-
31
-
32
- def run_git(repository_root: Path, *git_arguments: str) -> None:
33
- """Run one Git command in a test repository."""
34
- subprocess.run(
35
- ["git", "-C", str(repository_root), *git_arguments],
36
- check=True,
37
- capture_output=True,
38
- env=clean_git_environment(),
39
- )
40
-
41
-
42
- def initialize_repository(repository_root: Path) -> None:
43
- """Create a Git repository on a non-protected test branch."""
44
- run_git(repository_root, "init")
45
- run_git(repository_root, "checkout", "-b", "test-session-stage")
46
- run_git(repository_root, "config", "user.email", "tests@example.com")
47
- run_git(repository_root, "config", "user.name", "Gate Tests")
48
-
49
-
50
- def prepare_repository_with_unstaged_edit(repository_root: Path) -> Path:
51
- """Create one tracked file with an unstaged edit."""
52
- initialize_repository(repository_root)
53
- tracked_file = repository_root / "widget.py"
54
- tracked_file.write_text("x = 1\n", encoding="utf-8")
55
- run_git(repository_root, "add", "widget.py")
56
- run_git(repository_root, "commit", "-m", "add widget")
57
- tracked_file.write_text("x = 2\n", encoding="utf-8")
58
- return tracked_file
59
-
60
-
61
- def prepare_repository_with_two_unstaged_edits(repository_root: Path) -> tuple[Path, Path]:
62
- """Create two tracked files with separate unstaged edits."""
63
- initialize_repository(repository_root)
64
- tracked_file = repository_root / "widget.py"
65
- second_file = repository_root / "second_widget.py"
66
- tracked_file.write_text("x = 1\n", encoding="utf-8")
67
- second_file.write_text("y = 1\n", encoding="utf-8")
68
- run_git(repository_root, "add", "widget.py", "second_widget.py")
69
- run_git(repository_root, "commit", "-m", "add widgets")
70
- tracked_file.write_text("x = 2\n", encoding="utf-8")
71
- second_file.write_text("y = 2\n", encoding="utf-8")
72
- return tracked_file, second_file
73
-
74
-
75
- def make_test_directories(tmp_path: Path) -> tuple[Path, Path, Path]:
76
- """Create repository, tracker, and home directories for one test."""
77
- repository_root = tmp_path / "repo"
78
- repository_root.mkdir()
79
- temp_directory = tmp_path / "tmp"
80
- temp_directory.mkdir()
81
- home_directory = tmp_path / "home"
82
- home_directory.mkdir()
83
- return repository_root, temp_directory, home_directory
84
-
85
-
86
- def make_process_repository(tmp_path: Path) -> Path:
87
- """Create a separate Git repository for the dispatcher process directory."""
88
- process_repository = tmp_path / "process-repository"
89
- process_repository.mkdir()
90
- initialize_repository(process_repository)
91
- return process_repository
92
-
93
-
94
- def prepare_process_child_repository(process_repository: Path) -> None:
95
- """Create the relative command directory inside process repository B."""
96
- process_child_repository = process_repository / "child"
97
- process_child_repository.mkdir()
98
- initialize_repository(process_child_repository)
99
-
100
-
101
- def build_hook_environment(
102
- temp_directory: Path,
103
- home_directory: Path,
104
- ) -> dict[str, str]:
105
- """Build isolated environment variables for production hooks."""
106
- environment = clean_git_environment()
107
- environment["TMP"] = str(temp_directory)
108
- environment["TEMP"] = str(temp_directory)
109
- environment["TMPDIR"] = str(temp_directory)
110
- environment["USERPROFILE"] = str(home_directory)
111
- environment["HOME"] = str(home_directory)
112
- return environment
113
-
114
-
115
- def run_production_hook(
116
- hook_path: Path,
117
- payload: str,
118
- repository_root: Path,
119
- temp_directory: Path,
120
- home_directory: Path,
121
- process_directory: Path | None = None,
122
- ) -> subprocess.CompletedProcess[str]:
123
- """Run one hook script as a production subprocess."""
124
- return subprocess.run(
125
- [sys.executable, str(hook_path)],
126
- check=False,
127
- input=payload,
128
- capture_output=True,
129
- text=True,
130
- cwd=str(process_directory or repository_root),
131
- env=build_hook_environment(temp_directory, home_directory),
132
- timeout=60,
133
- )
134
-
135
-
136
- def run_tracker_event(
137
- tracked_file: Path,
138
- session_id: str,
139
- repository_root: Path,
140
- temp_directory: Path,
141
- home_directory: Path,
142
- ) -> subprocess.CompletedProcess[str]:
143
- """Run the production PostToolUse tracker for one edited file."""
144
- payload = json.dumps(
145
- {
146
- "tool_name": "Edit",
147
- "session_id": session_id,
148
- "tool_input": {"file_path": str(tracked_file)},
149
- }
150
- )
151
- return run_production_hook(
152
- _HOOKS_TREE / "observability" / "session_file_edit_tracker.py",
153
- payload,
154
- repository_root,
155
- temp_directory,
156
- home_directory,
157
- )
158
-
159
-
160
- def run_bash_dispatcher(
161
- bash_command: str,
162
- session_id: str,
163
- repository_root: Path,
164
- temp_directory: Path,
165
- home_directory: Path,
166
- process_directory: Path | None = None,
167
- ) -> subprocess.CompletedProcess[str]:
168
- """Run the production Bash PreToolUse dispatcher for one command."""
169
- payload = json.dumps(
170
- {
171
- "tool_name": "Bash",
172
- "cwd": str(repository_root),
173
- "session_id": session_id,
174
- "tool_input": {"command": bash_command},
175
- }
176
- )
177
- return run_production_hook(
178
- _SUPPORT_DIRECTORY / "bash_pre_tool_use_dispatcher.py",
179
- payload,
180
- repository_root,
181
- temp_directory,
182
- home_directory,
183
- process_directory,
184
- )
185
-
186
-
187
- def read_tracker_paths(temp_directory: Path, session_id: str) -> list[str]:
188
- """Read the recorded paths from one production tracker file."""
189
- tracker_file = (
190
- temp_directory / f"{SESSION_EDIT_FILE_PREFIX}{session_id}{SESSION_EDIT_FILE_SUFFIX}"
191
- )
192
- tracker_payload = json.loads(tracker_file.read_text(encoding="utf-8"))
193
- return tracker_payload[ALL_EDITED_FILE_PATHS_KEY]
194
-
195
-
196
- def parse_dispatcher_decision(stdout_text: str) -> tuple[str, str]:
197
- """Return the dispatcher decision and reason from its stdout payload."""
198
- if not stdout_text.strip():
199
- return "", ""
200
- hook_specific_output = json.loads(stdout_text)["hookSpecificOutput"]
201
- return (
202
- hook_specific_output.get("permissionDecision", ""),
203
- hook_specific_output.get("permissionDecisionReason", ""),
204
- )
@@ -1,116 +0,0 @@
1
- """Production-path tracker event-order tests for the session-edit stage gate."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from pathlib import Path
7
-
8
- import pytest
9
-
10
- _TEST_DIRECTORY = Path(__file__).resolve().parent
11
- if str(_TEST_DIRECTORY) not in sys.path:
12
- sys.path.insert(0, str(_TEST_DIRECTORY))
13
-
14
- from test_session_edit_stage_gate_support import ( # noqa: E402
15
- make_test_directories,
16
- make_process_repository,
17
- parse_dispatcher_decision,
18
- prepare_process_child_repository,
19
- prepare_repository_with_unstaged_edit,
20
- prepare_repository_with_two_unstaged_edits,
21
- read_tracker_paths,
22
- run_bash_dispatcher,
23
- run_tracker_event,
24
- )
25
-
26
- SESSION_ID = "tracker-production-session"
27
-
28
-
29
- def test_tracker_events_precede_dispatcher_stage_decision(tmp_path: Path) -> None:
30
- """The dispatcher sees every edit recorded before the commit event."""
31
- repository_root, temp_directory, home_directory = make_test_directories(tmp_path)
32
- tracked_file, second_file = prepare_repository_with_two_unstaged_edits(repository_root)
33
- for each_file in (tracked_file, second_file):
34
- tracker_run = run_tracker_event(
35
- each_file, SESSION_ID, repository_root, temp_directory, home_directory
36
- )
37
- assert tracker_run.returncode == 0
38
-
39
- assert read_tracker_paths(temp_directory, SESSION_ID) == [
40
- str(tracked_file.resolve()),
41
- str(second_file.resolve()),
42
- ]
43
-
44
- dispatcher_run = run_bash_dispatcher(
45
- "git commit -m update",
46
- SESSION_ID,
47
- repository_root,
48
- temp_directory,
49
- home_directory,
50
- )
51
- assert dispatcher_run.returncode == 0
52
- decision, reason = parse_dispatcher_decision(dispatcher_run.stdout)
53
- assert decision == "deny"
54
- assert "widget.py" in reason
55
- assert "second_widget.py" in reason
56
-
57
-
58
- def test_dispatcher_uses_event_cwd_when_process_runs_elsewhere(tmp_path: Path) -> None:
59
- """The dispatcher passes event cwd so the gate inspects the event repository."""
60
- repository_root, temp_directory, home_directory = make_test_directories(tmp_path)
61
- process_repository = make_process_repository(tmp_path)
62
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
63
- tracker_run = run_tracker_event(
64
- tracked_file, SESSION_ID, repository_root, temp_directory, home_directory
65
- )
66
- assert tracker_run.returncode == 0
67
-
68
- dispatcher_run = run_bash_dispatcher(
69
- "git commit -m update",
70
- SESSION_ID,
71
- repository_root,
72
- temp_directory,
73
- home_directory,
74
- process_directory=process_repository,
75
- )
76
- assert dispatcher_run.returncode == 0
77
- decision, reason = parse_dispatcher_decision(dispatcher_run.stdout)
78
- assert decision == "deny"
79
- assert "widget.py" in reason
80
-
81
-
82
- @pytest.mark.parametrize(
83
- "bash_command",
84
- [
85
- "cd child && git commit -m update",
86
- "pushd child && git commit -m update",
87
- "git -C child commit -m update",
88
- ],
89
- )
90
- def test_dispatcher_anchors_relative_command_directory_to_event_cwd(
91
- tmp_path: Path,
92
- bash_command: str,
93
- ) -> None:
94
- """Relative commit directories resolve from event cwd across command forms."""
95
- repository_root, temp_directory, home_directory = make_test_directories(tmp_path)
96
- process_repository = make_process_repository(tmp_path)
97
- prepare_process_child_repository(process_repository)
98
- (repository_root / "child").mkdir()
99
- tracked_file = prepare_repository_with_unstaged_edit(repository_root)
100
- tracker_run = run_tracker_event(
101
- tracked_file, SESSION_ID, repository_root, temp_directory, home_directory
102
- )
103
- assert tracker_run.returncode == 0
104
-
105
- dispatcher_run = run_bash_dispatcher(
106
- bash_command,
107
- SESSION_ID,
108
- repository_root,
109
- temp_directory,
110
- home_directory,
111
- process_directory=process_repository,
112
- )
113
- assert dispatcher_run.returncode == 0
114
- decision, reason = parse_dispatcher_decision(dispatcher_run.stdout)
115
- assert decision == "deny"
116
- assert "widget.py" in reason
@@ -1,318 +0,0 @@
1
- """Tests for session_handoff_blocker hook response shape."""
2
-
3
- import importlib.util
4
- import json
5
- import os
6
- import subprocess
7
- import sys
8
-
9
- HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "session_handoff_blocker.py")
10
- _HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
11
- _HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
12
- _HOOK_CONFIG_DIR = os.path.join(_HOOKS_ROOT, "hooks_constants")
13
- if _HOOKS_DIR not in sys.path:
14
- sys.path.insert(0, _HOOKS_DIR)
15
- if _HOOKS_ROOT not in sys.path:
16
- sys.path.insert(0, _HOOKS_ROOT)
17
- import session_handoff_blocker
18
- from hooks_constants.messages import USER_FACING_CONTEXT_REASSURANCE_NOTICE
19
- from hooks_constants.text_stripping import strip_code_and_quotes
20
-
21
-
22
- def test_blocker_uses_shared_strip_code_and_quotes() -> None:
23
- assert session_handoff_blocker.strip_code_and_quotes is strip_code_and_quotes
24
-
25
-
26
- NEW_SESSION_PROPOSAL_MESSAGE = (
27
- "I recommend we continue this in a fresh session to keep things manageable."
28
- )
29
- RUNNING_LOW_ON_CONTEXT_MESSAGE = (
30
- "We are running low on context, so let me summarize where things stand."
31
- )
32
- SHORT_ON_TOKENS_MESSAGE = "I'm getting short on tokens, so I'll wrap up here."
33
- CONSERVE_CONTEXT_MESSAGE = "To conserve context, let me stop and hand off the remaining work."
34
- CONTEXT_WINDOW_HANDOFF_MESSAGE = (
35
- "The context window is filling up, so I'll wrap up and we can continue later."
36
- )
37
- BENIGN_TOPICAL_MESSAGE = "The function accepts a context manager and a token string."
38
- CLEAN_MESSAGE = "The parser handles every fixture and returns a deduplicated list."
39
- TECHNICAL_TERMINAL_SESSION_MESSAGE = (
40
- "Consider starting a new session in your terminal to pick up the env vars."
41
- )
42
- LOAD_TEST_SESSION_MESSAGE = "We can spin up a fresh session for the load test."
43
- DATABASE_SESSION_MESSAGE = "Open a new database session before running the query."
44
- HANDOFF_NEW_SESSION_MESSAGE = (
45
- "Let's wrap up and continue this in a fresh session to pick this up later."
46
- )
47
- LOW_ON_CONTEXT_WITHOUT_CUE_MESSAGE = (
48
- "I am low on context for this edge case in the parser."
49
- )
50
- SAVE_TOKENS_REPORT_MESSAGE = "To save tokens, I inlined the constant."
51
- LOW_ON_CONTEXT_WITH_HANDOFF_CUE_MESSAGE = (
52
- "I'm low on context, so let me wrap up and hand off."
53
- )
54
- NEW_SESSION_TOKEN_MESSAGE = (
55
- "To continue this task I need the new session token from the API."
56
- )
57
- RESUME_TRAFFIC_FRESH_SESSION_POOL_MESSAGE = (
58
- "After deploy, resume traffic in a fresh session pool."
59
- )
60
- THIRD_PERSON_RUNS_LOW_DESIGN_MESSAGE = (
61
- "The agent should stop when it runs low on context, per the old design."
62
- )
63
- DOCUMENT_RUNNING_LOW_MESSAGE = (
64
- "Document that agents running low on tokens should not stop."
65
- )
66
- USER_ADVICE_NEW_SESSION_MESSAGE = (
67
- "To save tokens, you could start a new session for the unrelated task."
68
- )
69
- CONTEXT_WINDOW_SUMMARIZE_MESSAGE = (
70
- "The context window is getting full, so let me summarize where we are."
71
- )
72
- BENIGN_SUMMARIZE_REPORT_MESSAGE = "I should summarize the findings for the report."
73
- BENIGN_STOP_PLUS_UNRELATED_RUNNING_LOW_MESSAGE = (
74
- "Let me pause the animation timer. The job fails when it is running low on tokens."
75
- )
76
- BENIGN_STOP_PLUS_USER_DIRECTED_NEW_SESSION_MESSAGE = (
77
- "Let me stop the timer. The user can continue this in a fresh session."
78
- )
79
- EMPTY_MESSAGE = ""
80
-
81
-
82
- def run_hook_with_message(assistant_message: str) -> subprocess.CompletedProcess:
83
- hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
84
- return subprocess.run(
85
- [sys.executable, HOOK_SCRIPT_PATH],
86
- input=hook_input_payload,
87
- capture_output=True,
88
- text=True,
89
- check=False,
90
- )
91
-
92
-
93
- def run_hook_with_payload(hook_input_payload: dict) -> subprocess.CompletedProcess:
94
- return subprocess.run(
95
- [sys.executable, HOOK_SCRIPT_PATH],
96
- input=json.dumps(hook_input_payload),
97
- capture_output=True,
98
- text=True,
99
- check=False,
100
- )
101
-
102
-
103
- def test_user_facing_notice_matches_config_messages_module():
104
- config_messages_path = os.path.join(_HOOK_CONFIG_DIR, "messages.py")
105
- specification = importlib.util.spec_from_file_location("messages", config_messages_path)
106
- module = importlib.util.module_from_spec(specification)
107
- specification.loader.exec_module(module)
108
-
109
- assert module.USER_FACING_CONTEXT_REASSURANCE_NOTICE == USER_FACING_CONTEXT_REASSURANCE_NOTICE
110
- assert (
111
- session_handoff_blocker.USER_FACING_CONTEXT_REASSURANCE_NOTICE
112
- == module.USER_FACING_CONTEXT_REASSURANCE_NOTICE
113
- )
114
-
115
-
116
- def test_new_session_proposal_emits_block_with_short_user_notice():
117
- completed_process = run_hook_with_message(NEW_SESSION_PROPOSAL_MESSAGE)
118
-
119
- assert completed_process.returncode == 0
120
- parsed_response = json.loads(completed_process.stdout)
121
-
122
- assert parsed_response["decision"] == "block"
123
- assert parsed_response["systemMessage"] == USER_FACING_CONTEXT_REASSURANCE_NOTICE
124
- assert parsed_response["suppressOutput"] is True
125
- assert "ample context remaining" in parsed_response["reason"]
126
- assert "long-horizon-autonomy" in parsed_response["reason"]
127
-
128
-
129
- def test_running_low_on_context_emits_block():
130
- completed_process = run_hook_with_message(RUNNING_LOW_ON_CONTEXT_MESSAGE)
131
-
132
- assert completed_process.returncode == 0
133
- parsed_response = json.loads(completed_process.stdout)
134
-
135
- assert parsed_response["decision"] == "block"
136
- assert parsed_response["systemMessage"] == USER_FACING_CONTEXT_REASSURANCE_NOTICE
137
-
138
-
139
- def test_short_on_tokens_emits_block():
140
- completed_process = run_hook_with_message(SHORT_ON_TOKENS_MESSAGE)
141
-
142
- assert completed_process.returncode == 0
143
- parsed_response = json.loads(completed_process.stdout)
144
-
145
- assert parsed_response["decision"] == "block"
146
-
147
-
148
- def test_conserve_context_emits_block():
149
- completed_process = run_hook_with_message(CONSERVE_CONTEXT_MESSAGE)
150
-
151
- assert completed_process.returncode == 0
152
- parsed_response = json.loads(completed_process.stdout)
153
-
154
- assert parsed_response["decision"] == "block"
155
-
156
-
157
- def test_context_window_co_occurring_handoff_cue_emits_block():
158
- completed_process = run_hook_with_message(CONTEXT_WINDOW_HANDOFF_MESSAGE)
159
-
160
- assert completed_process.returncode == 0
161
- parsed_response = json.loads(completed_process.stdout)
162
-
163
- assert parsed_response["decision"] == "block"
164
-
165
-
166
- def test_benign_topical_mention_passes_through_with_no_output():
167
- completed_process = run_hook_with_message(BENIGN_TOPICAL_MESSAGE)
168
-
169
- assert completed_process.returncode == 0
170
- assert completed_process.stdout == ""
171
-
172
-
173
- def test_technical_terminal_session_passes_through_with_no_output():
174
- completed_process = run_hook_with_message(TECHNICAL_TERMINAL_SESSION_MESSAGE)
175
-
176
- assert completed_process.returncode == 0
177
- assert completed_process.stdout == ""
178
-
179
-
180
- def test_load_test_session_passes_through_with_no_output():
181
- completed_process = run_hook_with_message(LOAD_TEST_SESSION_MESSAGE)
182
-
183
- assert completed_process.returncode == 0
184
- assert completed_process.stdout == ""
185
-
186
-
187
- def test_database_session_passes_through_with_no_output():
188
- completed_process = run_hook_with_message(DATABASE_SESSION_MESSAGE)
189
-
190
- assert completed_process.returncode == 0
191
- assert completed_process.stdout == ""
192
-
193
-
194
- def test_new_session_with_handoff_framing_emits_block():
195
- completed_process = run_hook_with_message(HANDOFF_NEW_SESSION_MESSAGE)
196
-
197
- assert completed_process.returncode == 0
198
- parsed_response = json.loads(completed_process.stdout)
199
-
200
- assert parsed_response["decision"] == "block"
201
- assert parsed_response["systemMessage"] == USER_FACING_CONTEXT_REASSURANCE_NOTICE
202
-
203
-
204
- def test_low_on_context_without_handoff_cue_passes_through_with_no_output():
205
- completed_process = run_hook_with_message(LOW_ON_CONTEXT_WITHOUT_CUE_MESSAGE)
206
-
207
- assert completed_process.returncode == 0
208
- assert completed_process.stdout == ""
209
-
210
-
211
- def test_save_tokens_work_report_passes_through_with_no_output():
212
- completed_process = run_hook_with_message(SAVE_TOKENS_REPORT_MESSAGE)
213
-
214
- assert completed_process.returncode == 0
215
- assert completed_process.stdout == ""
216
-
217
-
218
- def test_low_on_context_with_handoff_cue_emits_block():
219
- completed_process = run_hook_with_message(LOW_ON_CONTEXT_WITH_HANDOFF_CUE_MESSAGE)
220
-
221
- assert completed_process.returncode == 0
222
- parsed_response = json.loads(completed_process.stdout)
223
-
224
- assert parsed_response["decision"] == "block"
225
- assert parsed_response["systemMessage"] == USER_FACING_CONTEXT_REASSURANCE_NOTICE
226
-
227
-
228
- def test_new_session_token_passes_through_with_no_output():
229
- completed_process = run_hook_with_message(NEW_SESSION_TOKEN_MESSAGE)
230
-
231
- assert completed_process.returncode == 0
232
- assert completed_process.stdout == ""
233
-
234
-
235
- def test_resume_traffic_fresh_session_pool_passes_through_with_no_output():
236
- completed_process = run_hook_with_message(RESUME_TRAFFIC_FRESH_SESSION_POOL_MESSAGE)
237
-
238
- assert completed_process.returncode == 0
239
- assert completed_process.stdout == ""
240
-
241
-
242
- def test_third_person_runs_low_description_passes_through_with_no_output():
243
- completed_process = run_hook_with_message(THIRD_PERSON_RUNS_LOW_DESIGN_MESSAGE)
244
-
245
- assert completed_process.returncode == 0
246
- assert completed_process.stdout == ""
247
-
248
-
249
- def test_document_running_low_instruction_passes_through_with_no_output():
250
- completed_process = run_hook_with_message(DOCUMENT_RUNNING_LOW_MESSAGE)
251
-
252
- assert completed_process.returncode == 0
253
- assert completed_process.stdout == ""
254
-
255
-
256
- def test_user_directed_new_session_advice_passes_through_with_no_output():
257
- completed_process = run_hook_with_message(USER_ADVICE_NEW_SESSION_MESSAGE)
258
-
259
- assert completed_process.returncode == 0
260
- assert completed_process.stdout == ""
261
-
262
-
263
- def test_context_window_summarize_handoff_emits_block():
264
- completed_process = run_hook_with_message(CONTEXT_WINDOW_SUMMARIZE_MESSAGE)
265
-
266
- assert completed_process.returncode == 0
267
- parsed_response = json.loads(completed_process.stdout)
268
-
269
- assert parsed_response["decision"] == "block"
270
- assert parsed_response["systemMessage"] == USER_FACING_CONTEXT_REASSURANCE_NOTICE
271
-
272
-
273
- def test_benign_summarize_report_passes_through_with_no_output():
274
- completed_process = run_hook_with_message(BENIGN_SUMMARIZE_REPORT_MESSAGE)
275
-
276
- assert completed_process.returncode == 0
277
- assert completed_process.stdout == ""
278
-
279
-
280
- def test_benign_stop_with_unrelated_running_low_sentence_passes_through_with_no_output():
281
- completed_process = run_hook_with_message(
282
- BENIGN_STOP_PLUS_UNRELATED_RUNNING_LOW_MESSAGE
283
- )
284
-
285
- assert completed_process.returncode == 0
286
- assert completed_process.stdout == ""
287
-
288
-
289
- def test_benign_stop_with_user_directed_new_session_sentence_passes_through_with_no_output():
290
- completed_process = run_hook_with_message(
291
- BENIGN_STOP_PLUS_USER_DIRECTED_NEW_SESSION_MESSAGE
292
- )
293
-
294
- assert completed_process.returncode == 0
295
- assert completed_process.stdout == ""
296
-
297
-
298
- def test_clean_message_passes_through_with_no_output():
299
- completed_process = run_hook_with_message(CLEAN_MESSAGE)
300
-
301
- assert completed_process.returncode == 0
302
- assert completed_process.stdout == ""
303
-
304
-
305
- def test_empty_message_passes_through_with_no_output():
306
- completed_process = run_hook_with_message(EMPTY_MESSAGE)
307
-
308
- assert completed_process.returncode == 0
309
- assert completed_process.stdout == ""
310
-
311
-
312
- def test_stop_hook_active_short_circuits_with_no_output():
313
- completed_process = run_hook_with_payload(
314
- {"last_assistant_message": NEW_SESSION_PROPOSAL_MESSAGE, "stop_hook_active": True}
315
- )
316
-
317
- assert completed_process.returncode == 0
318
- assert completed_process.stdout == ""