claude-dev-env 8.88.5 → 8.88.6

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,23 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ SKILL_ROOT = Path(__file__).resolve().parents[2]
5
+ if str(SKILL_ROOT) not in sys.path:
6
+ sys.path.insert(0, str(SKILL_ROOT))
7
+
8
+ from tools.config import efficient_review_constants as constants
9
+ from tools.efficient_review_plan import plan_review
10
+
11
+
12
+ def test_host_markers_select_the_claude_plan_only_on_claude() -> None:
13
+ claude_plan = plan_review({constants.ALL_CLAUDE_HOST_VARIABLES[0]: "1"})
14
+ other_plan = plan_review(
15
+ {
16
+ constants.ALL_CLAUDE_HOST_VARIABLES[0]: "1",
17
+ constants.ALL_OTHER_HOST_VARIABLES[0]: "1",
18
+ }
19
+ )
20
+
21
+ assert claude_plan["runtime"] == "claude"
22
+ assert claude_plan["model"] == "haiku"
23
+ assert other_plan == {"runtime": "other", "model": "deferred", "profile": "deferred"}
@@ -0,0 +1,33 @@
1
+ import io
2
+ import json
3
+ from pathlib import Path
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
9
+ if str(SKILL_ROOT) not in sys.path:
10
+ sys.path.insert(0, str(SKILL_ROOT))
11
+
12
+ from tools.efficient_review import main
13
+
14
+
15
+ def test_plan_command_prints_one_deferred_plan(monkeypatch: pytest.MonkeyPatch) -> None:
16
+ monkeypatch.delenv("CLAUDECODE", raising=False)
17
+ monkeypatch.delenv("CLAUDE_CODE_ENTRYPOINT", raising=False)
18
+ plan_stream = io.StringIO()
19
+
20
+ assert main(["plan"], plan_stream) == 0
21
+ assert json.loads(plan_stream.getvalue()) == {
22
+ "runtime": "other",
23
+ "model": "deferred",
24
+ "profile": "deferred",
25
+ }
26
+ assert plan_stream.getvalue().endswith("\n")
27
+
28
+
29
+ def test_unknown_command_is_rejected() -> None:
30
+ with pytest.raises(SystemExit) as exit_info:
31
+ main(["unknown"], io.StringIO())
32
+
33
+ assert exit_info.value.code == 2
@@ -0,0 +1,27 @@
1
+ from pathlib import Path
2
+ import sys
3
+
4
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
5
+ if str(SKILL_ROOT) not in sys.path:
6
+ sys.path.insert(0, str(SKILL_ROOT))
7
+
8
+ from tools.efficient_review_plan import plan_review
9
+
10
+
11
+ def test_claude_plan_supplies_the_headless_slim_profile() -> None:
12
+ plan = plan_review({"CLAUDECODE": "1"})
13
+
14
+ assert plan["runtime"] == "claude"
15
+ assert plan["model"] == "haiku"
16
+ assert plan["profile"] == "slim"
17
+ assert plan["command"][:3] == ["claude", "-p", "--model"]
18
+ assert "--strict-mcp-config" in plan["command"]
19
+ assert "CLAUDE_CODE_DISABLE_CLAUDE_MDS=1" in plan["environment"]
20
+
21
+
22
+ def test_other_host_takes_priority_over_claude_marker() -> None:
23
+ assert plan_review({"CLAUDECODE": "1", "CODEX_CI": "1"}) == {
24
+ "runtime": "other",
25
+ "model": "deferred",
26
+ "profile": "deferred",
27
+ }
@@ -471,7 +471,7 @@ def test_should_register_a_matcher_covering_every_mutating_tool_name() -> None:
471
471
  each_entry["matcher"]
472
472
  for each_entry in hooks_configuration["hooks"]["PostToolUse"]
473
473
  if any(
474
- "verify_before_acting.py" in each_hook["command"] for each_hook in each_entry["hooks"]
474
+ "verify_before_acting.py" in each_hook.get("command", "") for each_hook in each_entry["hooks"]
475
475
  )
476
476
  ]
477
477
  assert len(all_matchers) == 1
@@ -5,6 +5,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
5
5
  ## Checks
6
6
 
7
7
  - `session/skill_loaded_reminder.py` injects a poteto-mode reminder when the skill needs loading or reloading.
8
+ - `session/compaction_recall_prompt.py` tells the agent after a compaction to run `session/compaction_user_messages.py` on its transcript. That command writes every message the user typed to a temp file and prints only the path. A PostToolUse agent hook reads the file and returns the messages in caveman style as its block reason.
8
9
  - `session/task_tool_prompt.py` injects a task tracking directive at session start.
9
10
  - `session/working_style_prompt.py` injects the session's working style at session start.
10
11
  - `session/advisor_rules_prompt.py` injects advisor guidance when the built-in advisor is enabled.
@@ -16,6 +17,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
16
17
  ## When it fires
17
18
 
18
19
  - `session/skill_loaded_reminder.py` runs on `PreToolUse` matcher `Agent|Task` at `10` seconds, `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
20
+ - `session/compaction_recall_prompt.py` runs on `SessionStart` matcher `compact` at `10` seconds in `hooks.json`. The condensing hook is a `type: "agent"` hook on `PostToolUse` matcher `Bash`, with `if` set to `Bash(*compaction_user_messages.py*)`, model `haiku`, and a `180` second timeout.
19
21
  - `session/task_tool_prompt.py`, `session/working_style_prompt.py`, `session/advisor_rules_prompt.py`, and `session/orchestrator_auto_starter.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
20
22
  - `session/deepen_first_prompt.py` runs on `UserPromptSubmit`, matcher empty, timeout `10` seconds in `hooks.json`.
21
23
  - In Cursor, `session/deepen_first_prompt.py` also runs on `beforeSubmitPrompt`. Cursor passes that event name to the `UserPromptSubmit` copy it imports from the Claude Code settings too. When both copies run, `session/deepen_turn_marker.py` creates the turn marker only when it does not exist yet, so one copy injects. A later prompt skips when either the transcript marker or the session marker exists, or when the transcript already has a user line in Claude Code's or Cursor's shape.
@@ -29,6 +31,7 @@ Preconditions:
29
31
  - Run each command from the repository root with Python and pytest available. Enable opt-in flags only inside the tests.
30
32
 
31
33
  - **Skill reminder.** Input is a helper spawn whose prompt lacks poteto-mode. Run `python -m pytest packages/claude-dev-env/hooks/session/test_skill_loaded_reminder.py -q`. The adjacent test observes the reminder and leaves an already prepared prompt unchanged.
34
+ - **Compaction recall.** Input is a transcript with typed, meta, sidechain, peer, and harness entries across a compaction. Run `python -m pytest packages/claude-dev-env/hooks/session/test_compaction_user_messages.py packages/claude-dev-env/hooks/session/test_compaction_recall_prompt.py -q`. The tests observe only typed messages in the file, a stable result across a second compaction, only the path on stdout, and an `if` rule that matches the command the recall names. `python packages/claude-dev-env/hooks/session/compaction_trim_eval.py` runs the agent hook prompt through `claude -p` on fixture transcripts with planted facts and prints the share of facts that survive and the compression ratio.
32
35
  - **Task tracking.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_task_tool_prompt.py -q`. The adjacent test observes a `SessionStart` context directive that names the task tool.
33
36
  - **Working style.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_working_style_prompt.py -q`. The adjacent test observes the fixed context text.
34
37
  - **Advisor guidance.** Input is SessionStart with `advisorModel` set. Run `python -m pytest packages/claude-dev-env/hooks/session/test_advisor_rules_prompt.py -q`. The adjacent test observes guidance only when the advisor setting is enabled.
@@ -39,6 +42,7 @@ Preconditions:
39
42
 
40
43
  ## Gotchas
41
44
 
45
+ - A SessionStart hook cannot run a prompt or agent hook, so the condensing step waits for the agent's Bash call. The agent hook reaches the agent only through `ok: false`, and Claude Code shows the reason as a PostToolUse hook blocking error. The Claude Code docs mark agent hooks experimental. When the agent hook fails or times out, the agent reads the printed file itself. The recall command is a plain Bash call, so a session that does not pre-approve Bash asks before it runs, and a headless `claude -p` run without Bash in `--allowedTools` denies it and keeps only the compaction summary.
42
46
  - `session/skill_loaded_reminder.py` has four registration points. Its tests cover helper prompt rewriting and repeated prompt suppression.
43
47
  - The advisor prompt depends on settings and an environment disable flag. The two starter scripts each use separate opt-in checks.
44
48
  - The issue tracker starter requires the checkout to appear in the project path registry.
package/hooks/hooks.json CHANGED
@@ -356,6 +356,11 @@
356
356
  "type": "command",
357
357
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
358
358
  "timeout": 10
359
+ },
360
+ {
361
+ "type": "command",
362
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/compaction_recall_prompt.py",
363
+ "timeout": 10
359
364
  }
360
365
  ]
361
366
  }
@@ -451,6 +456,19 @@
451
456
  }
452
457
  ]
453
458
  },
459
+ {
460
+ "matcher": "Bash",
461
+ "hooks": [
462
+ {
463
+ "type": "agent",
464
+ "if": "Bash(*compaction_user_messages.py*)",
465
+ "prompt": "The Bash command in this PostToolUse input printed one file path in tool_response.stdout. Read that file with the Read tool and follow the rules at its top. Return {\"ok\": false, \"reason\": \"<the condensed lines>\"}. Always return ok false, because the reason is how the condensed messages reach the agent. When the file cannot be read, return {\"ok\": false, \"reason\": \"Read the printed file yourself; the condensing hook could not.\"}\n\n$ARGUMENTS",
466
+ "model": "haiku",
467
+ "timeout": 180,
468
+ "statusMessage": "claude-dev-env: condensing the user's messages after compaction"
469
+ }
470
+ ]
471
+ },
454
472
  {
455
473
  "matcher": "Write|Edit|MultiEdit|NotebookEdit|Agent|Task|apply_patch|Bash|PowerShell|mcp__.*",
456
474
  "hooks": [
@@ -0,0 +1,101 @@
1
+ """Names, limits, and texts for the compaction user-message recall hooks.
2
+
3
+ Groups: the transcript entry shapes the extractor reads, the bounds on the
4
+ file it writes, the trim rules at the top of that file, and the SessionStart
5
+ context that tells the agent to run the extractor.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from pathlib import Path
12
+
13
+ ENTRY_TYPE_KEY = "type"
14
+ USER_ENTRY_TYPE = "user"
15
+ ATTACHMENT_ENTRY_TYPE = "attachment"
16
+ ASSISTANT_ENTRY_TYPE = "assistant"
17
+ ATTACHMENT_KEY = "attachment"
18
+ QUEUED_COMMAND_ATTACHMENT_TYPE = "queued_command"
19
+ QUEUED_PROMPT_KEY = "prompt"
20
+ MESSAGE_KEY = "message"
21
+ CONTENT_KEY = "content"
22
+ TIMESTAMP_KEY = "timestamp"
23
+ ORIGIN_KEY = "origin"
24
+ ORIGIN_KIND_KEY = "kind"
25
+ HUMAN_ORIGIN_KIND = "human"
26
+ ALL_SKIPPED_ENTRY_FLAGS = ("isMeta", "isCompactSummary", "isSidechain")
27
+ BLOCK_TYPE_KEY = "type"
28
+ TEXT_BLOCK_TYPE = "text"
29
+ TEXT_KEY = "text"
30
+ IMAGE_BLOCK_TYPE = "image"
31
+ IMAGE_PLACEHOLDER = "[image]"
32
+ BLOCK_SEPARATOR = " "
33
+ HARNESS_TAG_OPENING_PATTERN = re.compile(r"<[a-z]+(?:-[a-z]+)+[\s>]")
34
+ SLASH_COMMAND_NAME_PATTERN = re.compile(r"<command-name>\s*(.*?)\s*</command-name>", re.DOTALL)
35
+ SLASH_COMMAND_ARGS_PATTERN = re.compile(r"<command-args>(.*?)</command-args>", re.DOTALL)
36
+
37
+ MAXIMUM_MESSAGE_CHARACTERS = 4000
38
+ MAXIMUM_EXTRACT_CHARACTERS = 60000
39
+ CUT_MESSAGE_SUFFIX_TEMPLATE = "\n[{cut_count} more characters left out]"
40
+ LEFT_OUT_MESSAGES_TEMPLATE = "[{left_out_count} earlier messages left out to fit the budget]\n\n"
41
+ NO_MESSAGES_TEXT = "No typed user messages were found in this transcript.\n"
42
+ MESSAGE_HEADING_TEMPLATE = "## {position} | {timestamp}\n{text}\n\n"
43
+ MESSAGES_HEADING = "# User messages, oldest first\n\n"
44
+
45
+ OUTPUT_FILE_PREFIX = "compaction-user-messages-"
46
+ OUTPUT_FILE_SUFFIX = ".md"
47
+ OUTPUT_ENCODING = "utf-8"
48
+ TRANSCRIPT_ARGUMENT_COUNT = 2
49
+ USAGE_TEXT = "usage: compaction_user_messages.py <transcript.jsonl>\n"
50
+ USAGE_EXIT_CODE = 2
51
+
52
+ TRIM_WORD_LIMIT = 1500
53
+ TRIM_INSTRUCTIONS = (
54
+ "# How to condense this file\n\n"
55
+ "These are the messages the user typed in one Claude Code session, oldest first. "
56
+ "The session's context was just compacted, and the agent reads your condensed copy "
57
+ "in place of the user's lost words.\n\n"
58
+ "Rewrite them in caveman style: drop articles, filler, greetings, and politeness, "
59
+ "and keep the substance. Write one line per message, in order, starting with its "
60
+ "number, such as `#3`. Copy every request, constraint, correction, decision, "
61
+ "preference, question, name, file path, command, number, date, and link exactly as "
62
+ "the user wrote it. When a later message corrects or reverses an earlier one, keep "
63
+ "both and end the earlier line with `(overruled by #N)`. Drop a message only when it "
64
+ "carries nothing, such as `ok` or `thanks`. Keep the user's own words for anything "
65
+ "you are unsure how to shorten. "
66
+ f"Stay under {TRIM_WORD_LIMIT} words.\n\n"
67
+ )
68
+
69
+ SESSION_START_EVENT_NAME = "SessionStart"
70
+ COMPACTION_SOURCE = "compact"
71
+ HOOK_EVENT_NAME_INPUT_KEY = "hook_event_name"
72
+ SOURCE_INPUT_KEY = "source"
73
+ TRANSCRIPT_PATH_INPUT_KEY = "transcript_path"
74
+ ADDITIONAL_CONTEXT_KEY = "additionalContext"
75
+ EXTRACTOR_SCRIPT_NAME = "compaction_user_messages.py"
76
+ RECALL_CONTEXT_TEMPLATE = (
77
+ "The context was just compacted, so the user's own words are gone from it. "
78
+ "Run this Bash command first, before you reply and before any other tool call:\n\n"
79
+ "{command}\n\n"
80
+ "It prints a file path. A PostToolUse hook then reads that file and returns every "
81
+ "message the user typed in this session, condensed, as a hook blocking error. That "
82
+ "block is no error. It is the user's requests in order, and a later message overrides "
83
+ "an earlier one. Hold them as the user's instructions next to the summary, then "
84
+ "continue the work. Read the printed file only when no condensed block arrives."
85
+ )
86
+
87
+ EVAL_SESSION_DIRECTORY = Path(__file__).resolve().parent.parent / "session"
88
+ EVAL_FIXTURE_DIRECTORY = EVAL_SESSION_DIRECTORY / "fixtures" / "compaction_user_messages"
89
+ EVAL_HOOKS_JSON_PATH = EVAL_SESSION_DIRECTORY.parent / "hooks.json"
90
+ EVAL_RESULTS_FILE_NAME = "results.json"
91
+ EVAL_RESULTS_JSON_INDENT = 2
92
+ EVAL_SERVED_MODEL_SEPARATOR = ","
93
+ EVAL_MESSAGE_LINE_PATTERN_TEMPLATE = r"^\W*#?{position}\b.*$"
94
+ EVAL_OVERRULED_MARKER = "overruled"
95
+ EVAL_ARGUMENTS_PLACEHOLDER = "$ARGUMENTS"
96
+ EVAL_CASES_FILE_NAME = "trim_cases.json"
97
+ EVAL_CLAUDE_EXECUTABLE = "claude"
98
+ EVAL_DEFAULT_MODEL = "haiku"
99
+ EVAL_HOOK_TYPE = "agent"
100
+ EVAL_TIMEOUT_SECONDS = 240
101
+ EVAL_JSON_OBJECT_START = "{"
@@ -0,0 +1,18 @@
1
+ import json
2
+ import shlex
3
+
4
+ from hooks_constants import haiku_spawn_slim_gate_constants as constants
5
+
6
+
7
+ def test_slim_profile_command_passes_settings_and_mcp_config() -> None:
8
+ command = shlex.split(constants.SLIM_PROFILE_SHELL_COMMAND)
9
+
10
+ assert command == [constants.SLIM_PROFILE_EXECUTABLE, *constants.ALL_SLIM_PROFILE_ARGUMENTS]
11
+ assert command[command.index("--model") + 1] == "haiku"
12
+ assert json.loads(command[command.index("--settings") + 1])["autoMemoryEnabled"] is False
13
+ assert json.loads(command[command.index("--mcp-config") + 1]) == {"mcpServers": {}}
14
+
15
+
16
+ def test_deny_reason_includes_the_command_and_environment() -> None:
17
+ assert constants.SLIM_PROFILE_SHELL_COMMAND in constants.SLIM_PROFILE_DENY_REASON
18
+ assert constants.SLIM_PROFILE_ENVIRONMENT_TEXT in constants.SLIM_PROFILE_DENY_REASON
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart hook: after a compaction, tell the agent to recall the user's messages.
3
+
4
+ ::
5
+
6
+ SessionStart, source compact, transcript_path set -> run the extractor command
7
+ any other source, or no transcript_path -> nothing
8
+
9
+ The context names one Bash command that runs ``compaction_user_messages.py``
10
+ on this session's transcript. A PostToolUse agent hook in ``hooks.json``
11
+ matches that command and returns the user's messages, condensed.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import shlex
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
22
+ if _hooks_directory not in sys.path:
23
+ sys.path.insert(0, _hooks_directory)
24
+
25
+ from hooks_constants.compaction_user_messages_constants import (
26
+ ADDITIONAL_CONTEXT_KEY,
27
+ COMPACTION_SOURCE,
28
+ EXTRACTOR_SCRIPT_NAME,
29
+ HOOK_EVENT_NAME_INPUT_KEY,
30
+ RECALL_CONTEXT_TEMPLATE,
31
+ SESSION_START_EVENT_NAME,
32
+ SOURCE_INPUT_KEY,
33
+ TRANSCRIPT_PATH_INPUT_KEY,
34
+ )
35
+ from hooks_constants.hook_specific_output_keys import (
36
+ HOOK_EVENT_NAME_KEY,
37
+ HOOK_SPECIFIC_OUTPUT_KEY,
38
+ )
39
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
40
+
41
+
42
+ def recall_context(all_hook_fields: dict[str, object]) -> str | None:
43
+ """Return the recall instruction for a compaction, or None for any other start.
44
+
45
+ ::
46
+
47
+ {"hook_event_name": "SessionStart", "source": "compact",
48
+ "transcript_path": "/t/abc.jsonl"}
49
+ -> "...run this Bash command:\\n\\n'/usr/bin/python3' '.../compaction_user_messages.py' /t/abc.jsonl..."
50
+
51
+ The command names the interpreter running this hook, so it works where
52
+ ``python3`` is not on PATH.
53
+
54
+ Args:
55
+ all_hook_fields: The parsed hook input.
56
+
57
+ Returns:
58
+ The additionalContext text, or None.
59
+ """
60
+ if all_hook_fields.get(HOOK_EVENT_NAME_INPUT_KEY) != SESSION_START_EVENT_NAME:
61
+ return None
62
+ if all_hook_fields.get(SOURCE_INPUT_KEY) != COMPACTION_SOURCE:
63
+ return None
64
+ transcript_path = all_hook_fields.get(TRANSCRIPT_PATH_INPUT_KEY)
65
+ if not isinstance(transcript_path, str) or not transcript_path:
66
+ return None
67
+ extractor_path = Path(__file__).resolve().parent / EXTRACTOR_SCRIPT_NAME
68
+ command = shlex.join([sys.executable, str(extractor_path), transcript_path])
69
+ return RECALL_CONTEXT_TEMPLATE.format(command=command)
70
+
71
+
72
+ def main() -> None:
73
+ """Print the recall instruction as SessionStart additionalContext, or stay quiet."""
74
+ hook_input = read_hook_input_dictionary_from_stdin()
75
+ if hook_input is None:
76
+ return
77
+ context_text = recall_context(hook_input)
78
+ if context_text is None:
79
+ return
80
+ hook_output = {
81
+ HOOK_SPECIFIC_OUTPUT_KEY: {
82
+ HOOK_EVENT_NAME_KEY: SESSION_START_EVENT_NAME,
83
+ ADDITIONAL_CONTEXT_KEY: context_text,
84
+ }
85
+ }
86
+ sys.stdout.write(json.dumps(hook_output) + "\n")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()