claude-dev-env 7.2.0 → 7.2.1

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.
@@ -15,7 +15,11 @@ Search files instantly on Windows using the Everything command-line interface (e
15
15
 
16
16
  Every search carries a scope: a project path or registry token, an `ext:` filter, a `dm:` date filter, a `size:` filter, or a name pattern. A bare whole-drive scan or a network-share sweep is out of bounds. Narrow the search to what you need.
17
17
 
18
- When `es.exe` fails or returns nothing, self-heal first: fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents), and report the outage so the reader knows the index was unavailable. When self-healing also fails, ask the user through `AskUserQuestion` with a short analysis and next-step options.
18
+ When `es.exe` returns Error 8, the Everything IPC client window is missing. Retry the same search twice with a short pause. Do not report that Everything is not running, not loaded, or that the index is unavailable from that first miss. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to the `Glob` tool (name and path patterns) or `Grep` (file contents) with the same scope.
19
+
20
+ When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. A failed search is not proof the index is down. When those fallbacks also fail, ask the user through `AskUserQuestion` with a short analysis and next-step options.
21
+
22
+ Direct `es.exe` calls use the same Error 8 retry. The wrapper in `scripts/everything_search.py` already retries Error 8 twice.
19
23
 
20
24
  ## Registry tokens
21
25
 
@@ -139,7 +143,8 @@ Run separate searches for each extension type:
139
143
 
140
144
  ## Best Practices
141
145
 
142
- - Everything must be running (system tray) for es.exe to work
146
+ - `es.exe` talks to the Everything tray client IPC window. The Everything service can be running while Error 8 still fires.
147
+ - On Error 8, retry the same search twice before treating the search as failed.
143
148
  - Run one extension per search for cleaner results
144
149
  - Use `dm:` to filter recent files by the index timestamp
145
150
  - Combine with path to narrow scope
@@ -165,5 +170,5 @@ Everything indexes REAL paths only, not junctions or mapped drives.
165
170
 
166
171
  ## Files
167
172
 
168
- - `scripts/everything_search.py` runs one Everything search without a shell.
169
- - `scripts/test_everything_search.py` verifies registry expansion and command failures.
173
+ - `scripts/everything_search.py` runs one Everything search without a shell, retries Error 8 twice, and reports that the IPC client did not answer and that the service state was not probed when those retries fail.
174
+ - `scripts/test_everything_search.py` verifies registry expansion, command failures, Error 8 retry, and the IPC-client miss message.
@@ -4,6 +4,7 @@ import json
4
4
  import shutil
5
5
  import subprocess
6
6
  import sys
7
+ import time
7
8
  from collections.abc import Mapping, Sequence
8
9
  from pathlib import Path, PurePosixPath, PureWindowsPath
9
10
  from typing import TextIO
@@ -14,6 +15,10 @@ from everything_search_command_constants.config.constants import (
14
15
  EXECUTION_ERROR_EXIT_CODE,
15
16
  INFORMATIONAL_ARGUMENT,
16
17
  INVALID_INPUT_EXIT_CODE,
18
+ IPC_CLIENT_DID_NOT_ANSWER_MESSAGE,
19
+ IPC_WINDOW_NOT_FOUND_EXIT_CODE,
20
+ IPC_WINDOW_RETRY_COUNT,
21
+ IPC_WINDOW_RETRY_DELAY_SECONDS,
17
22
  PROJECT_PATHS_FILE_NAME,
18
23
  REGISTRY_META_KEY,
19
24
  SEARCH_SCOPE_REQUIRED_MESSAGE,
@@ -115,21 +120,32 @@ def _run_search(
115
120
  search_stdout: TextIO,
116
121
  search_stderr: TextIO,
117
122
  ) -> int:
118
- try:
119
- search_run = subprocess.run(
120
- [executable_path, *all_search_arguments],
121
- capture_output=True,
122
- check=False,
123
- encoding=UTF8_ENCODING,
124
- shell=False,
125
- text=True,
126
- )
127
- except OSError as error:
128
- search_stderr.write(f"{error}\n")
129
- return EXECUTION_ERROR_EXIT_CODE
130
- search_stdout.write(search_run.stdout)
131
- search_stderr.write(search_run.stderr)
132
- return search_run.returncode
123
+ ipc_client_did_not_answer_message = IPC_CLIENT_DID_NOT_ANSWER_MESSAGE
124
+ ipc_window_not_found_exit_code = IPC_WINDOW_NOT_FOUND_EXIT_CODE
125
+ ipc_window_retry_count = IPC_WINDOW_RETRY_COUNT
126
+ ipc_window_retry_delay_seconds = IPC_WINDOW_RETRY_DELAY_SECONDS
127
+ last_search_run = None
128
+ for each_attempt_index in range(ipc_window_retry_count + 1):
129
+ try:
130
+ last_search_run = subprocess.run(
131
+ [executable_path, *all_search_arguments],
132
+ capture_output=True,
133
+ check=False,
134
+ encoding=UTF8_ENCODING,
135
+ shell=False,
136
+ text=True,
137
+ )
138
+ except OSError as error:
139
+ search_stderr.write(f"{error}\n")
140
+ return EXECUTION_ERROR_EXIT_CODE
141
+ if last_search_run.returncode != ipc_window_not_found_exit_code:
142
+ search_stdout.write(last_search_run.stdout)
143
+ search_stderr.write(last_search_run.stderr)
144
+ return last_search_run.returncode
145
+ if each_attempt_index < ipc_window_retry_count:
146
+ time.sleep(ipc_window_retry_delay_seconds)
147
+ search_stderr.write(ipc_client_did_not_answer_message)
148
+ return ipc_window_not_found_exit_code
133
149
 
134
150
 
135
151
  def _run_registered_search(
@@ -5,6 +5,12 @@ EXECUTABLE_NAME = "es.exe"
5
5
  EXECUTION_ERROR_EXIT_CODE = 1
6
6
  INFORMATIONAL_ARGUMENT = "-version"
7
7
  INVALID_INPUT_EXIT_CODE = 2
8
+ IPC_CLIENT_DID_NOT_ANSWER_MESSAGE = (
9
+ "Everything IPC client did not answer. Everything service state was not probed.\n"
10
+ )
11
+ IPC_WINDOW_NOT_FOUND_EXIT_CODE = 8
12
+ IPC_WINDOW_RETRY_COUNT = 2
13
+ IPC_WINDOW_RETRY_DELAY_SECONDS = 0.5
8
14
  PROJECT_PATHS_FILE_NAME = "project-paths.json"
9
15
  REGISTRY_META_KEY = "_meta"
10
16
  SEARCH_SCOPE_REQUIRED_MESSAGE = "The first search argument must define a path, project, name, extension, date, or size scope.\n"
@@ -113,12 +113,79 @@ def test_main_passes_through_child_exit_status() -> None:
113
113
  with (
114
114
  patch.object(search, "load_registry", return_value={}),
115
115
  patch.object(search.shutil, "which", return_value="es.exe"),
116
- patch.object(search.subprocess, "run", return_value=completed_process),
116
+ patch.object(
117
+ search.subprocess, "run", return_value=completed_process
118
+ ) as run_process,
117
119
  patch.object(search.sys, "stderr", captured_stderr),
118
120
  ):
119
121
  exit_code = search.main(["config.py"])
120
122
  assert exit_code == 7
121
123
  assert captured_stderr.getvalue() == "failed\n"
124
+ run_process.assert_called_once()
125
+
126
+
127
+ def _ipc_window_error_process() -> search.subprocess.CompletedProcess:
128
+ return search.subprocess.CompletedProcess(
129
+ args=["es.exe", "config.py"],
130
+ returncode=8,
131
+ stdout="",
132
+ stderr=(
133
+ "Error 8: Everything IPC window not found. "
134
+ "Please make sure Everything is running.\n"
135
+ ),
136
+ )
137
+
138
+
139
+ def test_main_retries_ipc_window_error_then_returns_search_hits() -> None:
140
+ successful_process = search.subprocess.CompletedProcess(
141
+ args=["es.exe", "config.py"],
142
+ returncode=0,
143
+ stdout="found path\n",
144
+ stderr="",
145
+ )
146
+ captured_stdout = io.StringIO()
147
+ captured_stderr = io.StringIO()
148
+ with (
149
+ patch.object(search, "load_registry", return_value={}),
150
+ patch.object(search.shutil, "which", return_value="es.exe"),
151
+ patch.object(
152
+ search.subprocess,
153
+ "run",
154
+ side_effect=[_ipc_window_error_process(), successful_process],
155
+ ) as run_process,
156
+ patch.object(search.sys, "stdout", captured_stdout),
157
+ patch.object(search.sys, "stderr", captured_stderr),
158
+ patch("time.sleep"),
159
+ ):
160
+ exit_code = search.main(["config.py"])
161
+ assert exit_code == 0
162
+ assert captured_stdout.getvalue() == "found path\n"
163
+ assert "not loaded" not in captured_stderr.getvalue().casefold()
164
+ assert "not running" not in captured_stderr.getvalue().casefold()
165
+ assert run_process.call_count == 2
166
+
167
+
168
+ def test_main_reports_ipc_client_miss_after_ipc_window_retries_exhaust() -> None:
169
+ captured_stderr = io.StringIO()
170
+ with (
171
+ patch.object(search, "load_registry", return_value={}),
172
+ patch.object(search.shutil, "which", return_value="es.exe"),
173
+ patch.object(
174
+ search.subprocess,
175
+ "run",
176
+ return_value=_ipc_window_error_process(),
177
+ ) as run_process,
178
+ patch.object(search.sys, "stderr", captured_stderr),
179
+ patch("time.sleep"),
180
+ ):
181
+ exit_code = search.main(["config.py"])
182
+ assert exit_code == 8
183
+ stderr_text = captured_stderr.getvalue()
184
+ assert "Everything IPC client did not answer" in stderr_text
185
+ assert "service state was not probed" in stderr_text.casefold()
186
+ assert "Please make sure Everything is running" not in stderr_text
187
+ assert "not loaded" not in stderr_text.casefold()
188
+ assert run_process.call_count == 3
122
189
 
123
190
 
124
191
  def test_main_reports_missing_executable() -> None:
@@ -1353,6 +1353,23 @@ test('mergeHooksIntoSettings is idempotent when run twice against an already-upd
1353
1353
  });
1354
1354
 
1355
1355
 
1356
+ test('shipped SessionEnd hook timeouts stay within the Codex runtime limit', () => {
1357
+ const shippedHooksConfig = JSON.parse(
1358
+ readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
1359
+ );
1360
+ const allSessionEndHooks = (shippedHooksConfig.hooks.SessionEnd || [])
1361
+ .flatMap(eachMatcherGroup => eachMatcherGroup.hooks);
1362
+
1363
+ assert.ok(allSessionEndHooks.length > 0, 'shipped hooks.json must register SessionEnd hooks');
1364
+ for (const eachSessionEndHook of allSessionEndHooks) {
1365
+ assert.ok(
1366
+ eachSessionEndHook.timeout <= 3,
1367
+ `${eachSessionEndHook.command} exceeds the Codex SessionEnd runtime limit`,
1368
+ );
1369
+ }
1370
+ });
1371
+
1372
+
1356
1373
  test('shipped hooks.json keeps only nonblocking policy handlers and lifecycle hooks', () => {
1357
1374
  const shippedHooksConfig = JSON.parse(
1358
1375
  readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
@@ -0,0 +1 @@
1
+ """Retired search hook entrypoint kept inert for stale command references."""
@@ -0,0 +1,46 @@
1
+ """Regression tests for the inert retired search hook entrypoint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
12
+ _ENTRYPOINT_PATH = _BLOCKING_DIRECTORY / "content_search_to_zoekt_redirector.py"
13
+ _HOOKS_MANIFEST_PATH = _BLOCKING_DIRECTORY.parent / "hooks.json"
14
+
15
+
16
+ def test_retired_entrypoint_allows_valid_search_events_silently() -> None:
17
+ all_search_events = (
18
+ {"tool_name": "Bash", "tool_input": {"command": "rg --files packages"}},
19
+ {
20
+ "tool_name": "Grep",
21
+ "tool_input": {"pattern": "zoekt", "path": "packages"},
22
+ },
23
+ {
24
+ "tool_name": "Search",
25
+ "tool_input": {"pattern": "zoekt", "path": "packages"},
26
+ },
27
+ )
28
+
29
+ for each_search_event in all_search_events:
30
+ completed_process = subprocess.run(
31
+ [sys.executable, str(_ENTRYPOINT_PATH)],
32
+ check=False,
33
+ input=json.dumps(each_search_event),
34
+ capture_output=True,
35
+ text=True,
36
+ encoding="utf-8",
37
+ )
38
+ assert completed_process.returncode == 0
39
+ assert completed_process.stdout == ""
40
+ assert completed_process.stderr == ""
41
+
42
+
43
+ def test_shipped_hook_manifest_omits_retired_entrypoint() -> None:
44
+ hooks_manifest_text = _HOOKS_MANIFEST_PATH.read_text(encoding="utf-8")
45
+
46
+ assert _ENTRYPOINT_PATH.name not in hooks_manifest_text
package/hooks/hooks.json CHANGED
@@ -84,12 +84,12 @@
84
84
  {
85
85
  "type": "command",
86
86
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/lifecycle/session_end_cleanup.py",
87
- "timeout": 15
87
+ "timeout": 3
88
88
  },
89
89
  {
90
90
  "type": "command",
91
91
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/session_edit_tracker_cleanup.py",
92
- "timeout": 10
92
+ "timeout": 3
93
93
  }
94
94
  ]
95
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,7 +19,7 @@ Three tools are equally sanctioned; pick by what you know:
19
19
  | A name or path pattern | The harness `Glob` tool |
20
20
  | Text inside files | The harness `Grep` tool |
21
21
 
22
- When `es.exe` fails or returns nothing, fall back to `Glob` or `Grep` without pausing, and report the outage so the reader knows the index was unavailable. Ask the user only after all three tools fail.
22
+ When `es.exe` returns Error 8, retry the same search twice before any fallback. Error 8 is a missing IPC client window, not proof the index is down. After those retries fail, report that the Everything IPC client did not answer and that the service state was not probed, then fall back to `Glob` or `Grep` with the same scope. When `es.exe` is missing, or a later search returns no hits, fall back to `Glob` or `Grep`. Ask the user only after all three tools fail.
23
23
 
24
24
  `skills/everything-search/SKILL.md` holds the full `es.exe` operator reference: `ext:`, `dm:`, `size:`, wildcards, OR/AND/NOT, output flags, and the junction and drive-mapping note.
25
25