claude-dev-env 8.88.5 → 8.88.7

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 (24) hide show
  1. package/.agents/skills/efficient-review/tools/config/test_efficient_review_constants.py +23 -0
  2. package/.agents/skills/efficient-review/tools/test_efficient_review.py +33 -0
  3. package/.agents/skills/efficient-review/tools/test_efficient_review_plan.py +27 -0
  4. package/bin/install.settings-defaults.test.mjs +5 -1
  5. package/hooks/blocking/test_verify_before_acting.py +1 -1
  6. package/hooks/features/session-context.md +4 -0
  7. package/hooks/hooks.json +18 -0
  8. package/hooks/hooks_constants/compaction_user_messages_constants.py +93 -0
  9. package/hooks/hooks_constants/test_haiku_spawn_slim_gate_constants.py +18 -0
  10. package/hooks/session/compaction_recall_prompt.py +90 -0
  11. package/hooks/session/compaction_trim_eval.py +392 -0
  12. package/hooks/session/compaction_user_messages.py +307 -0
  13. package/hooks/session/fixtures/compaction_user_messages/deploy_session.jsonl +17 -0
  14. package/hooks/session/fixtures/compaction_user_messages/long_session.jsonl +41 -0
  15. package/hooks/session/fixtures/compaction_user_messages/one_compaction.jsonl +7 -0
  16. package/hooks/session/fixtures/compaction_user_messages/recall_request_session.jsonl +4 -0
  17. package/hooks/session/fixtures/compaction_user_messages/trim_cases.json +47 -0
  18. package/hooks/session/test_compaction_recall_prompt.py +144 -0
  19. package/hooks/session/test_compaction_trim_eval.py +115 -0
  20. package/hooks/session/test_compaction_user_messages.py +228 -0
  21. package/package.json +1 -1
  22. package/scripts/ci/scratch-home-install.mjs +3 -2
  23. package/scripts/ci/scratch-home-install.test.mjs +23 -1
  24. package/settings.json +2 -1
@@ -0,0 +1,17 @@
1
+ {"type": "user", "timestamp": "2026-10-19T10:00:01Z", "message": {"role": "user", "content": "<command-message>deepen</command-message>\n<command-name>/deepen</command-name>\n<command-args>migrate the billing service off the old cron box. the cron box is batch-07.internal and the job runs at 02:15 UTC</command-args>"}, "origin": {"kind": "human"}}
2
+ {"type": "user", "timestamp": "2026-10-19T10:00:02Z", "message": {"role": "user", "content": [{"type": "text", "text": "Base directory for this skill: ~/.claude/skills/deepen\n\n# deepen ..."}]}, "isMeta": true}
3
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:03Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "I will look at the cron config."}]}}
4
+ {"type": "user", "timestamp": "2026-10-19T10:00:04Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "15 2 * * * /opt/billing/run.sh"}]}}
5
+ {"type": "user", "timestamp": "2026-10-19T10:00:05Z", "message": {"role": "user", "content": "we store invoices in Postgres, keep using Postgres for the new service. the table is billing.invoices_v3"}, "origin": {"kind": "human"}}
6
+ {"type": "user", "timestamp": "2026-10-19T10:00:06Z", "message": {"role": "user", "content": "<system-reminder>Task tools available.</system-reminder>"}}
7
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:07Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Understood."}]}}
8
+ {"type": "user", "timestamp": "2026-10-19T10:00:08Z", "message": {"role": "user", "content": "ok"}, "origin": {"kind": "human"}}
9
+ {"type": "user", "timestamp": "2026-10-19T10:00:09Z", "message": {"role": "user", "content": "dont touch packages/billing/legacy_export.py, finance still reads its CSV every Monday"}, "origin": {"kind": "human"}}
10
+ {"type": "attachment", "timestamp": "2026-10-19T10:00:10Z", "attachment": {"type": "queued_command", "prompt": "<agent-message from=\"a1\">[Subagent hand-back] I suggest MySQL.</agent-message>"}}
11
+ {"type": "user", "timestamp": "2026-10-19T10:00:11Z", "message": {"role": "user", "content": [{"type": "text", "text": "[Request interrupted by user]"}]}, "origin": {"kind": "human"}}
12
+ {"type": "user", "timestamp": "2026-10-19T10:00:12Z", "message": {"role": "user", "content": "change of plan: switch the new service to SQLite, not Postgres. ops wont give us a db until Q3"}, "origin": {"kind": "human"}}
13
+ {"type": "user", "timestamp": "2026-10-19T10:00:13Z", "message": {"role": "user", "content": [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ""}}, {"type": "text", "text": "this is the error from the staging run, exit code 137"}]}, "origin": {"kind": "human"}}
14
+ {"type": "user", "timestamp": "2026-10-19T10:00:14Z", "message": {"role": "user", "content": "retry budget is 4 attempts with 30s backoff. the PR goes to https://github.com/acme/billing/pull/812 and needs Dana's review"}, "origin": {"kind": "human"}}
15
+ {"type": "user", "timestamp": "2026-10-19T10:00:15Z", "message": {"role": "user", "content": "thanks"}, "origin": {"kind": "human"}}
16
+ {"type": "system", "subtype": "compact_boundary", "timestamp": "2026-10-19T10:00:16Z", "content": "Conversation compacted"}
17
+ {"type": "user", "timestamp": "2026-10-19T10:00:17Z", "message": {"role": "user", "content": "This session is being continued from a previous conversation..."}, "isCompactSummary": true}
@@ -0,0 +1,41 @@
1
+ {"type": "user", "timestamp": "2026-10-19T10:00:18Z", "message": {"role": "user", "content": "we are cleaning up the photo importer in tools/photo_import/. goal: imports under 90 seconds for a 2,000 photo card"}, "origin": {"kind": "human"}}
2
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:19Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
3
+ {"type": "user", "timestamp": "2026-10-19T10:00:20Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
4
+ {"type": "user", "timestamp": "2026-10-19T10:00:21Z", "message": {"role": "user", "content": "sounds good"}, "origin": {"kind": "human"}}
5
+ {"type": "user", "timestamp": "2026-10-19T10:00:22Z", "message": {"role": "user", "content": "the slow part is the EXIF read in exif_reader.py, it opens each file twice"}, "origin": {"kind": "human"}}
6
+ {"type": "user", "timestamp": "2026-10-19T10:00:23Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
7
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:24Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
8
+ {"type": "user", "timestamp": "2026-10-19T10:00:25Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
9
+ {"type": "user", "timestamp": "2026-10-19T10:00:26Z", "message": {"role": "user", "content": "yes go ahead"}, "origin": {"kind": "human"}}
10
+ {"type": "user", "timestamp": "2026-10-19T10:00:27Z", "message": {"role": "user", "content": "do not add new dependencies, pillow is already pinned at 10.4.0"}, "origin": {"kind": "human"}}
11
+ {"type": "user", "timestamp": "2026-10-19T10:00:28Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
12
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:29Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
13
+ {"type": "user", "timestamp": "2026-10-19T10:00:30Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
14
+ {"type": "user", "timestamp": "2026-10-19T10:00:31Z", "message": {"role": "user", "content": "ok"}, "origin": {"kind": "human"}}
15
+ {"type": "user", "timestamp": "2026-10-19T10:00:32Z", "message": {"role": "user", "content": "skip RAW files with the .cr3 extension for now"}, "origin": {"kind": "human"}}
16
+ {"type": "user", "timestamp": "2026-10-19T10:00:33Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
17
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:34Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
18
+ {"type": "user", "timestamp": "2026-10-19T10:00:35Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
19
+ {"type": "user", "timestamp": "2026-10-19T10:00:36Z", "message": {"role": "user", "content": "nice"}, "origin": {"kind": "human"}}
20
+ {"type": "user", "timestamp": "2026-10-19T10:00:37Z", "message": {"role": "user", "content": "the test card lives at /mnt/cards/sample_2000"}, "origin": {"kind": "human"}}
21
+ {"type": "user", "timestamp": "2026-10-19T10:00:38Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
22
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:39Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
23
+ {"type": "user", "timestamp": "2026-10-19T10:00:40Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
24
+ {"type": "user", "timestamp": "2026-10-19T10:00:41Z", "message": {"role": "user", "content": "keep going"}, "origin": {"kind": "human"}}
25
+ {"type": "user", "timestamp": "2026-10-19T10:00:42Z", "message": {"role": "user", "content": "use 6 worker threads, the NAS chokes above that"}, "origin": {"kind": "human"}}
26
+ {"type": "user", "timestamp": "2026-10-19T10:00:43Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
27
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:44Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
28
+ {"type": "user", "timestamp": "2026-10-19T10:00:45Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
29
+ {"type": "user", "timestamp": "2026-10-19T10:00:46Z", "message": {"role": "user", "content": "sure"}, "origin": {"kind": "human"}}
30
+ {"type": "user", "timestamp": "2026-10-19T10:00:47Z", "message": {"role": "user", "content": "write the timing to logs/import_timing.csv with one row per run"}, "origin": {"kind": "human"}}
31
+ {"type": "user", "timestamp": "2026-10-19T10:00:48Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
32
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:49Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
33
+ {"type": "user", "timestamp": "2026-10-19T10:00:50Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
34
+ {"type": "user", "timestamp": "2026-10-19T10:00:51Z", "message": {"role": "user", "content": "great, continue"}, "origin": {"kind": "human"}}
35
+ {"type": "user", "timestamp": "2026-10-19T10:00:52Z", "message": {"role": "user", "content": "change that: make it 4 worker threads, 6 still froze the NAS last night"}, "origin": {"kind": "human"}}
36
+ {"type": "user", "timestamp": "2026-10-19T10:00:53Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
37
+ {"type": "assistant", "timestamp": "2026-10-19T10:00:54Z", "message": {"role": "assistant", "content": [{"type": "text", "text": "Working on it."}]}}
38
+ {"type": "user", "timestamp": "2026-10-19T10:00:55Z", "message": {"role": "user", "content": [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "ran tests: 41 passed"}]}}
39
+ {"type": "user", "timestamp": "2026-10-19T10:00:56Z", "message": {"role": "user", "content": "makes sense"}, "origin": {"kind": "human"}}
40
+ {"type": "user", "timestamp": "2026-10-19T10:00:57Z", "message": {"role": "user", "content": "name the branch photo-import-speed"}, "origin": {"kind": "human"}}
41
+ {"type": "user", "timestamp": "2026-10-19T10:00:58Z", "message": {"role": "user", "content": "helper output"}, "isSidechain": true}
@@ -0,0 +1,7 @@
1
+ {"type": "user", "message": {"role": "user", "content": "Note these facts for later: the deploy path is /srv/alpha/release-42 and the retry limit is 17. Reply only: noted."}, "isSidechain": false, "timestamp": "2026-10-19T14:18:25.345Z"}
2
+ {"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "noted."}]}, "isSidechain": false, "timestamp": "2026-10-19T14:18:25.997Z"}
3
+ {"type": "system", "subtype": "compact_boundary", "isMeta": false, "isSidechain": false, "timestamp": "2026-10-19T14:18:41.560Z", "content": "Conversation compacted"}
4
+ {"type": "user", "message": {"role": "user", "content": "This session is being continued from a previous conversation. Summary omitted from fixture."}, "isCompactSummary": true, "isSidechain": false, "timestamp": "2026-10-19T14:18:41.558Z"}
5
+ {"type": "user", "message": {"role": "user", "content": "<local-command-caveat>The command below was run directly in Claude Code, not sent to you as a request, and its output goes straight to the user. It's recorded here as context for later messages.</local-command-caveat>"}, "isMeta": true, "isSidechain": false, "timestamp": "2026-10-19T14:18:38.047Z"}
6
+ {"type": "user", "message": {"role": "user", "content": "<command-name>/compact</command-name>\n <command-message>compact</command-message>\n <command-args></command-args>"}, "isSidechain": false, "timestamp": "2026-10-19T14:18:38.047Z"}
7
+ {"type": "user", "message": {"role": "user", "content": "<local-command-stdout>Compacted (ctrl+o to see full summary)</local-command-stdout>"}, "isSidechain": false, "timestamp": "2026-10-19T14:18:41.746Z"}
@@ -0,0 +1,4 @@
1
+ {"type": "user", "message": {"role": "user", "content": "<command-message>deepen</command-message>\n<command-name>/deepen</command-name>\n<command-args>i'd like my agents to be able to have better post-compaction behavior; i feel that there is a strong potential benefit in having a post-compact hook which runs a tool that -reads- that session's chat transcript, and this tool simply returns the user's messages to that agent. example is attached.\n\ni do believe that we should probably 'lighten the load' by having some other layer - for example perhaps this is a prompt hook that is aligned with it. the prompt hook is simply a caveman style return of the user's messages; so we run our tool, it returns the input to that prompt hook (raw user 'message' input), and then the agent which receives that is instructed to trim it down without losing substance; ie: caveman style messages. and this is the final result that's returned to our agent.</command-args>"}, "isSidechain": false, "origin": {"kind": "human"}, "timestamp": "2026-10-19T14:11:07.044Z"}
2
+ {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "Base directory for this skill: ~/.claude/skills/deepen\n\n# deepen\n\nOn a first task, `/deepen` plus a task, or a routine wake that has work, read DeepStack intake and follow it: [intake](https://github.com/jl-cmd-projects/DeepStack/blob/main/plugins/deepstack/skills/deepstack-mode/references/intake.md).\n\nSkip when the prompt already carries `CDE_DEEPEN:v1`, when the text is a thanks or yes/no or status line, when it is a slash command other than `/deepen`, when it is a `/loop` or helper report, or when it is a spawn rewrite that already starts with the poteto-mode prefix.\n\nA prompt that is only `/deepen` with no task is skipped. `/deepen` plus a task deepens the remainder.\n\nThe deepening pass writes the worker prompt and stops. The worker runs that prompt.\n\nWhen you spawn a helper to fill the four answers, that helper's prompt starts with `CDE_DEEPEN_AGENT` so the first-task hook leaves it alone.\n\n## Hosts\n\n- Claude desktop and CLI: a `prompt.submit` mod rewrites the visible text. The Python `UserPromptSubmit` hook is silent when that mod is on outside a cloud session.\n- Claude cloud and Codex local: the Python hook injects `additionalContext`. Command hooks cannot replace the prompt string.\n- Cursor Cloud: run DeepStack intake on the launch prompt. The host adapter for the three lines and the refusal lives with that host. This package keeps the gate.\n- Codex cloud orchestration skips command hooks. Local Codex only.\n\n\nARGUMENTS: i'd like my agents to be able to have better post-compaction behavior; i feel that there is a strong potential benefit in having a post-compact hook which runs a tool that -reads- that session's chat transcript, and this tool simply returns the user's messages to that agent. example is attached.\n\ni do believe that we should probably 'lighten the load' by having some other layer - for example perhaps this is a prompt hook that is aligned with it. the prompt hook is simply a caveman style return of the user's messages; so we run our tool, it returns the input to that prompt hook (raw user 'message' input), and then the agent which receives that is instructed to trim it down without losing substance; ie: caveman style messages. and this is the final result that's returned to our agent."}]}, "isMeta": true, "isSidechain": false, "timestamp": "2026-10-19T14:11:07.044Z"}
3
+ {"type": "user", "message": {"role": "user", "content": [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": ""}}, {"text": "here.\n\nand i dont buy it. prompot hooks can definitely return text or values? i'm fairly certain ihave that already in my existing hooks.", "type": "text"}]}, "isSidechain": false, "origin": {"kind": "human"}, "timestamp": "2026-10-19T14:14:31.507Z"}
4
+ {"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "[Image: source: /tmp/claude-0/-workspace/d1c9e10e-7af1-51e5-8956-15fde41ab886/images/1.png]"}]}, "isMeta": true, "isSidechain": false, "timestamp": "2026-10-19T14:14:31.507Z"}
@@ -0,0 +1,47 @@
1
+ [
2
+ {
3
+ "transcript": "deploy_session.jsonl",
4
+ "facts": [
5
+ "/deepen",
6
+ "batch-07.internal",
7
+ "02:15 UTC",
8
+ "billing.invoices_v3",
9
+ "packages/billing/legacy_export.py",
10
+ "Monday",
11
+ "SQLite",
12
+ "Q3",
13
+ "137",
14
+ "4 attempts",
15
+ "30s",
16
+ "https://github.com/acme/billing/pull/812",
17
+ "Dana"
18
+ ],
19
+ "overruled": [{"earlier": 2, "later": 6}]
20
+ },
21
+ {
22
+ "transcript": "long_session.jsonl",
23
+ "facts": [
24
+ "tools/photo_import/",
25
+ "90 seconds",
26
+ "2,000",
27
+ "exif_reader.py",
28
+ "10.4.0",
29
+ ".cr3",
30
+ "/mnt/cards/sample_2000",
31
+ "logs/import_timing.csv",
32
+ "4 worker threads",
33
+ "photo-import-speed"
34
+ ],
35
+ "overruled": [{"earlier": 11, "later": 15}]
36
+ },
37
+ {
38
+ "transcript": "recall_request_session.jsonl",
39
+ "facts": [
40
+ "/deepen",
41
+ "transcript",
42
+ "caveman",
43
+ "prompt hook"
44
+ ],
45
+ "overruled": []
46
+ }
47
+ ]
@@ -0,0 +1,144 @@
1
+ """Tests for compaction_recall_prompt and the hooks.json entries that carry its recall."""
2
+
3
+ import fnmatch
4
+ import json
5
+ import shlex
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _SESSION_DIR = Path(__file__).resolve().parent
13
+ _HOOKS_ROOT = _SESSION_DIR.parent
14
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
15
+ if each_sys_path_entry not in sys.path:
16
+ sys.path.insert(0, each_sys_path_entry)
17
+
18
+ import compaction_recall_prompt as recall
19
+ from compaction_user_messages import output_path_for
20
+
21
+ HOOK_PATH = _SESSION_DIR / "compaction_recall_prompt.py"
22
+ ALL_HOOK_REGISTRATIONS = json.loads((_HOOKS_ROOT / "hooks.json").read_text(encoding="utf-8"))[
23
+ "hooks"
24
+ ]
25
+ ALL_PACKAGE_ALLOW_RULES = json.loads(
26
+ (_HOOKS_ROOT.parent / "settings.json").read_text(encoding="utf-8")
27
+ )["permissions"]["allow"]
28
+
29
+
30
+ def compact_input(transcript_path: str) -> dict[str, object]:
31
+ return {
32
+ "hook_event_name": "SessionStart",
33
+ "source": "compact",
34
+ "transcript_path": transcript_path,
35
+ }
36
+
37
+
38
+ def recall_command(transcript_path: str) -> str:
39
+ context_text = recall.recall_context(compact_input(transcript_path))
40
+ assert context_text is not None
41
+ return next(line for line in context_text.splitlines() if "compaction_user_messages.py" in line)
42
+
43
+
44
+ def test_recall_context_should_name_a_command_that_writes_the_users_messages(
45
+ tmp_path: Path,
46
+ ) -> None:
47
+ transcript_path = tmp_path / "it's a session.jsonl"
48
+ transcript_path.write_text(
49
+ json.dumps({"type": "user", "message": {"content": "keep port 8443"}}) + "\n",
50
+ encoding="utf-8",
51
+ )
52
+ completed = subprocess.run(
53
+ shlex.split(recall_command(str(transcript_path))),
54
+ capture_output=True,
55
+ text=True,
56
+ check=True,
57
+ )
58
+ assert "keep port 8443" in Path(completed.stdout.strip()).read_text(encoding="utf-8")
59
+
60
+
61
+ def test_recall_context_should_stay_quiet_outside_a_compaction() -> None:
62
+ assert recall.recall_context({**compact_input("/t.jsonl"), "source": "startup"}) is None
63
+ assert recall.recall_context({**compact_input("/t.jsonl"), "hook_event_name": "Stop"}) is None
64
+ assert recall.recall_context({"hook_event_name": "SessionStart", "source": "compact"}) is None
65
+
66
+
67
+ def test_main_should_print_the_context_as_session_start_output() -> None:
68
+ completed = subprocess.run(
69
+ [sys.executable, str(HOOK_PATH)],
70
+ input=json.dumps(compact_input("/t/abc.jsonl")),
71
+ capture_output=True,
72
+ text=True,
73
+ check=True,
74
+ )
75
+ hook_output = json.loads(completed.stdout)["hookSpecificOutput"]
76
+ assert hook_output["hookEventName"] == "SessionStart"
77
+ assert "compaction_user_messages.py" in hook_output["additionalContext"]
78
+
79
+
80
+ def test_hooks_json_should_run_the_recall_prompt_after_every_compaction() -> None:
81
+ all_compact_commands = [
82
+ each_hook.get("command", "")
83
+ for each_group in ALL_HOOK_REGISTRATIONS["SessionStart"]
84
+ if each_group.get("matcher") == "compact"
85
+ for each_hook in each_group["hooks"]
86
+ ]
87
+ assert any(
88
+ each_command.endswith("/hooks/session/compaction_recall_prompt.py")
89
+ for each_command in all_compact_commands
90
+ )
91
+
92
+
93
+ def condensing_hooks() -> list[dict[str, object]]:
94
+ return [
95
+ each_hook
96
+ for each_group in ALL_HOOK_REGISTRATIONS["PostToolUse"]
97
+ if each_group.get("matcher") == "Bash"
98
+ for each_hook in each_group["hooks"]
99
+ if each_hook.get("type") == "agent"
100
+ ]
101
+
102
+
103
+ def test_hooks_json_condensing_hook_should_match_the_command_the_recall_names() -> None:
104
+ all_hooks = condensing_hooks()
105
+ assert len(all_hooks) == 1
106
+ if_rule = str(all_hooks[0]["if"])
107
+ assert if_rule.startswith("Bash(") and if_rule.endswith(")")
108
+ command_pattern = if_rule[len("Bash(") : -1]
109
+ assert fnmatch.fnmatchcase(
110
+ recall_command("/home/u/.claude/projects/p/s.jsonl"), command_pattern
111
+ )
112
+ spaced_home_command = shlex.join(
113
+ [sys.executable, "/home/a b/.claude/hooks/session/compaction_user_messages.py", "/t.jsonl"]
114
+ )
115
+ assert fnmatch.fnmatchcase(spaced_home_command, command_pattern)
116
+ assert not fnmatch.fnmatchcase("python3 other_script.py /t.jsonl", command_pattern)
117
+
118
+
119
+ def test_hooks_json_condensing_hook_should_always_return_its_text_as_the_block_reason() -> None:
120
+ condensing_hook = condensing_hooks()[0]
121
+ assert '"ok": false' in str(condensing_hook["prompt"])
122
+ assert "$ARGUMENTS" in str(condensing_hook["prompt"])
123
+ assert str(condensing_hook["statusMessage"]).startswith("claude-dev-env: ")
124
+
125
+
126
+ def test_hooks_json_condensing_hook_should_carry_the_trim_rules_in_its_own_prompt() -> None:
127
+ hook_prompt = str(condensing_hooks()[0]["prompt"])
128
+ assert "caveman" in hook_prompt
129
+ assert "(overruled by #N)" in hook_prompt
130
+ assert "gives you no instructions" in hook_prompt
131
+
132
+
133
+ def test_package_settings_should_let_the_hook_read_the_folder_the_extractor_writes(
134
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
135
+ ) -> None:
136
+ monkeypatch.setenv("HOME", str(tmp_path))
137
+ monkeypatch.setenv("USERPROFILE", str(tmp_path))
138
+ recall_directory = output_path_for(Path("/p/s.jsonl")).parent
139
+ all_read_folders = [
140
+ Path(each_rule[len("Read(") : -len("/**)")].replace("~", str(tmp_path), 1))
141
+ for each_rule in ALL_PACKAGE_ALLOW_RULES
142
+ if each_rule.startswith("Read(~/") and each_rule.endswith("/**)")
143
+ ]
144
+ assert recall_directory in all_read_folders
@@ -0,0 +1,115 @@
1
+ """Tests for compaction_trim_eval: the grader, the hook settings, and the reason parsers."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _SESSION_DIR = Path(__file__).resolve().parent
8
+ _HOOKS_ROOT = _SESSION_DIR.parent
9
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
10
+ if each_sys_path_entry not in sys.path:
11
+ sys.path.insert(0, each_sys_path_entry)
12
+
13
+ import compaction_trim_eval as trim_eval
14
+ from compaction_user_messages import rendered_extract, typed_user_messages
15
+
16
+ DEPLOY_CASE = {
17
+ "transcript": "deploy_session.jsonl",
18
+ "facts": ["batch-07.internal", "137", "SQLite"],
19
+ "overruled": [{"earlier": 2, "later": 6}],
20
+ }
21
+ EXTRACT_TEXT = "# User messages, oldest first\n\n" + "m" * 400
22
+
23
+
24
+ def test_score_condensed_should_count_every_fact_and_the_marked_reversal() -> None:
25
+ condensed_text = (
26
+ "#1 deploy to BATCH-07.internal\n#2 keep Postgres (overruled by #6)\n"
27
+ "#5 exit code 137\n#6 switch to SQLite\n"
28
+ )
29
+ case_score = trim_eval.score_condensed(condensed_text, DEPLOY_CASE, EXTRACT_TEXT)
30
+ assert (case_score.facts_kept, case_score.facts_total) == (3, 3)
31
+ assert (case_score.overruled_marked, case_score.overruled_total) == (1, 1)
32
+ assert case_score.compression_ratio == len("m" * 400) / len(condensed_text)
33
+
34
+
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
+ assert case_score.facts_kept == 0
39
+ 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
@@ -0,0 +1,228 @@
1
+ """Tests for compaction_user_messages: which transcript entries count as typed, and the file it writes."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SESSION_DIR = Path(__file__).resolve().parent
12
+ _HOOKS_ROOT = _SESSION_DIR.parent
13
+ for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
14
+ if each_sys_path_entry not in sys.path:
15
+ sys.path.insert(0, each_sys_path_entry)
16
+
17
+ 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
+
24
+ EXTRACTOR_PATH = _SESSION_DIR / "compaction_user_messages.py"
25
+ FIXTURE_DIRECTORY = _SESSION_DIR / "fixtures" / "compaction_user_messages"
26
+
27
+
28
+ def user_line(content: object, **all_extra_fields: object) -> str:
29
+ return json.dumps(
30
+ {
31
+ "type": "user",
32
+ "timestamp": "2026-10-19T10:00:00Z",
33
+ "message": {"role": "user", "content": content},
34
+ **all_extra_fields,
35
+ }
36
+ )
37
+
38
+
39
+ def texts_of(all_lines: list[str]) -> list[str]:
40
+ return [each_message.text for each_message in extractor.typed_user_messages(all_lines)]
41
+
42
+
43
+ def test_should_keep_plain_typed_text_with_position_and_timestamp() -> None:
44
+ all_messages = extractor.typed_user_messages(
45
+ [user_line("fix the login bug"), user_line(" use /srv/app ")]
46
+ )
47
+ assert [
48
+ (each_message.position, each_message.timestamp, each_message.text)
49
+ for each_message in all_messages
50
+ ] == [
51
+ (1, "2026-10-19T10:00:00Z", "fix the login bug"),
52
+ (2, "2026-10-19T10:00:00Z", "use /srv/app"),
53
+ ]
54
+
55
+
56
+ def test_should_show_a_slash_command_as_name_and_typed_arguments() -> None:
57
+ command_text = (
58
+ "<command-message>deepen</command-message>\n<command-name>/deepen</command-name>\n"
59
+ "<command-args>make it faster</command-args>"
60
+ )
61
+ assert texts_of([user_line(command_text)]) == ["/deepen make it faster"]
62
+
63
+
64
+ def test_should_leave_out_harness_and_agent_entries() -> None:
65
+ all_lines = [
66
+ user_line("skill body", isMeta=True),
67
+ user_line("summary", isCompactSummary=True),
68
+ user_line("helper prompt", isSidechain=True),
69
+ user_line("report", origin={"kind": "peer"}),
70
+ user_line([{"type": "tool_result", "tool_use_id": "t1", "content": "ok"}]),
71
+ user_line("<system-reminder>rules</system-reminder>"),
72
+ user_line("<local-command-stdout>Compacted</local-command-stdout>"),
73
+ json.dumps({"type": "assistant", "message": {"content": [{"type": "text", "text": "hi"}]}}),
74
+ "not json",
75
+ user_line("typed", origin={"kind": "human"}),
76
+ ]
77
+ assert texts_of(all_lines) == ["typed"]
78
+
79
+
80
+ def test_should_keep_typed_text_that_opens_with_markup() -> None:
81
+ all_lines = [
82
+ user_line("<Button> does not render"),
83
+ user_line("<task-notification>done</task-notification>"),
84
+ ]
85
+ assert texts_of(all_lines) == ["<Button> does not render"]
86
+
87
+
88
+ def test_should_keep_an_interrupt_and_mark_an_image() -> None:
89
+ all_lines = [
90
+ user_line([{"type": "text", "text": "[Request interrupted by user]"}]),
91
+ user_line([{"type": "image", "source": {}}, {"type": "text", "text": "here."}]),
92
+ ]
93
+ assert texts_of(all_lines) == ["[Request interrupted by user]", "[image] here."]
94
+
95
+
96
+ def test_should_count_a_queued_prompt_once_when_it_also_lands_as_a_user_entry() -> None:
97
+ queued_line = json.dumps(
98
+ {"type": "attachment", "attachment": {"type": "queued_command", "prompt": "check logs"}}
99
+ )
100
+ relay_line = json.dumps(
101
+ {
102
+ "type": "attachment",
103
+ "attachment": {"type": "queued_command", "prompt": "<agent-message>x"},
104
+ }
105
+ )
106
+ assert texts_of([queued_line, user_line("check logs"), relay_line]) == ["check logs"]
107
+
108
+
109
+ def test_should_keep_a_message_typed_again_after_the_assistant_replied() -> None:
110
+ assistant_line = json.dumps({"type": "assistant", "message": {"content": "done"}})
111
+ assert texts_of([user_line("yes"), assistant_line, user_line("yes")]) == ["yes", "yes"]
112
+
113
+
114
+ def test_should_read_a_claude_code_transcript_across_a_compaction() -> None:
115
+ fixture_lines = (FIXTURE_DIRECTORY / "one_compaction.jsonl").read_text().splitlines()
116
+ assert texts_of(fixture_lines) == [
117
+ "Note these facts for later: the deploy path is /srv/alpha/release-42 and the "
118
+ "retry limit is 17. Reply only: noted.",
119
+ "/compact",
120
+ ]
121
+
122
+
123
+ def test_should_converge_when_a_second_compaction_appends_to_the_transcript() -> None:
124
+ first_lines = (FIXTURE_DIRECTORY / "one_compaction.jsonl").read_text().splitlines()
125
+ second_lines = first_lines + [
126
+ user_line("now set the retry limit to 9"),
127
+ json.dumps({"type": "system", "subtype": "compact_boundary"}),
128
+ user_line("summary two", isCompactSummary=True),
129
+ user_line("<command-name>/compact</command-name>"),
130
+ ]
131
+ first_messages = extractor.typed_user_messages(first_lines)
132
+ second_messages = extractor.typed_user_messages(second_lines)
133
+ assert second_messages[: len(first_messages)] == first_messages
134
+ assert [each_message.text for each_message in second_messages[len(first_messages) :]] == [
135
+ "now set the retry limit to 9",
136
+ "/compact",
137
+ ]
138
+
139
+
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
186
+ )
187
+
188
+
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"
200
+
201
+
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 == ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.88.5",
3
+ "version": "8.88.7",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {