claude-dev-env 1.30.1 → 1.32.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.
- package/agents/clean-coder.md +275 -111
- package/agents/code-quality-agent.md +196 -209
- package/bin/install.mjs +81 -0
- package/bin/install.test.mjs +158 -0
- package/bin/install_mypy_ini.mjs +51 -0
- package/bin/install_mypy_ini.test.mjs +121 -0
- package/commands/hook-log-extract.md +70 -0
- package/commands/hook-log-init.md +76 -0
- package/hooks/blocking/code_rules_enforcer.py +5 -3
- package/hooks/blocking/destructive_command_blocker.py +187 -0
- package/hooks/blocking/question_to_user_enforcer.py +140 -0
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +39 -0
- package/hooks/blocking/test_destructive_command_blocker.py +397 -0
- package/hooks/blocking/test_question_to_user_enforcer.py +163 -0
- package/hooks/blocking/test_windows_rmtree_blocker.py +148 -0
- package/hooks/blocking/windows_rmtree_blocker.py +106 -0
- package/hooks/config/hook_log_extractor_constants.py +234 -0
- package/hooks/config/messages.py +3 -0
- package/hooks/config/session_env_cleanup_constants.py +18 -0
- package/hooks/config/test_hook_log_extractor_constants.py +123 -0
- package/hooks/config/test_messages.py +5 -0
- package/hooks/config/test_session_env_cleanup_constants.py +55 -0
- package/hooks/diagnostic/hook_log_extractor.py +907 -0
- package/hooks/diagnostic/hook_log_init.py +202 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +172 -0
- package/hooks/diagnostic/migrations/2026-04-25-drop-themes-hook-events.sql +3 -0
- package/hooks/diagnostic/migrations/README.md +77 -0
- package/hooks/diagnostic/queries/block_details_for_hook.sql +26 -0
- package/hooks/diagnostic/queries/blocks_by_category.sql +10 -0
- package/hooks/diagnostic/queries/blocks_by_tool.sql +9 -0
- package/hooks/diagnostic/queries/blocks_last_7_days.sql +11 -0
- package/hooks/diagnostic/queries/top_blockers_last_24_hours.sql +12 -0
- package/hooks/diagnostic/queries/top_blockers_overall.sql +12 -0
- package/hooks/diagnostic/requirements-hook-logs-dev.txt +2 -0
- package/hooks/diagnostic/requirements-hook-logs.txt +1 -0
- package/hooks/diagnostic/schema.sql +51 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +1531 -0
- package/hooks/diagnostic/test_hook_log_init.py +227 -0
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +345 -0
- package/hooks/hooks.json +25 -0
- package/hooks/session/session_env_cleanup.py +129 -0
- package/hooks/session/test_session_env_cleanup.py +278 -0
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +44 -0
- package/rules/windows-filesystem-safe.md +93 -0
- package/scripts/config/test_spec_implementer_prompt.py +0 -4
- package/scripts/test_groq_bugteam_spec.py +0 -8
- package/skills/bugteam/SKILL.md +15 -1
- package/skills/bugteam/SKILL_EVALS.md +1 -1
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/scripts/README.md +17 -0
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +238 -0
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +267 -0
- package/skills/logifix/SKILL.md +69 -0
- package/skills/logifix/scripts/logifix.ps1 +205 -0
- package/skills/rebase/SKILL.md +157 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _expected_hooks_path_suffix() -> str:
|
|
10
|
+
return "hooks/git-hooks"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _canonical_hooks_directory_components() -> tuple[str, ...]:
|
|
14
|
+
return (".claude", "hooks", "git-hooks")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _home_env_var_names() -> tuple[str, ...]:
|
|
18
|
+
return ("HOME", "USERPROFILE")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def resolve_canonical_hooks_directory(
|
|
22
|
+
environment_overrides: dict[str, str] | None,
|
|
23
|
+
) -> Path:
|
|
24
|
+
components = _canonical_hooks_directory_components()
|
|
25
|
+
if environment_overrides is not None:
|
|
26
|
+
for each_env_var_name in _home_env_var_names():
|
|
27
|
+
home_value = environment_overrides.get(each_env_var_name)
|
|
28
|
+
if home_value:
|
|
29
|
+
return Path(home_value).joinpath(*components)
|
|
30
|
+
return Path.home().joinpath(*components)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def list_local_core_hooks_path_values(
|
|
34
|
+
repository_root: Path,
|
|
35
|
+
environment_overrides: dict[str, str] | None,
|
|
36
|
+
) -> list[str]:
|
|
37
|
+
git_command = [
|
|
38
|
+
"git",
|
|
39
|
+
"-C",
|
|
40
|
+
str(repository_root),
|
|
41
|
+
"config",
|
|
42
|
+
"--local",
|
|
43
|
+
"--get-all",
|
|
44
|
+
"core.hooksPath",
|
|
45
|
+
]
|
|
46
|
+
completed_process = subprocess.run(
|
|
47
|
+
git_command,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
text=True,
|
|
50
|
+
encoding="utf-8",
|
|
51
|
+
errors="replace",
|
|
52
|
+
check=False,
|
|
53
|
+
env=environment_overrides,
|
|
54
|
+
)
|
|
55
|
+
if completed_process.returncode != 0:
|
|
56
|
+
return []
|
|
57
|
+
return [
|
|
58
|
+
each_line.strip()
|
|
59
|
+
for each_line in completed_process.stdout.splitlines()
|
|
60
|
+
if each_line.strip()
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_global_core_hooks_path(
|
|
65
|
+
environment_overrides: dict[str, str] | None,
|
|
66
|
+
) -> str:
|
|
67
|
+
git_command = ["git", "config", "--global", "--get", "core.hooksPath"]
|
|
68
|
+
completed_process = subprocess.run(
|
|
69
|
+
git_command,
|
|
70
|
+
capture_output=True,
|
|
71
|
+
text=True,
|
|
72
|
+
encoding="utf-8",
|
|
73
|
+
errors="replace",
|
|
74
|
+
check=False,
|
|
75
|
+
env=environment_overrides,
|
|
76
|
+
)
|
|
77
|
+
if completed_process.returncode != 0:
|
|
78
|
+
return ""
|
|
79
|
+
return completed_process.stdout.strip()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def unset_local_core_hooks_path(
|
|
83
|
+
repository_root: Path,
|
|
84
|
+
environment_overrides: dict[str, str] | None,
|
|
85
|
+
) -> None:
|
|
86
|
+
git_command = [
|
|
87
|
+
"git",
|
|
88
|
+
"-C",
|
|
89
|
+
str(repository_root),
|
|
90
|
+
"config",
|
|
91
|
+
"--local",
|
|
92
|
+
"--unset-all",
|
|
93
|
+
"core.hooksPath",
|
|
94
|
+
]
|
|
95
|
+
subprocess.run(
|
|
96
|
+
git_command,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
check=False,
|
|
100
|
+
env=environment_overrides,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def set_global_core_hooks_path(
|
|
105
|
+
target_value: str,
|
|
106
|
+
environment_overrides: dict[str, str] | None,
|
|
107
|
+
) -> int:
|
|
108
|
+
git_command = ["git", "config", "--global", "core.hooksPath", target_value]
|
|
109
|
+
completed_process = subprocess.run(
|
|
110
|
+
git_command,
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
check=False,
|
|
114
|
+
env=environment_overrides,
|
|
115
|
+
)
|
|
116
|
+
return completed_process.returncode
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def normalize_hooks_path(raw_value: str) -> str:
|
|
120
|
+
return raw_value.replace("\\", "/").rstrip("/")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def is_canonical_hooks_path(raw_value: str) -> bool:
|
|
124
|
+
if not raw_value:
|
|
125
|
+
return False
|
|
126
|
+
return normalize_hooks_path(raw_value).endswith(_expected_hooks_path_suffix())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def find_repository_root(start: Path) -> Path:
|
|
130
|
+
resolved_start = start.resolve()
|
|
131
|
+
candidate_paths = [resolved_start, *resolved_start.parents]
|
|
132
|
+
for each_candidate in candidate_paths:
|
|
133
|
+
marker = each_candidate / ".git"
|
|
134
|
+
if marker.is_dir() or marker.is_file():
|
|
135
|
+
return each_candidate
|
|
136
|
+
return resolved_start
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def rerun_preflight(
|
|
140
|
+
repository_root: Path,
|
|
141
|
+
environment_overrides: dict[str, str] | None,
|
|
142
|
+
) -> int:
|
|
143
|
+
preflight_script_path = Path(__file__).resolve().parent / "bugteam_preflight.py"
|
|
144
|
+
rerun_command = [
|
|
145
|
+
sys.executable,
|
|
146
|
+
str(preflight_script_path),
|
|
147
|
+
"--no-pytest",
|
|
148
|
+
"--repo-root",
|
|
149
|
+
str(repository_root),
|
|
150
|
+
]
|
|
151
|
+
completed_process = subprocess.run(
|
|
152
|
+
rerun_command,
|
|
153
|
+
check=False,
|
|
154
|
+
env=environment_overrides,
|
|
155
|
+
)
|
|
156
|
+
return completed_process.returncode
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_arguments(argv: list[str]) -> argparse.Namespace:
|
|
160
|
+
parser = argparse.ArgumentParser(
|
|
161
|
+
description=(
|
|
162
|
+
"Auto-fix core.hooksPath when bugteam preflight detects a stale override. "
|
|
163
|
+
"Removes a local-scope override and ensures global core.hooksPath points "
|
|
164
|
+
"at the canonical claude-dev-env git-hooks directory."
|
|
165
|
+
),
|
|
166
|
+
)
|
|
167
|
+
parser.add_argument(
|
|
168
|
+
"--repo-root",
|
|
169
|
+
type=Path,
|
|
170
|
+
default=None,
|
|
171
|
+
help="Repository root (default: discover from cwd).",
|
|
172
|
+
)
|
|
173
|
+
return parser.parse_args(argv)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main(
|
|
177
|
+
argv: list[str] | None = None,
|
|
178
|
+
*,
|
|
179
|
+
environment_overrides: dict[str, str] | None = None,
|
|
180
|
+
) -> int:
|
|
181
|
+
arguments = parse_arguments(sys.argv[1:] if argv is None else argv)
|
|
182
|
+
start_directory = Path.cwd()
|
|
183
|
+
repository_root = (
|
|
184
|
+
arguments.repo_root.resolve()
|
|
185
|
+
if arguments.repo_root is not None
|
|
186
|
+
else find_repository_root(start_directory)
|
|
187
|
+
)
|
|
188
|
+
canonical_hooks_directory = resolve_canonical_hooks_directory(environment_overrides)
|
|
189
|
+
expected_suffix = _expected_hooks_path_suffix()
|
|
190
|
+
if not canonical_hooks_directory.is_dir():
|
|
191
|
+
print(
|
|
192
|
+
"bugteam_fix_hookspath: canonical hooks directory does not exist: "
|
|
193
|
+
f"{canonical_hooks_directory}\n"
|
|
194
|
+
"Run: npx claude-dev-env .\n"
|
|
195
|
+
"Then re-run /bugteam. The directory must end in "
|
|
196
|
+
f"'{expected_suffix}' and contain the claude-dev-env git hook shims.",
|
|
197
|
+
file=sys.stderr,
|
|
198
|
+
)
|
|
199
|
+
return 1
|
|
200
|
+
local_hooks_path_values = list_local_core_hooks_path_values(
|
|
201
|
+
repository_root,
|
|
202
|
+
environment_overrides,
|
|
203
|
+
)
|
|
204
|
+
has_non_canonical_local_override = any(
|
|
205
|
+
not is_canonical_hooks_path(each_value)
|
|
206
|
+
for each_value in local_hooks_path_values
|
|
207
|
+
)
|
|
208
|
+
if has_non_canonical_local_override:
|
|
209
|
+
unset_local_core_hooks_path(repository_root, environment_overrides)
|
|
210
|
+
print(
|
|
211
|
+
"bugteam_fix_hookspath: removed stale local core.hooksPath override on "
|
|
212
|
+
f"{repository_root}",
|
|
213
|
+
file=sys.stderr,
|
|
214
|
+
)
|
|
215
|
+
current_global_value = read_global_core_hooks_path(environment_overrides)
|
|
216
|
+
if not is_canonical_hooks_path(current_global_value):
|
|
217
|
+
canonical_target_value = str(canonical_hooks_directory).replace("\\", "/")
|
|
218
|
+
global_set_exit_code = set_global_core_hooks_path(
|
|
219
|
+
canonical_target_value,
|
|
220
|
+
environment_overrides,
|
|
221
|
+
)
|
|
222
|
+
if global_set_exit_code != 0:
|
|
223
|
+
print(
|
|
224
|
+
"bugteam_fix_hookspath: failed to set global core.hooksPath to "
|
|
225
|
+
f"{canonical_target_value} (git exit {global_set_exit_code}).",
|
|
226
|
+
file=sys.stderr,
|
|
227
|
+
)
|
|
228
|
+
return 1
|
|
229
|
+
print(
|
|
230
|
+
"bugteam_fix_hookspath: set global core.hooksPath to "
|
|
231
|
+
f"{canonical_target_value}",
|
|
232
|
+
file=sys.stderr,
|
|
233
|
+
)
|
|
234
|
+
return rerun_preflight(repository_root, environment_overrides)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if __name__ == "__main__":
|
|
238
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Tests for bugteam_fix_hookspath auto-remediation.
|
|
2
|
+
|
|
3
|
+
Covers:
|
|
4
|
+
- removes a local-scope core.hooksPath override and re-runs preflight
|
|
5
|
+
- sets global core.hooksPath when missing
|
|
6
|
+
- idempotent: second invocation produces the same final state with no errors
|
|
7
|
+
- no-op when no override exists and global is already canonical
|
|
8
|
+
- exits non-zero with a clear message when canonical hooks dir is missing
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib.util
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from types import ModuleType
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_fix_module() -> ModuleType:
|
|
23
|
+
module_path = Path(__file__).parent / "bugteam_fix_hookspath.py"
|
|
24
|
+
spec = importlib.util.spec_from_file_location("bugteam_fix_hookspath", module_path)
|
|
25
|
+
assert spec is not None
|
|
26
|
+
assert spec.loader is not None
|
|
27
|
+
module = importlib.util.module_from_spec(spec)
|
|
28
|
+
spec.loader.exec_module(module)
|
|
29
|
+
return module
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
bugteam_fix_hookspath = _load_fix_module()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _make_isolated_git_environment(home_directory: Path) -> dict[str, str]:
|
|
36
|
+
"""Build an env dict that pins git's HOME and XDG paths into a tmp directory.
|
|
37
|
+
|
|
38
|
+
Without this, real `git config --global` reads/writes hit the developer's
|
|
39
|
+
actual ~/.gitconfig — which would corrupt the host machine and make tests
|
|
40
|
+
depend on global state. Pointing HOME, USERPROFILE, XDG_CONFIG_HOME, and
|
|
41
|
+
GIT_CONFIG_GLOBAL at a temp directory isolates the test fully.
|
|
42
|
+
"""
|
|
43
|
+
isolated_environment = os.environ.copy()
|
|
44
|
+
isolated_environment["HOME"] = str(home_directory)
|
|
45
|
+
isolated_environment["USERPROFILE"] = str(home_directory)
|
|
46
|
+
isolated_environment["XDG_CONFIG_HOME"] = str(home_directory / ".config")
|
|
47
|
+
isolated_environment["GIT_CONFIG_GLOBAL"] = str(home_directory / ".gitconfig")
|
|
48
|
+
isolated_environment["GIT_CONFIG_NOSYSTEM"] = "1"
|
|
49
|
+
return isolated_environment
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _initialize_repository(repository_path: Path, environment: dict[str, str]) -> None:
|
|
53
|
+
repository_path.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
subprocess.run(
|
|
55
|
+
["git", "init", "--quiet", str(repository_path)],
|
|
56
|
+
check=True,
|
|
57
|
+
env=environment,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _set_local_hooks_path(
|
|
62
|
+
repository_path: Path,
|
|
63
|
+
hooks_path_value: str,
|
|
64
|
+
environment: dict[str, str],
|
|
65
|
+
) -> None:
|
|
66
|
+
subprocess.run(
|
|
67
|
+
[
|
|
68
|
+
"git",
|
|
69
|
+
"-C",
|
|
70
|
+
str(repository_path),
|
|
71
|
+
"config",
|
|
72
|
+
"--local",
|
|
73
|
+
"core.hooksPath",
|
|
74
|
+
hooks_path_value,
|
|
75
|
+
],
|
|
76
|
+
check=True,
|
|
77
|
+
env=environment,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _set_global_hooks_path(hooks_path_value: str, environment: dict[str, str]) -> None:
|
|
82
|
+
subprocess.run(
|
|
83
|
+
["git", "config", "--global", "core.hooksPath", hooks_path_value],
|
|
84
|
+
check=True,
|
|
85
|
+
env=environment,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _read_local_hooks_path(repository_path: Path, environment: dict[str, str]) -> str:
|
|
90
|
+
completed_process = subprocess.run(
|
|
91
|
+
[
|
|
92
|
+
"git",
|
|
93
|
+
"-C",
|
|
94
|
+
str(repository_path),
|
|
95
|
+
"config",
|
|
96
|
+
"--local",
|
|
97
|
+
"--get",
|
|
98
|
+
"core.hooksPath",
|
|
99
|
+
],
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
check=False,
|
|
103
|
+
env=environment,
|
|
104
|
+
)
|
|
105
|
+
return completed_process.stdout.strip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _read_global_hooks_path(environment: dict[str, str]) -> str:
|
|
109
|
+
completed_process = subprocess.run(
|
|
110
|
+
["git", "config", "--global", "--get", "core.hooksPath"],
|
|
111
|
+
capture_output=True,
|
|
112
|
+
text=True,
|
|
113
|
+
check=False,
|
|
114
|
+
env=environment,
|
|
115
|
+
)
|
|
116
|
+
return completed_process.stdout.strip()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _create_canonical_hooks_directory(home_directory: Path) -> Path:
|
|
120
|
+
canonical_hooks_directory = home_directory / ".claude" / "hooks" / "git-hooks"
|
|
121
|
+
canonical_hooks_directory.mkdir(parents=True)
|
|
122
|
+
return canonical_hooks_directory
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_should_remove_local_override_and_pass_preflight(tmp_path: Path) -> None:
|
|
126
|
+
home_directory = tmp_path / "home"
|
|
127
|
+
home_directory.mkdir()
|
|
128
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
129
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
130
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
131
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
132
|
+
_initialize_repository(repository_path, environment)
|
|
133
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
134
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
135
|
+
|
|
136
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
137
|
+
["--repo-root", str(repository_path)],
|
|
138
|
+
environment_overrides=environment,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
assert exit_code == 0, (
|
|
142
|
+
"fix script must succeed when canonical global hooks dir exists"
|
|
143
|
+
)
|
|
144
|
+
assert _read_local_hooks_path(repository_path, environment) == "", (
|
|
145
|
+
"local core.hooksPath override must be removed"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_should_set_global_hooks_path_when_missing(tmp_path: Path) -> None:
|
|
150
|
+
home_directory = tmp_path / "home"
|
|
151
|
+
home_directory.mkdir()
|
|
152
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
153
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
154
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
155
|
+
_initialize_repository(repository_path, environment)
|
|
156
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
157
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
158
|
+
|
|
159
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
160
|
+
["--repo-root", str(repository_path)],
|
|
161
|
+
environment_overrides=environment,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
assert exit_code == 0
|
|
165
|
+
global_value_after_fix = _read_global_hooks_path(environment)
|
|
166
|
+
assert (
|
|
167
|
+
global_value_after_fix.replace("\\", "/")
|
|
168
|
+
.rstrip("/")
|
|
169
|
+
.endswith("hooks/git-hooks")
|
|
170
|
+
), (
|
|
171
|
+
"fix script must set canonical global core.hooksPath when missing; "
|
|
172
|
+
f"got '{global_value_after_fix}'"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_should_be_idempotent(tmp_path: Path) -> None:
|
|
177
|
+
home_directory = tmp_path / "home"
|
|
178
|
+
home_directory.mkdir()
|
|
179
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
180
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
181
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
182
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
183
|
+
_initialize_repository(repository_path, environment)
|
|
184
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
185
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
186
|
+
|
|
187
|
+
first_exit_code = bugteam_fix_hookspath.main(
|
|
188
|
+
["--repo-root", str(repository_path)],
|
|
189
|
+
environment_overrides=environment,
|
|
190
|
+
)
|
|
191
|
+
second_exit_code = bugteam_fix_hookspath.main(
|
|
192
|
+
["--repo-root", str(repository_path)],
|
|
193
|
+
environment_overrides=environment,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
assert first_exit_code == 0
|
|
197
|
+
assert second_exit_code == 0, "second invocation must succeed without errors"
|
|
198
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_should_no_op_when_already_clean(tmp_path: Path) -> None:
|
|
202
|
+
home_directory = tmp_path / "home"
|
|
203
|
+
home_directory.mkdir()
|
|
204
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
205
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
206
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
207
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
208
|
+
_initialize_repository(repository_path, environment)
|
|
209
|
+
|
|
210
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
211
|
+
["--repo-root", str(repository_path)],
|
|
212
|
+
environment_overrides=environment,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
assert exit_code == 0
|
|
216
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
217
|
+
assert (
|
|
218
|
+
_read_global_hooks_path(environment)
|
|
219
|
+
.replace("\\", "/")
|
|
220
|
+
.rstrip("/")
|
|
221
|
+
.endswith("hooks/git-hooks")
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_should_exit_nonzero_when_canonical_hooks_directory_missing(
|
|
226
|
+
tmp_path: Path,
|
|
227
|
+
capsys: pytest.CaptureFixture[str],
|
|
228
|
+
) -> None:
|
|
229
|
+
home_directory = tmp_path / "home"
|
|
230
|
+
home_directory.mkdir()
|
|
231
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
232
|
+
repository_path = tmp_path / "synthetic-repo"
|
|
233
|
+
_initialize_repository(repository_path, environment)
|
|
234
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
235
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
236
|
+
|
|
237
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
238
|
+
["--repo-root", str(repository_path)],
|
|
239
|
+
environment_overrides=environment,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert exit_code != 0, (
|
|
243
|
+
"fix script must fail clearly when ~/.claude/hooks/git-hooks does not exist "
|
|
244
|
+
"so the user knows to run `npx claude-dev-env .`"
|
|
245
|
+
)
|
|
246
|
+
captured_streams = capsys.readouterr()
|
|
247
|
+
assert "hooks/git-hooks" in captured_streams.err.replace("\\", "/")
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_should_handle_paths_with_spaces(tmp_path: Path) -> None:
|
|
251
|
+
home_directory = tmp_path / "home with space"
|
|
252
|
+
home_directory.mkdir()
|
|
253
|
+
environment = _make_isolated_git_environment(home_directory)
|
|
254
|
+
canonical_hooks_directory = _create_canonical_hooks_directory(home_directory)
|
|
255
|
+
_set_global_hooks_path(str(canonical_hooks_directory), environment)
|
|
256
|
+
repository_path = tmp_path / "repo with space"
|
|
257
|
+
_initialize_repository(repository_path, environment)
|
|
258
|
+
stale_local_value = str(repository_path / ".git" / "hooks")
|
|
259
|
+
_set_local_hooks_path(repository_path, stale_local_value, environment)
|
|
260
|
+
|
|
261
|
+
exit_code = bugteam_fix_hookspath.main(
|
|
262
|
+
["--repo-root", str(repository_path)],
|
|
263
|
+
environment_overrides=environment,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
assert exit_code == 0
|
|
267
|
+
assert _read_local_hooks_path(repository_path, environment) == ""
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: logifix
|
|
3
|
+
description: Restore the Logitech Gaming Software (LCore) tray icon when it disappears on Windows. Calls a PowerShell script that reproduces the verified Session 2 recovery procedure from 2026-04-25. Triggers on "logifix", "/logifix", "logitech tray icon missing", "LCore tray gone", "logitech is not loaded".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# logifix
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Restore the Logitech Gaming Software (LCore) tray icon by reproducing the verified recovery procedure documented in `sessions/System Support/2. Logitech Tray Icon Fix Recurrence.md`.
|
|
11
|
+
|
|
12
|
+
**Announce at start:** "Running /logifix to restore the LCore tray icon."
|
|
13
|
+
|
|
14
|
+
## What the script does
|
|
15
|
+
|
|
16
|
+
`scripts/logifix.ps1` runs this sequence (verified during Session 2 on 2026-04-25):
|
|
17
|
+
|
|
18
|
+
1. **State snapshot.** Explorer instances per session, Logitech services state, LCore process state.
|
|
19
|
+
2. **Stop LCore in user context.** `Stop-Process -Name LCore -Force`.
|
|
20
|
+
3. **Single elevated UAC step:**
|
|
21
|
+
- `Start-Service -Name LogiRegistryService` (handles the recurrence case where the service is Stopped).
|
|
22
|
+
- `Stop-Process -Name explorer -Force`.
|
|
23
|
+
- **Does NOT** call `Start-Process explorer` from inside the elevated block. Windows shell auto-respawn handles the user-session explorer cleanly. Skipping this line is the operative fix discovered in Session 2 — including it created a duplicate admin-context `explorer.exe` (no resolvable owner, parent = the admin pwsh) that blocked LCore tray registration.
|
|
24
|
+
4. **Wait for Windows shell auto-respawn** (default 5 seconds).
|
|
25
|
+
5. **Verify exactly one explorer in the user's session.**
|
|
26
|
+
6. **Perform `LCoreRelaunchAttemptCount` full stop-then-launch cycles of `LCore.exe /minimized`** (default 2). The full count always runs — a responsive LCore on the first attempt does NOT imply the tray icon registered. Per Session 2 gotcha #2, the first relaunch can leave LCore responsive with no tray icon; only a second stop+launch reliably triggers `Shell_NotifyIcon` registration.
|
|
27
|
+
7. **Final state snapshot.**
|
|
28
|
+
|
|
29
|
+
## Invocation
|
|
30
|
+
|
|
31
|
+
From Claude Code:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
/logifix
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Direct PowerShell:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
pwsh -File "$HOME\.claude\skills\logifix\scripts\logifix.ps1"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Optional parameters (all have safe defaults):
|
|
44
|
+
|
|
45
|
+
- `-ExplorerAutoRespawnWaitSeconds <int>` — wait after the elevated kill (default 5).
|
|
46
|
+
- `-LCoreInitializationWaitSeconds <int>` — wait after each LCore relaunch before checking responsiveness (default 5).
|
|
47
|
+
- `-LCoreRelaunchAttemptCount <int>` — number of LCore stop+launch cycles to perform (default 2). Always runs the full count.
|
|
48
|
+
|
|
49
|
+
## When to use
|
|
50
|
+
|
|
51
|
+
- LCore tray icon missing (also confirmed absent from the overflow `^` chevron).
|
|
52
|
+
- LCore process is running but the tray icon never appears.
|
|
53
|
+
- After resume from sleep, system restart, or a Logitech service crash that leaves LCore in a half-loaded state.
|
|
54
|
+
|
|
55
|
+
## Fallback (if the script does not restore the icon)
|
|
56
|
+
|
|
57
|
+
If `/logifix` reports that UAC was canceled, or LCore is still not responding after both relaunch cycles:
|
|
58
|
+
|
|
59
|
+
1. **Ctrl+Shift+Esc** → Task Manager.
|
|
60
|
+
2. Find **Windows Explorer** → right-click → **Restart**.
|
|
61
|
+
3. Re-run `/logifix`.
|
|
62
|
+
|
|
63
|
+
The Task Manager restart path is guaranteed to hit the correct interactive session and elevation context regardless of how the calling shell was launched. In Session 2, elevated calls from Claude Code's PowerShell tool could not be confirmed to reach the user's interactive Session 1 — Task Manager bypasses that ambiguity.
|
|
64
|
+
|
|
65
|
+
## Source
|
|
66
|
+
|
|
67
|
+
Procedure verified during Session 2 (Logitech Tray Icon Fix Recurrence), 2026-04-25. The session log lists the verified command set, the gotcha catalog, and the final process/service state.
|
|
68
|
+
|
|
69
|
+
The "always run the full relaunch count" behavior was added on 2026-04-26 after `/logifix` reported success on the first responsive attempt but the tray icon never appeared — the original early-break optimization conflicted with documented Session 2 gotcha #2 (responsive LCore, no tray icon).
|