claude-dev-env 8.74.0 → 8.74.2

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 (39) hide show
  1. package/bin/cde.mjs +1 -1
  2. package/bin/cde.test.mjs +1 -1
  3. package/bin/install.mjs +1 -0
  4. package/bin/install.test.mjs +1 -0
  5. package/bin/merge_profile_settings.mjs +2 -3
  6. package/bin/merge_profile_settings.test.mjs +5 -5
  7. package/commands/fix-followups.md +5 -2
  8. package/docs/rule-guides/flag-non-breaking-findings.md +3 -5
  9. package/docs/rule-guides/paired-test-coverage.md +1 -1
  10. package/docs/rule-guides/session-title.md +3 -3
  11. package/docs/templates/html-plan/README.md +1 -1
  12. package/hooks/features/conduct-gates.md +0 -3
  13. package/hooks/features/session-context.md +1 -1
  14. package/hooks/followup_ledger.py +179 -116
  15. package/hooks/followup_ledger_maintenance.py +127 -0
  16. package/hooks/hooks.json +0 -11
  17. package/hooks/hooks_constants/artifact_template_pointer_constants.py +5 -1
  18. package/hooks/hooks_constants/followup_ledger_constants.py +26 -6
  19. package/hooks/hooks_constants/session_title_constants.py +1 -29
  20. package/hooks/session/artifact_template_pointer.py +5 -4
  21. package/hooks/session/test_artifact_template_pointer.py +13 -0
  22. package/hooks/test_followup_ledger.py +151 -55
  23. package/hooks/test_followup_ledger_maintenance.py +107 -0
  24. package/package.json +1 -1
  25. package/rules/flag-non-breaking-findings.md +1 -1
  26. package/scripts/dev_env_scripts_constants/followup_constants.py +4 -1
  27. package/scripts/followup_cli.py +11 -12
  28. package/scripts/policy_lint/adapter_configuration.py +0 -1
  29. package/scripts/policy_lint/config/constants.py +0 -1
  30. package/scripts/repository_checks/config/constants.py +0 -6
  31. package/scripts/repository_checks/runner.py +0 -7
  32. package/scripts/test_followup_cli.py +71 -7
  33. package/scripts/tests/test_adapter_configuration.py +2 -5
  34. package/scripts/tests/test_cde_lint.py +3 -4
  35. package/scripts/tests/test_repository_policy.py +1 -3
  36. package/hooks/blocking/session_title_stop_gate.py +0 -217
  37. package/hooks/blocking/test_session_title_stop_gate.py +0 -295
  38. package/scripts/repository_checks/followup_ledger_duplicates.py +0 -55
  39. package/scripts/tests/test_followup_ledger_duplicates.py +0 -58
@@ -1,295 +0,0 @@
1
- import io
2
- import json
3
- from pathlib import Path
4
-
5
- import pytest
6
-
7
- import session_title_stop_gate
8
-
9
- REMOTE_TITLE_TOOL = "mcp__claude-code-remote__set_session_title"
10
- DESKTOP_TITLE_TOOL = "mcp__ccd_session_mgmt__set_session_title"
11
- REMOTE_SESSION_VARIABLE = "CLAUDE_CODE_REMOTE_SESSION_ID"
12
-
13
-
14
- def prompt(text: str) -> dict[str, object]:
15
- return {"type": "user", "message": {"role": "user", "content": text}}
16
-
17
-
18
- def meta_prompt(text: str) -> dict[str, object]:
19
- return {
20
- "type": "user",
21
- "isMeta": True,
22
- "message": {"role": "user", "content": [{"type": "text", "text": text}]},
23
- }
24
-
25
-
26
- def tool_call(tool_use_id: str, name: str) -> dict[str, object]:
27
- return {
28
- "type": "assistant",
29
- "message": {
30
- "role": "assistant",
31
- "content": [{"type": "tool_use", "id": tool_use_id, "name": name, "input": {}}],
32
- },
33
- }
34
-
35
-
36
- def tool_result(tool_use_id: str, is_error: bool = False) -> dict[str, object]:
37
- return {
38
- "type": "user",
39
- "message": {
40
- "role": "user",
41
- "content": [
42
- {
43
- "type": "tool_result",
44
- "tool_use_id": tool_use_id,
45
- "content": "ok",
46
- "is_error": is_error,
47
- }
48
- ],
49
- },
50
- }
51
-
52
-
53
- def deferred_tools(*all_names: str) -> dict[str, object]:
54
- return {
55
- "type": "attachment",
56
- "attachment": {"type": "deferred_tools_delta", "addedNames": list(all_names)},
57
- }
58
-
59
-
60
- def run_gate(
61
- monkeypatch: pytest.MonkeyPatch,
62
- capsys: pytest.CaptureFixture[str],
63
- tmp_path: Path,
64
- all_entries: list[dict[str, object]],
65
- stop_hook_active: bool = False,
66
- ) -> tuple[int, str]:
67
- transcript_path = tmp_path / "session.jsonl"
68
- transcript_path.write_text(
69
- "".join(json.dumps(each) + "\n" for each in all_entries), encoding="utf-8"
70
- )
71
- hook_input = {
72
- "hook_event_name": "Stop",
73
- "transcript_path": str(transcript_path),
74
- "stop_hook_active": stop_hook_active,
75
- }
76
- monkeypatch.setattr(
77
- "sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
78
- )
79
- exit_code = session_title_stop_gate.main()
80
- return exit_code, capsys.readouterr().out
81
-
82
-
83
- @pytest.fixture(autouse=True)
84
- def isolated_home(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
85
- monkeypatch.setenv("HOME", str(tmp_path))
86
- monkeypatch.setenv("USERPROFILE", str(tmp_path))
87
- monkeypatch.delenv(REMOTE_SESSION_VARIABLE, raising=False)
88
-
89
-
90
- def test_should_block_a_remote_turn_that_set_no_title(
91
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
92
- ) -> None:
93
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
94
- exit_code, stdout_text = run_gate(
95
- monkeypatch,
96
- capsys,
97
- tmp_path,
98
- [prompt("Fix the runner"), tool_call("t1", "Bash"), tool_result("t1")],
99
- )
100
- decision = json.loads(stdout_text)
101
- assert exit_code == 0
102
- assert decision["decision"] == "block"
103
- assert REMOTE_TITLE_TOOL in decision["reason"]
104
-
105
-
106
- def test_should_pass_a_turn_whose_title_call_succeeded(
107
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
108
- ) -> None:
109
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
110
- all_entries = [prompt("Fix the runner"), tool_call("t1", REMOTE_TITLE_TOOL), tool_result("t1")]
111
- assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
112
-
113
-
114
- def test_should_block_when_the_only_title_call_was_denied(
115
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
116
- ) -> None:
117
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
118
- all_entries = [
119
- prompt("Fix the runner"),
120
- tool_call("t1", REMOTE_TITLE_TOOL),
121
- tool_result("t1", is_error=True),
122
- ]
123
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
124
- assert json.loads(stdout_text)["decision"] == "block"
125
-
126
-
127
- def test_should_block_when_the_title_was_set_only_in_an_earlier_turn(
128
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
129
- ) -> None:
130
- all_entries = [
131
- prompt("Fix the runner"),
132
- tool_call("t1", DESKTOP_TITLE_TOOL),
133
- tool_result("t1"),
134
- prompt("Now open the pull request"),
135
- tool_call("t2", "Bash"),
136
- tool_result("t2"),
137
- ]
138
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
139
- assert DESKTOP_TITLE_TOOL in json.loads(stdout_text)["reason"]
140
-
141
-
142
- def test_should_keep_a_title_set_before_a_meta_entry_in_the_same_turn(
143
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
144
- ) -> None:
145
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
146
- all_entries = [
147
- prompt("Fix the runner"),
148
- tool_call("t1", REMOTE_TITLE_TOOL),
149
- tool_result("t1"),
150
- meta_prompt("Base directory for this skill: /skills/pr-lifecycle"),
151
- ]
152
- assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
153
-
154
-
155
- def test_should_block_a_desktop_session_once_its_title_tool_is_listed(
156
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
157
- ) -> None:
158
- all_entries = [deferred_tools("TaskCreate", DESKTOP_TITLE_TOOL), prompt("Fix the runner")]
159
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
160
- assert DESKTOP_TITLE_TOOL in json.loads(stdout_text)["reason"]
161
-
162
-
163
- def test_should_stay_silent_in_a_session_with_no_title_tool(
164
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
165
- ) -> None:
166
- all_entries = [
167
- deferred_tools("TaskCreate"),
168
- prompt("Fix the runner"),
169
- tool_call("t1", "Bash"),
170
- tool_result("t1"),
171
- ]
172
- assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
173
-
174
-
175
- def test_should_stay_silent_in_a_remote_session_whose_tool_list_lacks_the_title_tool(
176
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
177
- ) -> None:
178
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
179
- all_entries = [
180
- deferred_tools("TaskCreate", "mcp__claude-code-remote__get_session"),
181
- prompt("Fix the runner"),
182
- tool_call("t1", "Bash"),
183
- tool_result("t1"),
184
- ]
185
- assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
186
-
187
-
188
- def test_should_block_a_remote_session_whose_deferred_list_holds_no_remote_server_tool(
189
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
190
- ) -> None:
191
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
192
- all_entries = [
193
- deferred_tools("TaskCreate", "WebFetch"),
194
- prompt("Fix the runner"),
195
- tool_call("t1", "Bash"),
196
- tool_result("t1"),
197
- ]
198
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
199
- assert json.loads(stdout_text)["decision"] == "block"
200
-
201
-
202
- def test_should_block_a_remote_session_whose_tool_list_names_the_title_tool(
203
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
204
- ) -> None:
205
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
206
- all_entries = [
207
- deferred_tools("TaskCreate", REMOTE_TITLE_TOOL),
208
- prompt("Fix the runner"),
209
- tool_call("t1", "Bash"),
210
- tool_result("t1"),
211
- ]
212
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
213
- assert json.loads(stdout_text)["decision"] == "block"
214
-
215
-
216
- def earlier_stop_block() -> dict[str, object]:
217
- return {
218
- "type": "attachment",
219
- "attachment": {"type": "hook_blocking_error", "hookEvent": "Stop"},
220
- }
221
-
222
-
223
- def test_should_stay_silent_in_a_remote_session_after_an_unmet_earlier_block(
224
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
225
- ) -> None:
226
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
227
- all_entries = [
228
- prompt("Fix the runner"),
229
- earlier_stop_block(),
230
- prompt("Next task"),
231
- tool_call("t1", "Bash"),
232
- tool_result("t1"),
233
- ]
234
- assert run_gate(monkeypatch, capsys, tmp_path, all_entries) == (0, "")
235
-
236
-
237
- def test_should_block_after_an_earlier_block_once_the_title_tool_is_listed(
238
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
239
- ) -> None:
240
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
241
- all_entries = [
242
- deferred_tools(REMOTE_TITLE_TOOL),
243
- prompt("Fix the runner"),
244
- earlier_stop_block(),
245
- prompt("Next task"),
246
- tool_call("t1", "Bash"),
247
- tool_result("t1"),
248
- ]
249
- _, stdout_text = run_gate(monkeypatch, capsys, tmp_path, all_entries)
250
- assert json.loads(stdout_text)["decision"] == "block"
251
-
252
-
253
- def test_should_stay_silent_on_the_retry_after_a_block(
254
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
255
- ) -> None:
256
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
257
- assert run_gate(
258
- monkeypatch, capsys, tmp_path, [prompt("Fix the runner")], stop_hook_active=True
259
- ) == (0, "")
260
-
261
-
262
- def test_should_stay_silent_when_the_transcript_is_missing(
263
- monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path
264
- ) -> None:
265
- monkeypatch.setenv(REMOTE_SESSION_VARIABLE, "cse_1")
266
- hook_input = {"hook_event_name": "Stop", "transcript_path": str(tmp_path / "absent.jsonl")}
267
- monkeypatch.setattr(
268
- "sys.stdin", io.TextIOWrapper(io.BytesIO(json.dumps(hook_input).encode("utf-8")))
269
- )
270
- assert session_title_stop_gate.main() == 0
271
- assert capsys.readouterr().out == ""
272
-
273
-
274
- def test_stop_block_reason_returns_none_after_a_successful_title_call() -> None:
275
- all_entries = [prompt("Fix the runner"), tool_call("t1", REMOTE_TITLE_TOOL), tool_result("t1")]
276
- assert session_title_stop_gate.stop_block_reason(all_entries, is_remote_session=True) is None
277
-
278
-
279
- def test_stop_block_reason_names_the_seen_tool_when_the_turn_has_no_title() -> None:
280
- all_entries = [
281
- prompt("Fix the runner"),
282
- tool_call("t1", DESKTOP_TITLE_TOOL),
283
- tool_result("t1"),
284
- prompt("Next"),
285
- ]
286
- block_reason = session_title_stop_gate.stop_block_reason(all_entries, is_remote_session=False)
287
- assert block_reason is not None and DESKTOP_TITLE_TOOL in block_reason
288
-
289
-
290
- def test_stop_block_reason_tells_the_model_to_set_the_title_in_silence() -> None:
291
- all_entries = [prompt("Fix the runner"), tool_call("t1", REMOTE_TITLE_TOOL), prompt("Next")]
292
- block_reason = session_title_stop_gate.stop_block_reason(all_entries, is_remote_session=True)
293
- assert block_reason is not None
294
- assert "Say nothing to the user about the title" in block_reason
295
- assert "end with no text" in block_reason
@@ -1,55 +0,0 @@
1
- """Committed follow-up ledger check for exact repeated lines.
2
-
3
- GitHub's server-side merge ignores ``merge=union``, so repeated lines arrive
4
- only through a local union merge. This check stops them at the merge queue.
5
- """
6
-
7
- from __future__ import annotations
8
-
9
- from collections.abc import Sequence
10
- from pathlib import Path
11
- from types import ModuleType
12
-
13
- from policy_lint.config import constants as policy_constants
14
-
15
- from repository_checks.config import constants as repository_constants
16
- from repository_checks.hook_modules import load_hooks_module
17
- from repository_checks.models import RepositoryFinding
18
-
19
-
20
- def collect_followup_ledger_duplicate_findings(
21
- repository_root: Path, all_tracked_paths: Sequence[str]
22
- ) -> list[RepositoryFinding]:
23
- """Return a finding when the tracked ledger repeats a line.
24
-
25
- Args:
26
- repository_root: Git repository root.
27
- all_tracked_paths: Repository-relative tracked paths.
28
-
29
- Returns:
30
- One finding naming the repeated-line count, or no findings.
31
- """
32
- ledger = load_hooks_module(repository_constants.FOLLOWUP_LEDGER_MODULE_NAME)
33
- ledger_path = ledger.followup_ledger_path(repository_root)
34
- relative_path = ledger_path.relative_to(repository_root).as_posix()
35
- if relative_path not in all_tracked_paths:
36
- return []
37
- repeated_line_count = _repeated_line_count(ledger, ledger_path)
38
- if not repeated_line_count:
39
- return []
40
- return [
41
- RepositoryFinding(
42
- repository_constants.CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
43
- relative_path,
44
- repository_constants.FOLLOWUP_LEDGER_DUPLICATE_MESSAGE_TEMPLATE.format(
45
- repeated_line_count=repeated_line_count
46
- ),
47
- )
48
- ]
49
-
50
-
51
- def _repeated_line_count(ledger: ModuleType, ledger_path: Path) -> int:
52
- all_ledger_lines = ledger_path.read_text(
53
- encoding=policy_constants.UTF8_ENCODING
54
- ).splitlines()
55
- return len(all_ledger_lines) - len(ledger.deduplicated_lines(all_ledger_lines))
@@ -1,58 +0,0 @@
1
- """Behavior tests for the committed follow-up ledger duplicate check."""
2
-
3
- from __future__ import annotations
4
-
5
- import sys
6
- from pathlib import Path
7
-
8
- _TESTS_DIRECTORY = Path(__file__).resolve().parent
9
- if str(_TESTS_DIRECTORY) not in sys.path:
10
- sys.path.insert(0, str(_TESTS_DIRECTORY))
11
-
12
- from repository_checks.config.constants import (
13
- CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES,
14
- FINDINGS_EXIT_CODE,
15
- SUCCESS_EXIT_CODE,
16
- )
17
- from repository_policy_test_support import (
18
- commit_tracked_files,
19
- initialize_repository,
20
- run_policy,
21
- write_text,
22
- )
23
-
24
- _LEDGER_RELATIVE_PATH = ".claude/followups/smells.jsonl"
25
-
26
-
27
- def test_should_flag_a_committed_ledger_with_a_repeated_line(tmp_path: Path) -> None:
28
- repository_root = tmp_path / "repo"
29
- initialize_repository(repository_root)
30
- write_text(
31
- repository_root / _LEDGER_RELATIVE_PATH,
32
- '{"rule_id": "a"}\n{"rule_id": "b"}\n{"rule_id": "a"}\n',
33
- )
34
- commit_tracked_files(repository_root)
35
-
36
- exit_code, stdout_text, _stderr_text = run_policy(repository_root)
37
-
38
- assert exit_code == FINDINGS_EXIT_CODE
39
- assert (
40
- f"{CHECK_ID_FOLLOWUP_LEDGER_DUPLICATES}: {_LEDGER_RELATIVE_PATH}:"
41
- in stdout_text
42
- )
43
- assert "cde followup dedupe" in stdout_text
44
-
45
-
46
- def test_should_pass_a_committed_ledger_without_repeats(tmp_path: Path) -> None:
47
- repository_root = tmp_path / "repo"
48
- initialize_repository(repository_root)
49
- write_text(
50
- repository_root / _LEDGER_RELATIVE_PATH,
51
- '{"rule_id": "a"}\n{"rule_id": "b"}\n',
52
- )
53
- commit_tracked_files(repository_root)
54
-
55
- exit_code, stdout_text, _stderr_text = run_policy(repository_root)
56
-
57
- assert exit_code == SUCCESS_EXIT_CODE
58
- assert stdout_text == ""