claude-dev-env 8.90.0 → 8.91.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.
@@ -0,0 +1,169 @@
1
+ """Tests for compaction_trim: the caveman trim, the overruled marks, and the recall budget."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _SESSION_DIR = Path(__file__).resolve().parent
7
+ _HOOKS_ROOT = _SESSION_DIR.parent
8
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
9
+ if each_sys_path_entry not in sys.path:
10
+ sys.path.insert(0, each_sys_path_entry)
11
+
12
+ import compaction_trim as trim
13
+ from compaction_user_messages import TypedMessage
14
+ from hooks_constants.compaction_user_messages_constants import (
15
+ MAXIMUM_MESSAGE_CHARACTERS,
16
+ MAXIMUM_RECALL_CHARACTERS,
17
+ )
18
+
19
+
20
+ def message(position: int, text: str) -> TypedMessage:
21
+ return TypedMessage(position=position, timestamp="t", text=text)
22
+
23
+
24
+ def test_caveman_text_should_drop_filler_and_articles() -> None:
25
+ assert (
26
+ trim.caveman_text("Could you please just add the retry to a deploy script , basically.")
27
+ == "just add retry to deploy script."
28
+ )
29
+
30
+
31
+ def test_caveman_text_should_keep_a_restrictive_just() -> None:
32
+ assert trim.caveman_text("use just the staging db") == "use just staging db"
33
+
34
+
35
+ def test_caveman_text_should_keep_backtick_spans_and_path_tokens_as_typed() -> None:
36
+ assert (
37
+ trim.caveman_text("run `the just a tool` on just/the.py and the-a.md")
38
+ == "run `the just a tool` on just/the.py and the-a.md"
39
+ )
40
+
41
+
42
+ def test_caveman_text_should_keep_a_hedge() -> None:
43
+ assert trim.caveman_text("I think the cache is stale") == "I think cache is stale"
44
+
45
+
46
+ def test_caveman_text_should_keep_a_capital_letter_label() -> None:
47
+ assert trim.caveman_text("The plan A is good, an Option A or B") == "plan A is good, Option A or B"
48
+
49
+
50
+ def test_trimmed_lines_should_not_mark_a_line_a_reminder_repeats() -> None:
51
+ all_messages = [
52
+ message(1, "Set up the linter in CI"),
53
+ message(2, "Don't forget the linter config too"),
54
+ ]
55
+ assert trim.trimmed_lines(all_messages) == [
56
+ "#1 Set up linter in CI",
57
+ "#2 Don't forget linter config too",
58
+ ]
59
+
60
+
61
+ def test_trimmed_lines_should_not_read_a_cue_inside_a_longer_word() -> None:
62
+ all_messages = [
63
+ message(1, "Add docs for the API"),
64
+ message(2, "Document the undocumented parts of the API"),
65
+ ]
66
+ assert trim.trimmed_lines(all_messages) == [
67
+ "#1 Add docs for API",
68
+ "#2 Document undocumented parts of API",
69
+ ]
70
+
71
+
72
+ def test_trimmed_lines_should_drop_gratitude_and_interrupts_and_keep_approvals() -> None:
73
+ all_messages = [
74
+ message(1, "ship it to staging"),
75
+ message(2, "thanks"),
76
+ message(3, "[Request interrupted by user]"),
77
+ message(4, "yes, go ahead"),
78
+ message(5, "ok but use port 8443"),
79
+ ]
80
+ assert trim.trimmed_lines(all_messages) == [
81
+ "#1 ship it to staging",
82
+ "#4 yes, go ahead",
83
+ "#5 ok but use port 8443",
84
+ ]
85
+
86
+
87
+ def test_trimmed_lines_should_mark_the_earlier_message_a_reversal_overrules() -> None:
88
+ all_messages = [
89
+ message(1, "Store the uploads in S3."),
90
+ message(2, "Log every retry to logs/retry.csv"),
91
+ message(3, "Change of plan, keep the uploads on local disk, not S3."),
92
+ ]
93
+ assert trim.trimmed_lines(all_messages) == [
94
+ "#1 Store uploads in S3. (overruled by #3)",
95
+ "#2 Log every retry to logs/retry.csv",
96
+ "#3 Change of plan, keep uploads on local disk, not S3.",
97
+ ]
98
+
99
+
100
+ ARTIFACT_COMMENT_TEXT = """[Artifact comment sent to Claude]
101
+ Artifact: https://claude.ai/code/artifact/e5f0
102
+ Comment thread: 02ec
103
+ I sent you this comment from the artifact's comment thread. Handle it in this session.
104
+ === BEGIN ARTIFACT COMMENT df7f ===
105
+ > Can you show me this?
106
+ > How is the agent spawned?
107
+ === END ARTIFACT COMMENT df7f ===
108
+ <comment-anchor-context source="artifact content">
109
+ Location: What happens when a session compacts
110
+ Element: rect
111
+ </comment-anchor-context>
112
+ Answer this comment in its thread, not here."""
113
+
114
+
115
+ def test_trimmed_lines_should_keep_only_the_link_anchor_and_quote_of_an_artifact_comment() -> None:
116
+ assert trim.trimmed_lines([message(1, ARTIFACT_COMMENT_TEXT)]) == [
117
+ "#1 Artifact comment on https://claude.ai/code/artifact/e5f0 at What happens when "
118
+ "session compacts: show me this? How is agent spawned?"
119
+ ]
120
+
121
+
122
+ def test_trimmed_lines_should_drop_html_comments_and_a_message_left_empty() -> None:
123
+ assert trim.trimmed_lines(
124
+ [message(1, "<!-- reply 1 --> use port 8443"), message(2, "<!-- draft -->")]
125
+ ) == ["#1 use port 8443"]
126
+
127
+
128
+ def test_trimmed_lines_should_cut_a_long_message_and_say_how_much() -> None:
129
+ long_text = "y" * (MAXIMUM_MESSAGE_CHARACTERS + 25)
130
+ assert trim.trimmed_lines([message(1, long_text)]) == [
131
+ "#1 " + "y" * MAXIMUM_MESSAGE_CHARACTERS + " [25 more characters left out]"
132
+ ]
133
+
134
+
135
+ def test_recall_text_should_keep_the_first_and_the_newest_lines_within_budget() -> None:
136
+ message_count = 3 * MAXIMUM_RECALL_CHARACTERS // MAXIMUM_MESSAGE_CHARACTERS
137
+ all_messages = [
138
+ message(each, f"m{each} " + "x" * (MAXIMUM_MESSAGE_CHARACTERS - 100))
139
+ for each in range(1, message_count + 1)
140
+ ]
141
+ all_recall_lines = (trim.recall_text(all_messages) or "").splitlines()
142
+ assert all_recall_lines[0].startswith("#1 m1 ")
143
+ assert all_recall_lines[-1].startswith(f"#{message_count} m{message_count} ")
144
+ kept_line_count = len(all_recall_lines) - 2
145
+ left_out_count = message_count - 1 - kept_line_count
146
+ assert left_out_count > 0
147
+ assert all_recall_lines[1] == f"[{left_out_count} earlier messages left out to fit the budget]"
148
+ recall_without_note = "\n".join([all_recall_lines[0], *all_recall_lines[2:]])
149
+ assert len(recall_without_note) <= MAXIMUM_RECALL_CHARACTERS
150
+
151
+
152
+ def test_recall_text_should_return_none_when_nothing_carries_content() -> None:
153
+ assert trim.recall_text([]) is None
154
+ assert trim.recall_text([message(1, "thank you"), message(2, "thanks")]) is None
155
+
156
+
157
+ def test_recall_text_should_keep_approval_and_continuation_messages() -> None:
158
+ for approval in (
159
+ "yes, go ahead",
160
+ "yes",
161
+ "ok",
162
+ "sure",
163
+ "sounds good",
164
+ "continue",
165
+ "proceed",
166
+ "you",
167
+ "thanks 8443",
168
+ ):
169
+ assert trim.recall_text([message(1, approval)]) == f"#1 {approval}"
@@ -1,9 +1,11 @@
1
- """Tests for compaction_trim_eval: the grader, the hook settings, and the reason parsers."""
1
+ """Tests for compaction_trim_eval: the grader and the fixture cases it scores."""
2
2
 
3
3
  import json
4
4
  import sys
5
5
  from pathlib import Path
6
6
 
7
+ import pytest
8
+
7
9
  _SESSION_DIR = Path(__file__).resolve().parent
8
10
  _HOOKS_ROOT = _SESSION_DIR.parent
9
11
  for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
@@ -11,105 +13,50 @@ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
11
13
  sys.path.insert(0, each_sys_path_entry)
12
14
 
13
15
  import compaction_trim_eval as trim_eval
14
- from compaction_user_messages import rendered_extract, typed_user_messages
15
16
 
17
+ FIXTURE_DIRECTORY = _SESSION_DIR / "fixtures" / "compaction_user_messages"
16
18
  DEPLOY_CASE = {
17
19
  "transcript": "deploy_session.jsonl",
18
20
  "facts": ["batch-07.internal", "137", "SQLite"],
19
21
  "overruled": [{"earlier": 2, "later": 6}],
20
22
  }
21
- EXTRACT_TEXT = "# User messages, oldest first\n\n" + "m" * 400
23
+ RAW_MESSAGES = "m" * 400
22
24
 
23
25
 
24
26
  def test_score_condensed_should_count_every_fact_and_the_marked_reversal() -> None:
25
- condensed_text = (
27
+ recall = (
26
28
  "#1 deploy to BATCH-07.internal\n#2 keep Postgres (overruled by #6)\n"
27
29
  "#5 exit code 137\n#6 switch to SQLite\n"
28
30
  )
29
- case_score = trim_eval.score_condensed(condensed_text, DEPLOY_CASE, EXTRACT_TEXT)
31
+ case_score = trim_eval.score_condensed(recall, DEPLOY_CASE, RAW_MESSAGES)
30
32
  assert (case_score.facts_kept, case_score.facts_total) == (3, 3)
31
33
  assert (case_score.overruled_marked, case_score.overruled_total) == (1, 1)
32
- assert case_score.compression_ratio == len("m" * 400) / len(condensed_text)
34
+ assert case_score.compression_ratio == len(RAW_MESSAGES) / len(recall)
35
+ assert case_score.is_lossless
33
36
 
34
37
 
35
- def test_score_condensed_should_score_a_copy_that_drops_facts_lower() -> None:
36
- lossy_text = "#1 deploy\n#2 keep Postgres\n#6 switch db\n"
37
- case_score = trim_eval.score_condensed(lossy_text, DEPLOY_CASE, EXTRACT_TEXT)
38
+ def test_score_condensed_should_score_a_recall_that_drops_facts_lower() -> None:
39
+ lossy_recall = "#1 deploy\n#2 keep Postgres\n#6 switch db\n"
40
+ case_score = trim_eval.score_condensed(lossy_recall, DEPLOY_CASE, RAW_MESSAGES)
38
41
  assert case_score.facts_kept == 0
39
42
  assert case_score.overruled_marked == 0
40
-
41
-
42
- def test_score_condensed_should_give_the_raw_extract_every_fact_and_an_empty_copy_none() -> None:
43
- fixture_directory = _SESSION_DIR / "fixtures" / "compaction_user_messages"
44
- all_cases = json.loads((fixture_directory / "trim_cases.json").read_text(encoding="utf-8"))
45
- for each_case in all_cases:
46
- transcript_lines = (fixture_directory / each_case["transcript"]).read_text().splitlines()
47
- extract_text = rendered_extract(typed_user_messages(transcript_lines))
48
- oracle_score = trim_eval.score_condensed(extract_text, each_case, extract_text)
49
- null_score = trim_eval.score_condensed("", each_case, extract_text)
50
- assert oracle_score.facts_kept == oracle_score.facts_total
51
- assert null_score.facts_kept == 0
52
-
53
-
54
- CONDENSING_HOOK = {
55
- "type": "agent",
56
- "if": "Bash(*compaction_user_messages.py*)",
57
- "prompt": "condense $ARGUMENTS",
58
- "model": "haiku",
59
- }
60
-
61
-
62
- def test_condensing_hook_group_should_keep_only_the_agent_hook_that_names_the_extractor() -> None:
63
- hooks_json_text = json.dumps(
64
- {
65
- "hooks": {
66
- "PostToolUse": [
67
- {"matcher": "Bash", "hooks": [{"type": "command", "command": "x"}]},
68
- {
69
- "matcher": "Bash",
70
- "hooks": [{"type": "command", "command": "y"}, CONDENSING_HOOK],
71
- },
72
- ]
73
- }
74
- }
75
- )
76
- assert trim_eval.condensing_hook_group(hooks_json_text) == {
77
- "matcher": "Bash",
78
- "hooks": [CONDENSING_HOOK],
79
- }
80
-
81
-
82
- def test_run_settings_should_register_the_hook_on_the_chosen_model_with_the_allow_rules() -> None:
83
- hook_group = {"matcher": "Bash", "hooks": [CONDENSING_HOOK]}
84
- all_allow_rules = ["Read(~/.claude/compaction-recall/**)"]
85
- assert trim_eval.run_settings(hook_group, all_allow_rules, "sonnet") == {
86
- "permissions": {"allow": all_allow_rules},
87
- "hooks": {
88
- "PostToolUse": [{"matcher": "Bash", "hooks": [{**CONDENSING_HOOK, "model": "sonnet"}]}]
89
- },
90
- }
91
-
92
-
93
- def test_debug_log_reason_should_return_the_last_structured_output_reason() -> None:
94
- debug_text = (
95
- "2026 [DEBUG] Hooks: Processing agent hook with prompt: condense\n"
96
- '2026 [DEBUG] Hooks: Got structured output: {"ok":false,"reason":"draft"}\n'
97
- '2026 [DEBUG] Hooks: Got structured output: {"ok":false,"reason":"#1 deploy\\n#2 keep"}\n'
98
- )
99
- assert trim_eval.debug_log_reason(debug_text) == "#1 deploy\n#2 keep"
100
-
101
-
102
- def test_debug_log_reason_should_return_none_when_no_hook_answered() -> None:
103
- assert trim_eval.debug_log_reason("2026 [DEBUG] Read tool permission denied\n") is None
104
-
105
-
106
- def test_hook_reason_should_return_the_last_decision_with_a_reason() -> None:
107
- model_reply = (
108
- 'Draft {"ok": false, "reason": "first"} then {"note": 1} and '
109
- '{"ok": false, "reason": "#1 use {braces} here"}'
110
- )
111
- assert trim_eval.hook_reason(model_reply) == "#1 use {braces} here"
112
-
113
-
114
- def test_hook_reason_should_return_none_when_the_reply_has_no_decision() -> None:
115
- assert trim_eval.hook_reason("I could not read the file {broken") is None
43
+ assert not case_score.is_lossless
44
+
45
+
46
+ def test_scored_cases_should_keep_every_fact_and_reversal_in_each_fixture() -> None:
47
+ all_cases = json.loads((FIXTURE_DIRECTORY / "trim_cases.json").read_text(encoding="utf-8"))
48
+ all_rows = trim_eval.scored_cases(FIXTURE_DIRECTORY)
49
+ assert [each_row[0] for each_row in all_rows] == [
50
+ each_case["transcript"] for each_case in all_cases
51
+ ]
52
+ for each_name, trim_score, oracle_score, null_score in all_rows:
53
+ assert trim_score.is_lossless, each_name
54
+ assert oracle_score.facts_kept == oracle_score.facts_total, each_name
55
+ assert null_score.facts_kept == 0, each_name
56
+
57
+
58
+ def test_main_should_exit_zero_when_the_trim_keeps_every_fact(
59
+ capsys: pytest.CaptureFixture[str],
60
+ ) -> None:
61
+ assert trim_eval.main() == 0
62
+ assert "trim: facts 1.00 overruled 1.00" in capsys.readouterr().out
@@ -1,13 +1,9 @@
1
- """Tests for compaction_user_messages: which transcript entries count as typed, and the file it writes."""
1
+ """Tests for compaction_user_messages: which Claude Code and Codex transcript entries count as typed."""
2
2
 
3
3
  import json
4
- import os
5
- import subprocess
6
4
  import sys
7
5
  from pathlib import Path
8
6
 
9
- import pytest
10
-
11
7
  _SESSION_DIR = Path(__file__).resolve().parent
12
8
  _HOOKS_ROOT = _SESSION_DIR.parent
13
9
  for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
@@ -15,13 +11,7 @@ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
15
11
  sys.path.insert(0, each_sys_path_entry)
16
12
 
17
13
  import compaction_user_messages as extractor
18
- from hooks_constants.compaction_user_messages_constants import (
19
- MAXIMUM_EXTRACT_CHARACTERS,
20
- MAXIMUM_MESSAGE_CHARACTERS,
21
- MESSAGES_HEADING,
22
- )
23
14
 
24
- EXTRACTOR_PATH = _SESSION_DIR / "compaction_user_messages.py"
25
15
  FIXTURE_DIRECTORY = _SESSION_DIR / "fixtures" / "compaction_user_messages"
26
16
 
27
17
 
@@ -137,92 +127,44 @@ def test_should_converge_when_a_second_compaction_appends_to_the_transcript() ->
137
127
  ]
138
128
 
139
129
 
140
- def message(position: int, text: str) -> extractor.TypedMessage:
141
- return extractor.TypedMessage(position=position, timestamp="t", text=text)
142
-
143
-
144
- def test_bounded_messages_should_keep_everything_that_fits() -> None:
145
- all_messages = [message(1, "a"), message(2, "b"), message(3, "c")]
146
- assert extractor.bounded_messages(all_messages) == all_messages
147
-
148
-
149
- def test_bounded_messages_should_keep_the_first_and_the_newest_when_over_budget() -> None:
150
- long_text = "x" * (MAXIMUM_MESSAGE_CHARACTERS - 100)
151
- message_count = 2 * MAXIMUM_EXTRACT_CHARACTERS // MAXIMUM_MESSAGE_CHARACTERS
152
- all_messages = [message(each, long_text) for each in range(1, message_count + 1)]
153
- all_kept = extractor.bounded_messages(all_messages)
154
- kept_positions = [each.position for each in all_kept]
155
- assert kept_positions[0] == 1
156
- assert kept_positions[-1] == message_count
157
- assert kept_positions[1:] == list(range(kept_positions[1], message_count + 1))
158
- assert len(extractor.rendered_extract(all_messages)) <= MAXIMUM_EXTRACT_CHARACTERS + 200
159
-
160
-
161
- def test_bounded_messages_should_return_nothing_for_no_messages() -> None:
162
- assert extractor.bounded_messages([]) == []
163
-
164
-
165
- def test_rendered_extract_should_hold_only_the_numbered_messages() -> None:
166
- rendered = extractor.rendered_extract([message(1, "fix login"), message(2, "use /srv")])
167
- assert rendered.startswith(MESSAGES_HEADING)
168
- assert "caveman" not in rendered
169
- assert "## 1 | t\nfix login\n" in rendered
170
- assert "## 2 | t\nuse /srv\n" in rendered
171
-
172
-
173
- def test_rendered_extract_should_cut_a_long_message_and_say_how_much() -> None:
174
- rendered = extractor.rendered_extract([message(1, "y" * (MAXIMUM_MESSAGE_CHARACTERS + 25))])
175
- assert "[25 more characters left out]" in rendered
176
-
177
-
178
- def test_rendered_extract_should_name_how_many_messages_were_left_out() -> None:
179
- long_text = "x" * (MAXIMUM_MESSAGE_CHARACTERS - 100)
180
- message_count = 2 * MAXIMUM_EXTRACT_CHARACTERS // MAXIMUM_MESSAGE_CHARACTERS
181
- all_messages = [message(each, long_text) for each in range(1, message_count + 1)]
182
- left_out_count = message_count - len(extractor.bounded_messages(all_messages))
183
- assert left_out_count > 0
184
- assert f"[{left_out_count} earlier messages left out" in extractor.rendered_extract(
185
- all_messages
130
+ def codex_line(payload: dict[str, object]) -> str:
131
+ return json.dumps(
132
+ {"timestamp": "2026-10-19T11:00:00Z", "type": "event_msg", "payload": payload}
186
133
  )
187
134
 
188
135
 
189
- def test_rendered_extract_should_say_when_there_are_no_messages() -> None:
190
- assert "No typed user messages" in extractor.rendered_extract([])
191
-
192
-
193
- def test_output_path_for_should_name_the_file_after_the_session_under_the_claude_home(
194
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
195
- ) -> None:
196
- monkeypatch.setenv("HOME", str(tmp_path))
197
- monkeypatch.setenv("USERPROFILE", str(tmp_path))
198
- output_path = extractor.output_path_for(Path("/p/abc-123.jsonl"))
199
- assert output_path == tmp_path / ".claude" / "compaction-recall" / "abc-123.md"
136
+ def test_should_keep_codex_user_messages_and_leave_out_injected_items() -> None:
137
+ all_lines = [
138
+ json.dumps({"type": "session_meta", "payload": {"cwd": "/w"}}),
139
+ json.dumps(
140
+ {
141
+ "type": "response_item",
142
+ "payload": {
143
+ "type": "message",
144
+ "role": "user",
145
+ "content": [{"type": "input_text", "text": "<environment_context>"}],
146
+ },
147
+ }
148
+ ),
149
+ codex_line({"type": "user_message", "message": "port the hook to codex", "kind": "plain"}),
150
+ codex_line({"type": "agent_message", "message": "on it"}),
151
+ codex_line({"type": "user_message", "message": "see this", "images": ["data:image/png"]}),
152
+ codex_line({"type": "user_message", "message": "", "images": ["data:image/png"]}),
153
+ ]
154
+ all_messages = extractor.typed_user_messages(all_lines)
155
+ assert [each_message.text for each_message in all_messages] == [
156
+ "port the hook to codex",
157
+ "see this [image]",
158
+ "[image]",
159
+ ]
160
+ assert all_messages[0].timestamp == "2026-10-19T11:00:00Z"
200
161
 
201
162
 
202
- def test_main_should_print_only_the_path_of_the_file_it_writes(tmp_path: Path) -> None:
203
- transcript_path = tmp_path / "session-xyz.jsonl"
204
- transcript_path.write_text(user_line("ship the deploy fix") + "\n", encoding="utf-8")
205
- home_directory = tmp_path / "home"
206
- completed = subprocess.run(
207
- [sys.executable, str(EXTRACTOR_PATH), str(transcript_path)],
208
- capture_output=True,
209
- text=True,
210
- check=True,
211
- env={**os.environ, "HOME": str(home_directory), "USERPROFILE": str(home_directory)},
212
- )
213
- printed_path = Path(completed.stdout.strip())
214
- assert printed_path == home_directory / ".claude" / "compaction-recall" / "session-xyz.md"
215
- assert "ship the deploy fix" not in completed.stdout
216
- assert "## 1 | 2026-10-19T10:00:00Z\nship the deploy fix" in printed_path.read_text()
217
-
218
-
219
- def test_main_should_exit_with_usage_when_the_transcript_is_missing(tmp_path: Path) -> None:
220
- for all_arguments in ([], [str(tmp_path / "absent.jsonl")]):
221
- completed = subprocess.run(
222
- [sys.executable, str(EXTRACTOR_PATH), *all_arguments],
223
- capture_output=True,
224
- text=True,
225
- check=False,
226
- )
227
- assert completed.returncode == 2
228
- assert completed.stdout == ""
163
+ def test_should_keep_a_codex_message_typed_again_after_the_agent_replied() -> None:
164
+ all_lines = [
165
+ codex_line({"type": "user_message", "message": "go"}),
166
+ codex_line({"type": "user_message", "message": "go"}),
167
+ codex_line({"type": "agent_message", "message": "done"}),
168
+ codex_line({"type": "user_message", "message": "go"}),
169
+ ]
170
+ assert texts_of(all_lines) == ["go", "go"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.90.0",
3
+ "version": "8.91.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/settings.json CHANGED
@@ -3,8 +3,7 @@
3
3
  "permissions": {
4
4
  "allow": [
5
5
  "WebFetch(domain:docs.github.com)",
6
- "Read(~/.claude/docs/**)",
7
- "Read(~/.claude/compaction-recall/**)"
6
+ "Read(~/.claude/docs/**)"
8
7
  ]
9
8
  },
10
9
  "advisorModel": "fable",