claude-dev-env 1.50.4 → 1.51.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/CLAUDE.md +0 -8
- package/_shared/pr-loop/audit-contract.md +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/preflight_self_heal_constants.py +28 -0
- package/_shared/pr-loop/scripts/preflight.py +18 -6
- package/_shared/pr-loop/scripts/preflight_self_heal.py +164 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +39 -0
- package/_shared/pr-loop/scripts/tests/test_preflight_self_heal.py +273 -0
- package/agents/clean-coder.md +1 -1
- package/agents/code-quality-agent.md +7 -5
- package/audit-rubrics/category_rubrics/category-a-api-contracts.md +3 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +3 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +8 -2
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +3 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +39 -0
- package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +40 -0
- package/audit-rubrics/prompts/category-a-api-contracts.md +11 -4
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +1 -1
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +1 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +1 -1
- package/audit-rubrics/prompts/category-f-silent-failures.md +13 -2
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +1 -1
- package/audit-rubrics/prompts/category-h-security-boundaries.md +1 -1
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +15 -5
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +1 -1
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +1 -1
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +10 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +74 -0
- package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +75 -0
- package/docs/CODE_RULES.md +24 -346
- package/package.json +1 -1
- package/rules/ask-user-question-required.md +2 -41
- package/rules/confirm-implementation-forks.md +3 -44
- package/rules/gh-body-file.md +2 -78
- package/rules/gh-paginate.md +2 -78
- package/rules/plain-language.md +2 -41
- package/rules/prompt-workflow-context-controls.md +9 -38
- package/rules/shell-invocation-policy.md +2 -141
- package/rules/testing.md +10 -0
- package/rules/vault-context.md +3 -32
- package/rules/windows-filesystem-safe.md +3 -87
- package/scripts/sync_to_cursor/rules.py +201 -79
- package/scripts/tests/test_sync_to_cursor.py +122 -26
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +2 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +51 -4
- package/skills/auditing-claude-config/SKILL.md +6 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/PROMPTS.md +8 -6
- package/skills/bugteam/SKILL.md +5 -5
- package/skills/bugteam/reference/audit-and-teammates.md +1 -1
- package/skills/bugteam/reference/audit-contract.md +4 -4
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
- package/skills/bugteam/reference/team-setup.md +17 -5
- package/skills/bugteam/scripts/bugteam_preflight.py +22 -10
- package/skills/bugteam/scripts/test_bugteam_preflight.py +32 -0
- package/skills/copilot-review/SKILL.md +5 -8
- package/skills/doc-gist/SKILL.md +5 -8
- package/skills/fixbugs/SKILL.md +1 -1
- package/skills/gh-paginate/SKILL.md +84 -0
- package/skills/pre-compact/SKILL.md +4 -9
- package/skills/refine/SKILL.md +8 -2
- package/skills/structure-prompt/SKILL.md +5 -10
package/CLAUDE.md
CHANGED
|
@@ -9,8 +9,6 @@ The user delegates execution to you and expects zero manual steps unless strictl
|
|
|
9
9
|
## Code Rules
|
|
10
10
|
@~/.claude/docs/CODE_RULES.md
|
|
11
11
|
|
|
12
|
-
When an edit deletes or rewrites code, delete everything it orphans in the same edit — unused variables, uncalled functions, unpassed parameters, dead branches, unused imports — once Serena's `find_referencing_symbols` (plus a text search for dynamic lookups) confirms they're unreachable from any live entry point, not merely unreferenced; when liveness is uncertain, ask via AskUserQuestion rather than risk deleting live code (CODE_RULES.md §9.8).
|
|
13
|
-
|
|
14
12
|
ALWAYS call the AskUserQuestion tool if you have a question for the user. Provide content-appropriate default options, with a flag for the recommended one.
|
|
15
13
|
|
|
16
14
|
## Timeless Documentation (all `.md` files)
|
|
@@ -29,12 +27,6 @@ When making code changes, make sure you are working in the proper worktree path
|
|
|
29
27
|
|
|
30
28
|
`Edit` changes existing files; `Write` creates new ones. Default to `Edit` — reach for `Write` only for a genuinely new path. For a true full rewrite, delete the file first, then `Write`.
|
|
31
29
|
|
|
32
|
-
## File-Global Constants
|
|
33
|
-
|
|
34
|
-
**file_global_constants_use_count:** Every module-level constant in production code outside `config/` must be referenced by at least two methods, functions, or classes in the same file. One reference → move to `config/` and import as a local alias. Zero references → delete (dead code). Test files are exempt.
|
|
35
|
-
|
|
36
|
-
Full rule including the decision table, examples, and exemption details: [`packages/claude-dev-env/rules/file-global-constants.md`](rules/file-global-constants.md).
|
|
37
|
-
|
|
38
30
|
## Test Philosophy
|
|
39
31
|
|
|
40
32
|
When writing tests, always write tests that actually test the behavior of the function against actual, real data and environments.
|
|
@@ -21,7 +21,7 @@ Each finding an audit produces MUST be one of exactly two shapes.
|
|
|
21
21
|
"id": "loop<N>-<K>",
|
|
22
22
|
"file": "path/relative/to/repo/root.py",
|
|
23
23
|
"line": 123,
|
|
24
|
-
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N",
|
|
24
|
+
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
|
|
25
25
|
"severity": "P0 | P1 | P2",
|
|
26
26
|
"excerpt": "verbatim code snippet from the offending line(s)",
|
|
27
27
|
"failure_mode": "one sentence describing what goes wrong and when",
|
|
@@ -37,7 +37,7 @@ Used when an audit investigates a category and does NOT find a bug. Bare "verifi
|
|
|
37
37
|
|
|
38
38
|
```json
|
|
39
39
|
{
|
|
40
|
-
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N",
|
|
40
|
+
"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P",
|
|
41
41
|
"files_opened": ["file1.py", "file2.py"],
|
|
42
42
|
"lines_quoted": [
|
|
43
43
|
{"file": "file1.py", "line": 88, "text": "verbatim line content"}
|
|
@@ -120,7 +120,7 @@ Sequence:
|
|
|
120
120
|
3. Run `py_compile` (or language-equivalent) on each modified file.
|
|
121
121
|
4. Compute `fix_diff` against pre-fix contents for the modified set.
|
|
122
122
|
5. Run `bugteam_code_rules_gate.py` with explicit paths for every modified file.
|
|
123
|
-
6. Spawn a scoped audit of `fix_diff` with full A–
|
|
123
|
+
6. Spawn a scoped audit of `fix_diff` with full A–P rigor, Shape A/B contract, adversarial pass, AND Haiku secondary in parallel (paranoid mode on post-fix).
|
|
124
124
|
7. Read the previous loop's outcome XML (`<worktree_path>/.bugteam-pr<N>-loop<L-1>.outcomes.xml`) and obtain its total finding count. If this is the first loop (L <= 1) or the file does not exist, skip this comparison. Compute the post-fix total: previous total minus bugs fixed in this round plus new violations found in the post-fix audit (step 6). If the post-fix total exceeds the previous total, flag all new findings as same-loop fix-targets and revise. An increase in total findings across loop transitions is a regression.
|
|
125
125
|
8. Any new findings become same-loop fix-targets. Internal iteration count increments by one.
|
|
126
126
|
9. After 3 internal iterations with fresh findings each time, exit `stuck: post-fix audit not converging`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Configuration constants for the preflight self-heal helper.
|
|
2
|
+
|
|
3
|
+
The helper unsets any local-scope ``core.hooksPath`` entry git seeds into a
|
|
4
|
+
new worktree's config so the canonical global setting takes effect without
|
|
5
|
+
preflight surfacing a failure or invoking the auto-remediation script.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ALL_GIT_CONFIG_LOCAL_GET_ALL_HOOKS_PATH_ARGUMENTS: tuple[str, ...] = (
|
|
11
|
+
"config",
|
|
12
|
+
"--local",
|
|
13
|
+
"--get-all",
|
|
14
|
+
"core.hooksPath",
|
|
15
|
+
)
|
|
16
|
+
ALL_GIT_CONFIG_LOCAL_UNSET_ALL_HOOKS_PATH_ARGUMENTS: tuple[str, ...] = (
|
|
17
|
+
"config",
|
|
18
|
+
"--local",
|
|
19
|
+
"--unset-all",
|
|
20
|
+
"core.hooksPath",
|
|
21
|
+
)
|
|
22
|
+
ALL_GIT_CONFIG_GLOBAL_GET_ALL_HOOKS_PATH_COMMAND: tuple[str, ...] = (
|
|
23
|
+
"git",
|
|
24
|
+
"config",
|
|
25
|
+
"--global",
|
|
26
|
+
"--get-all",
|
|
27
|
+
"core.hooksPath",
|
|
28
|
+
)
|
|
@@ -39,6 +39,7 @@ from pr_loop_shared_constants.preflight_constants import (
|
|
|
39
39
|
PYTHON_FILE_SUFFIX,
|
|
40
40
|
TESTS_DIRECTORY_NAME,
|
|
41
41
|
)
|
|
42
|
+
from preflight_self_heal import silently_clear_stale_local_hooks_path_override # noqa: E402
|
|
42
43
|
from reviews_disabled import (
|
|
43
44
|
CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
|
|
44
45
|
CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
|
|
@@ -50,9 +51,17 @@ from reviews_disabled import (
|
|
|
50
51
|
def verify_git_hooks_path(repository_root: Path | None = None) -> int:
|
|
51
52
|
"""Check that core.hooksPath resolves to the claude-dev-env git-hooks directory.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
Silently clears any stale, non-canonical local-scope core.hooksPath
|
|
55
|
+
override before querying the effective config, so a worktree-seeded local
|
|
56
|
+
entry cannot shadow a correctly configured global setting. When
|
|
57
|
+
*repository_root* is provided, queries the effective config for that
|
|
58
|
+
repository (``git -C <root> config --get``). When a canonical global
|
|
59
|
+
``core.hooksPath`` is already configured, the preceding self-heal step
|
|
60
|
+
clears non-canonical local-scope entries, so repo-level overrides such
|
|
61
|
+
as Husky or lefthook at local scope are silently removed in favor of
|
|
62
|
+
the canonical global; when the global is unset or non-canonical, the
|
|
63
|
+
self-heal stands down and the ``--get`` query still surfaces those
|
|
64
|
+
overrides through the failure path. Falls back to the current working
|
|
56
65
|
directory's effective config when *repository_root* is None.
|
|
57
66
|
|
|
58
67
|
Args:
|
|
@@ -64,6 +73,9 @@ def verify_git_hooks_path(repository_root: Path | None = None) -> int:
|
|
|
64
73
|
Non-zero and prints a correction message when unset or pointing elsewhere.
|
|
65
74
|
"""
|
|
66
75
|
expected_hooks_path_suffix = HOOKS_PATH_VERIFICATION_SUFFIX
|
|
76
|
+
silently_clear_stale_local_hooks_path_override(
|
|
77
|
+
repository_root, expected_hooks_path_suffix
|
|
78
|
+
)
|
|
67
79
|
enforcement_absent_message = (
|
|
68
80
|
"Git-side CODE_RULES enforcement is not active on this host.\n"
|
|
69
81
|
"Run: npx claude-dev-env .\n"
|
|
@@ -75,7 +87,7 @@ def verify_git_hooks_path(repository_root: Path | None = None) -> int:
|
|
|
75
87
|
git_command.extend(["-C", str(repository_root)])
|
|
76
88
|
git_command.extend(list(ALL_GIT_CONFIG_GET_CORE_HOOKS_PATH_SUBCOMMAND))
|
|
77
89
|
try:
|
|
78
|
-
|
|
90
|
+
query_completed_process = subprocess.run(
|
|
79
91
|
git_command,
|
|
80
92
|
capture_output=True,
|
|
81
93
|
text=True,
|
|
@@ -97,13 +109,13 @@ def verify_git_hooks_path(repository_root: Path | None = None) -> int:
|
|
|
97
109
|
file=sys.stderr,
|
|
98
110
|
)
|
|
99
111
|
return 1
|
|
100
|
-
if
|
|
112
|
+
if query_completed_process.returncode != 0:
|
|
101
113
|
print(
|
|
102
114
|
f"bugteam_preflight: {enforcement_absent_message}",
|
|
103
115
|
file=sys.stderr,
|
|
104
116
|
)
|
|
105
117
|
return 1
|
|
106
|
-
configured_path =
|
|
118
|
+
configured_path = query_completed_process.stdout.strip().replace("\\", "/").rstrip("/")
|
|
107
119
|
if not configured_path.endswith(expected_hooks_path_suffix):
|
|
108
120
|
print(
|
|
109
121
|
f"bugteam_preflight: core.hooksPath is '{configured_path}' — "
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Self-heal helper for stale local-scope ``core.hooksPath`` overrides.
|
|
2
|
+
|
|
3
|
+
Git seeds ``core.hooksPath = <repo>/.git/hooks`` into every new worktree's
|
|
4
|
+
local config. That repo-local entry shadows the correct global setting and
|
|
5
|
+
breaks downstream hook-dependent skills. The helper here is called from both
|
|
6
|
+
:mod:`bugteam_preflight` (skill-local) and :mod:`preflight` (shared) so the
|
|
7
|
+
shadowing entry is cleared the first time preflight runs against a fresh
|
|
8
|
+
worktree, without any caller-visible failure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
_parent_directory = str(Path(__file__).resolve().parent)
|
|
18
|
+
if _parent_directory not in sys.path:
|
|
19
|
+
sys.path.insert(0, _parent_directory)
|
|
20
|
+
|
|
21
|
+
from pr_loop_shared_constants.preflight_self_heal_constants import ( # noqa: E402
|
|
22
|
+
ALL_GIT_CONFIG_GLOBAL_GET_ALL_HOOKS_PATH_COMMAND,
|
|
23
|
+
ALL_GIT_CONFIG_LOCAL_GET_ALL_HOOKS_PATH_ARGUMENTS,
|
|
24
|
+
ALL_GIT_CONFIG_LOCAL_UNSET_ALL_HOOKS_PATH_ARGUMENTS,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _is_canonical_hooks_path_entry(
|
|
29
|
+
raw_hooks_path_entry: str,
|
|
30
|
+
expected_hooks_path_suffix: str,
|
|
31
|
+
) -> bool:
|
|
32
|
+
"""Return True when *raw_hooks_path_entry* matches the canonical hooks suffix.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
raw_hooks_path_entry: A core.hooksPath entry as written in git config.
|
|
36
|
+
expected_hooks_path_suffix: The canonical suffix the caller expects
|
|
37
|
+
(the bugteam and shared callers each pass their own constant so
|
|
38
|
+
the helper does not need to know which suffix is in force).
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
True when, after Windows-to-POSIX separator normalization and trailing
|
|
42
|
+
slash stripping, the entry ends with *expected_hooks_path_suffix*.
|
|
43
|
+
"""
|
|
44
|
+
return (
|
|
45
|
+
raw_hooks_path_entry.replace("\\", "/")
|
|
46
|
+
.rstrip("/")
|
|
47
|
+
.endswith(expected_hooks_path_suffix)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _canonical_global_hooks_path_is_set(expected_hooks_path_suffix: str) -> bool:
|
|
52
|
+
"""Return True when ``git config --global core.hooksPath`` has a canonical value.
|
|
53
|
+
|
|
54
|
+
Reads the global scope with ``--get-all`` so a multi-valued global key
|
|
55
|
+
does not produce a git "more than one value" exit code. Any one canonical
|
|
56
|
+
entry among the global values is enough.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
expected_hooks_path_suffix: The canonical suffix to check against.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
True when at least one global value normalizes to the canonical
|
|
63
|
+
suffix; False when git is missing, the read fails, or no global value
|
|
64
|
+
is canonical.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
global_read_completed_process = subprocess.run(
|
|
68
|
+
list(ALL_GIT_CONFIG_GLOBAL_GET_ALL_HOOKS_PATH_COMMAND),
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
errors="replace",
|
|
73
|
+
check=False,
|
|
74
|
+
)
|
|
75
|
+
except (FileNotFoundError, OSError):
|
|
76
|
+
return False
|
|
77
|
+
if global_read_completed_process.returncode != 0:
|
|
78
|
+
return False
|
|
79
|
+
all_global_hooks_path_entries = [
|
|
80
|
+
each_line.strip()
|
|
81
|
+
for each_line in global_read_completed_process.stdout.splitlines()
|
|
82
|
+
if each_line.strip()
|
|
83
|
+
]
|
|
84
|
+
return any(
|
|
85
|
+
_is_canonical_hooks_path_entry(
|
|
86
|
+
each_global_hooks_path_entry, expected_hooks_path_suffix
|
|
87
|
+
)
|
|
88
|
+
for each_global_hooks_path_entry in all_global_hooks_path_entries
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def silently_clear_stale_local_hooks_path_override(
|
|
93
|
+
repository_root: Path | None,
|
|
94
|
+
expected_hooks_path_suffix: str,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Remove every stale, non-canonical local-scope core.hooksPath override.
|
|
97
|
+
|
|
98
|
+
The unset runs only when BOTH conditions hold: at least one local-scope
|
|
99
|
+
entry is non-canonical, AND a canonical global setting is already
|
|
100
|
+
configured. When the global is unset or non-canonical, the helper stands
|
|
101
|
+
down so the downstream ``core.hooksPath is '<path>'`` diagnostic stays
|
|
102
|
+
informative and the auto-remediation script can repair the global from a
|
|
103
|
+
known starting point.
|
|
104
|
+
|
|
105
|
+
Silent on every git outcome — read errors, write errors, and process
|
|
106
|
+
launch errors are all suppressed so an unrelated git failure cannot block
|
|
107
|
+
preflight. The caller's subsequent ``--get`` verification step surfaces
|
|
108
|
+
the final config state through the normal failure path, so a real
|
|
109
|
+
misconfiguration is still reported with the canonical
|
|
110
|
+
``core.hooksPath is '<path>'`` diagnostic.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
repository_root: Repository root to operate on; a None argument is a
|
|
114
|
+
no-op so callers without a resolved root can call unconditionally.
|
|
115
|
+
expected_hooks_path_suffix: The canonical suffix used to classify
|
|
116
|
+
entries as canonical or stale.
|
|
117
|
+
"""
|
|
118
|
+
if repository_root is None:
|
|
119
|
+
return
|
|
120
|
+
read_command: list[str] = ["git", "-C", str(repository_root)]
|
|
121
|
+
read_command.extend(list(ALL_GIT_CONFIG_LOCAL_GET_ALL_HOOKS_PATH_ARGUMENTS))
|
|
122
|
+
try:
|
|
123
|
+
read_completed_process = subprocess.run(
|
|
124
|
+
read_command,
|
|
125
|
+
capture_output=True,
|
|
126
|
+
text=True,
|
|
127
|
+
encoding="utf-8",
|
|
128
|
+
errors="replace",
|
|
129
|
+
check=False,
|
|
130
|
+
)
|
|
131
|
+
except (FileNotFoundError, OSError):
|
|
132
|
+
return
|
|
133
|
+
if read_completed_process.returncode != 0:
|
|
134
|
+
return
|
|
135
|
+
all_local_hooks_path_entries = [
|
|
136
|
+
each_line.strip()
|
|
137
|
+
for each_line in read_completed_process.stdout.splitlines()
|
|
138
|
+
if each_line.strip()
|
|
139
|
+
]
|
|
140
|
+
if not all_local_hooks_path_entries:
|
|
141
|
+
return
|
|
142
|
+
has_non_canonical_local_hooks_path_entry = any(
|
|
143
|
+
not _is_canonical_hooks_path_entry(
|
|
144
|
+
each_local_hooks_path_entry, expected_hooks_path_suffix
|
|
145
|
+
)
|
|
146
|
+
for each_local_hooks_path_entry in all_local_hooks_path_entries
|
|
147
|
+
)
|
|
148
|
+
if not has_non_canonical_local_hooks_path_entry:
|
|
149
|
+
return
|
|
150
|
+
if not _canonical_global_hooks_path_is_set(expected_hooks_path_suffix):
|
|
151
|
+
return
|
|
152
|
+
unset_command: list[str] = ["git", "-C", str(repository_root)]
|
|
153
|
+
unset_command.extend(list(ALL_GIT_CONFIG_LOCAL_UNSET_ALL_HOOKS_PATH_ARGUMENTS))
|
|
154
|
+
try:
|
|
155
|
+
subprocess.run(
|
|
156
|
+
unset_command,
|
|
157
|
+
capture_output=True,
|
|
158
|
+
text=True,
|
|
159
|
+
encoding="utf-8",
|
|
160
|
+
errors="replace",
|
|
161
|
+
check=False,
|
|
162
|
+
)
|
|
163
|
+
except (FileNotFoundError, OSError):
|
|
164
|
+
return
|
|
@@ -233,6 +233,44 @@ def test_preflight_uses_shared_hooks_path_suffix_constant() -> None:
|
|
|
233
233
|
assert exit_code == 0
|
|
234
234
|
|
|
235
235
|
|
|
236
|
+
def test_verify_git_hooks_path_invokes_self_heal_before_effective_query(
|
|
237
|
+
tmp_path: Path,
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Shared verify_git_hooks_path must delegate to self-heal before --get.
|
|
240
|
+
|
|
241
|
+
Mirrors the bugteam-side guard so a future refactor that drops the
|
|
242
|
+
silently_clear_stale_local_hooks_path_override call in the shared
|
|
243
|
+
preflight would be caught.
|
|
244
|
+
"""
|
|
245
|
+
canonical_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
246
|
+
canonical_hooks_path.mkdir(parents=True)
|
|
247
|
+
with patch("subprocess.run") as mock_run:
|
|
248
|
+
mock_run.return_value = _make_completed_process(
|
|
249
|
+
str(canonical_hooks_path) + "\n", returncode=0
|
|
250
|
+
)
|
|
251
|
+
preflight.verify_git_hooks_path(tmp_path)
|
|
252
|
+
all_called_commands = [
|
|
253
|
+
each_call_args[0][0] for each_call_args in mock_run.call_args_list
|
|
254
|
+
]
|
|
255
|
+
has_self_heal_local_read = any(
|
|
256
|
+
"--get-all" in each_command for each_command in all_called_commands
|
|
257
|
+
)
|
|
258
|
+
has_effective_query = any(
|
|
259
|
+
"--get" in each_command and "--get-all" not in each_command
|
|
260
|
+
for each_command in all_called_commands
|
|
261
|
+
)
|
|
262
|
+
assert has_self_heal_local_read, (
|
|
263
|
+
"verify_git_hooks_path must run the --local --get-all read for self-heal"
|
|
264
|
+
)
|
|
265
|
+
assert has_effective_query, (
|
|
266
|
+
"verify_git_hooks_path must still run the effective --get verification"
|
|
267
|
+
)
|
|
268
|
+
first_called_command = all_called_commands[0]
|
|
269
|
+
assert "--get-all" in first_called_command, (
|
|
270
|
+
"Self-heal must run BEFORE the effective config query, not after"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
236
274
|
def test_preflight_skip_uses_shared_env_var_constant(
|
|
237
275
|
capsys: pytest.CaptureFixture[str],
|
|
238
276
|
monkeypatch: pytest.MonkeyPatch,
|
|
@@ -428,6 +466,7 @@ def test_main_should_not_double_print_when_git_ls_fails(
|
|
|
428
466
|
patch.object(preflight, "run_pytest", return_value=0) as mock_pytest,
|
|
429
467
|
):
|
|
430
468
|
mock_run.side_effect = [
|
|
469
|
+
mock_hooks_result,
|
|
431
470
|
mock_hooks_result,
|
|
432
471
|
subprocess.CalledProcessError(128, ["git", "ls-files"]),
|
|
433
472
|
]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Tests for the preflight self-heal helper.
|
|
2
|
+
|
|
3
|
+
Each test mocks ``subprocess.run`` with an ordered ``side_effect`` list so the
|
|
4
|
+
helper's three potential calls (local read, global read, local unset) are
|
|
5
|
+
distinguishable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import MagicMock, patch
|
|
14
|
+
|
|
15
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
16
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
18
|
+
|
|
19
|
+
import preflight_self_heal
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CANONICAL_SUFFIX = "hooks/git-hooks"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make_completed_process(
|
|
26
|
+
stdout: str, returncode: int
|
|
27
|
+
) -> subprocess.CompletedProcess:
|
|
28
|
+
process = MagicMock(spec=subprocess.CompletedProcess)
|
|
29
|
+
process.stdout = stdout
|
|
30
|
+
process.returncode = returncode
|
|
31
|
+
return process
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _was_called_with_argument(
|
|
35
|
+
mock_subprocess_run: MagicMock, expected_argument: str
|
|
36
|
+
) -> bool:
|
|
37
|
+
"""Return True when *expected_argument* appears as an element of any call's argv.
|
|
38
|
+
|
|
39
|
+
Uses list-membership (Python ``in`` on a list is element-equality), so
|
|
40
|
+
`"--get"` matches `["git", "config", "--get", ...]` but not
|
|
41
|
+
`["git", "config", "--get-all", ...]`.
|
|
42
|
+
"""
|
|
43
|
+
return any(
|
|
44
|
+
expected_argument in each_call_args[0][0]
|
|
45
|
+
for each_call_args in mock_subprocess_run.call_args_list
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_is_canonical_when_suffix_matches() -> None:
|
|
50
|
+
assert preflight_self_heal._is_canonical_hooks_path_entry(
|
|
51
|
+
"/home/me/.claude/hooks/git-hooks", CANONICAL_SUFFIX
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_is_canonical_normalizes_windows_separators_and_trailing_slash() -> None:
|
|
56
|
+
assert preflight_self_heal._is_canonical_hooks_path_entry(
|
|
57
|
+
"C:\\Users\\me\\.claude\\hooks\\git-hooks\\", CANONICAL_SUFFIX
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_is_canonical_rejects_non_matching_suffix() -> None:
|
|
62
|
+
assert not preflight_self_heal._is_canonical_hooks_path_entry(
|
|
63
|
+
"/some/other/path/.husky", CANONICAL_SUFFIX
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_is_canonical_rejects_seeded_git_hooks_path() -> None:
|
|
68
|
+
assert not preflight_self_heal._is_canonical_hooks_path_entry(
|
|
69
|
+
"/repo/.git/hooks", CANONICAL_SUFFIX
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_canonical_global_helper_returns_true_for_canonical_value() -> None:
|
|
74
|
+
with patch("subprocess.run") as mock_run:
|
|
75
|
+
mock_run.return_value = _make_completed_process(
|
|
76
|
+
"/home/me/.claude/hooks/git-hooks\n", returncode=0
|
|
77
|
+
)
|
|
78
|
+
assert preflight_self_heal._canonical_global_hooks_path_is_set(CANONICAL_SUFFIX)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_canonical_global_helper_returns_false_when_unset() -> None:
|
|
82
|
+
with patch("subprocess.run") as mock_run:
|
|
83
|
+
mock_run.return_value = _make_completed_process("", returncode=1)
|
|
84
|
+
assert not preflight_self_heal._canonical_global_hooks_path_is_set(
|
|
85
|
+
CANONICAL_SUFFIX
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_canonical_global_helper_returns_false_when_non_canonical() -> None:
|
|
90
|
+
with patch("subprocess.run") as mock_run:
|
|
91
|
+
mock_run.return_value = _make_completed_process(
|
|
92
|
+
"/some/other/path/.husky\n", returncode=0
|
|
93
|
+
)
|
|
94
|
+
assert not preflight_self_heal._canonical_global_hooks_path_is_set(
|
|
95
|
+
CANONICAL_SUFFIX
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_canonical_global_helper_normalizes_trailing_whitespace_and_separators() -> (
|
|
100
|
+
None
|
|
101
|
+
):
|
|
102
|
+
with patch("subprocess.run") as mock_run:
|
|
103
|
+
mock_run.return_value = _make_completed_process(
|
|
104
|
+
" C:\\Users\\me\\.claude\\hooks\\git-hooks \n", returncode=0
|
|
105
|
+
)
|
|
106
|
+
assert preflight_self_heal._canonical_global_hooks_path_is_set(CANONICAL_SUFFIX)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_canonical_global_helper_returns_true_when_any_multi_value_is_canonical() -> (
|
|
110
|
+
None
|
|
111
|
+
):
|
|
112
|
+
multi_global_entries = "/some/other/path/.husky\n/home/me/.claude/hooks/git-hooks\n"
|
|
113
|
+
with patch("subprocess.run") as mock_run:
|
|
114
|
+
mock_run.return_value = _make_completed_process(
|
|
115
|
+
multi_global_entries, returncode=0
|
|
116
|
+
)
|
|
117
|
+
assert preflight_self_heal._canonical_global_hooks_path_is_set(CANONICAL_SUFFIX)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_canonical_global_helper_returns_false_when_git_missing() -> None:
|
|
121
|
+
with patch("subprocess.run", side_effect=FileNotFoundError()):
|
|
122
|
+
assert not preflight_self_heal._canonical_global_hooks_path_is_set(
|
|
123
|
+
CANONICAL_SUFFIX
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_canonical_global_helper_returns_false_when_os_error() -> None:
|
|
128
|
+
with patch("subprocess.run", side_effect=OSError("permission denied")):
|
|
129
|
+
assert not preflight_self_heal._canonical_global_hooks_path_is_set(
|
|
130
|
+
CANONICAL_SUFFIX
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_silent_clear_is_noop_when_repository_root_is_none() -> None:
|
|
135
|
+
with patch("subprocess.run") as mock_run:
|
|
136
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
137
|
+
None, CANONICAL_SUFFIX
|
|
138
|
+
)
|
|
139
|
+
assert mock_run.call_count == 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_silent_clear_is_noop_when_local_core_hooks_path_unset() -> None:
|
|
143
|
+
with patch("subprocess.run") as mock_run:
|
|
144
|
+
mock_run.return_value = _make_completed_process("", returncode=1)
|
|
145
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
146
|
+
Path("."), CANONICAL_SUFFIX
|
|
147
|
+
)
|
|
148
|
+
assert mock_run.call_count == 1
|
|
149
|
+
assert not _was_called_with_argument(mock_run, "--unset-all")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_silent_clear_is_noop_when_local_value_is_canonical(tmp_path: Path) -> None:
|
|
153
|
+
canonical_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
154
|
+
canonical_hooks_path.mkdir(parents=True)
|
|
155
|
+
with patch("subprocess.run") as mock_run:
|
|
156
|
+
mock_run.return_value = _make_completed_process(
|
|
157
|
+
str(canonical_hooks_path) + "\n", returncode=0
|
|
158
|
+
)
|
|
159
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
160
|
+
Path("."), CANONICAL_SUFFIX
|
|
161
|
+
)
|
|
162
|
+
assert not _was_called_with_argument(mock_run, "--unset-all")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_silent_clear_unsets_stale_worktree_seeded_local_value(tmp_path: Path) -> None:
|
|
166
|
+
"""Git seeds <repo>/.git/hooks into worktree-local config; preflight must heal it."""
|
|
167
|
+
seeded_local_path_text = "/repo/.git/hooks"
|
|
168
|
+
canonical_global_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
169
|
+
canonical_global_hooks_path.mkdir(parents=True)
|
|
170
|
+
with patch("subprocess.run") as mock_run:
|
|
171
|
+
mock_run.side_effect = [
|
|
172
|
+
_make_completed_process(seeded_local_path_text + "\n", returncode=0),
|
|
173
|
+
_make_completed_process(
|
|
174
|
+
str(canonical_global_hooks_path) + "\n", returncode=0
|
|
175
|
+
),
|
|
176
|
+
_make_completed_process("", returncode=0),
|
|
177
|
+
]
|
|
178
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
179
|
+
Path("/repo"), CANONICAL_SUFFIX
|
|
180
|
+
)
|
|
181
|
+
assert _was_called_with_argument(mock_run, "--unset-all")
|
|
182
|
+
assert _was_called_with_argument(mock_run, "--local")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_silent_clear_unsets_when_any_local_entry_is_non_canonical(
|
|
186
|
+
tmp_path: Path,
|
|
187
|
+
) -> None:
|
|
188
|
+
canonical_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
189
|
+
canonical_hooks_path.mkdir(parents=True)
|
|
190
|
+
mixed_entries_text = f"{canonical_hooks_path}\n/some/other/path/.husky\n"
|
|
191
|
+
with patch("subprocess.run") as mock_run:
|
|
192
|
+
mock_run.side_effect = [
|
|
193
|
+
_make_completed_process(mixed_entries_text, returncode=0),
|
|
194
|
+
_make_completed_process(str(canonical_hooks_path) + "\n", returncode=0),
|
|
195
|
+
_make_completed_process("", returncode=0),
|
|
196
|
+
]
|
|
197
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
198
|
+
Path("."), CANONICAL_SUFFIX
|
|
199
|
+
)
|
|
200
|
+
assert _was_called_with_argument(mock_run, "--unset-all")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_silent_clear_stands_down_when_global_is_unset() -> None:
|
|
204
|
+
seeded_local_path_text = "/repo/.git/hooks"
|
|
205
|
+
with patch("subprocess.run") as mock_run:
|
|
206
|
+
mock_run.side_effect = [
|
|
207
|
+
_make_completed_process(seeded_local_path_text + "\n", returncode=0),
|
|
208
|
+
_make_completed_process("", returncode=1),
|
|
209
|
+
]
|
|
210
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
211
|
+
Path("/repo"), CANONICAL_SUFFIX
|
|
212
|
+
)
|
|
213
|
+
assert not _was_called_with_argument(mock_run, "--unset-all")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_silent_clear_stands_down_when_global_is_non_canonical() -> None:
|
|
217
|
+
seeded_local_path_text = "/repo/.git/hooks"
|
|
218
|
+
non_canonical_global_text = "/some/other/path/.husky"
|
|
219
|
+
with patch("subprocess.run") as mock_run:
|
|
220
|
+
mock_run.side_effect = [
|
|
221
|
+
_make_completed_process(seeded_local_path_text + "\n", returncode=0),
|
|
222
|
+
_make_completed_process(non_canonical_global_text + "\n", returncode=0),
|
|
223
|
+
]
|
|
224
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
225
|
+
Path("/repo"), CANONICAL_SUFFIX
|
|
226
|
+
)
|
|
227
|
+
assert not _was_called_with_argument(mock_run, "--unset-all")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_silent_clear_swallows_file_not_found_error() -> None:
|
|
231
|
+
with patch("subprocess.run", side_effect=FileNotFoundError()):
|
|
232
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
233
|
+
Path("."), CANONICAL_SUFFIX
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_silent_clear_swallows_os_error() -> None:
|
|
238
|
+
with patch("subprocess.run", side_effect=OSError("permission denied")):
|
|
239
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
240
|
+
Path("."), CANONICAL_SUFFIX
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_silent_clear_swallows_file_not_found_error_on_unset_call(tmp_path: Path) -> None:
|
|
245
|
+
"""A spawn-level FileNotFoundError on the unset-all write must not crash preflight."""
|
|
246
|
+
seeded_local_path_text = "/repo/.git/hooks"
|
|
247
|
+
canonical_global_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
248
|
+
canonical_global_hooks_path.mkdir(parents=True)
|
|
249
|
+
with patch("subprocess.run") as mock_run:
|
|
250
|
+
mock_run.side_effect = [
|
|
251
|
+
_make_completed_process(seeded_local_path_text + "\n", returncode=0),
|
|
252
|
+
_make_completed_process(str(canonical_global_hooks_path) + "\n", returncode=0),
|
|
253
|
+
FileNotFoundError(),
|
|
254
|
+
]
|
|
255
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
256
|
+
Path("/repo"), CANONICAL_SUFFIX
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def test_silent_clear_swallows_os_error_on_unset_call(tmp_path: Path) -> None:
|
|
261
|
+
"""A spawn-level OSError on the unset-all write must not crash preflight."""
|
|
262
|
+
seeded_local_path_text = "/repo/.git/hooks"
|
|
263
|
+
canonical_global_hooks_path = tmp_path / ".claude" / "hooks" / "git-hooks"
|
|
264
|
+
canonical_global_hooks_path.mkdir(parents=True)
|
|
265
|
+
with patch("subprocess.run") as mock_run:
|
|
266
|
+
mock_run.side_effect = [
|
|
267
|
+
_make_completed_process(seeded_local_path_text + "\n", returncode=0),
|
|
268
|
+
_make_completed_process(str(canonical_global_hooks_path) + "\n", returncode=0),
|
|
269
|
+
OSError("permission denied"),
|
|
270
|
+
]
|
|
271
|
+
preflight_self_heal.silently_clear_stale_local_hooks_path_override(
|
|
272
|
+
Path("/repo"), CANONICAL_SUFFIX
|
|
273
|
+
)
|
package/agents/clean-coder.md
CHANGED
|
@@ -438,7 +438,7 @@ Docstrings on functions, methods, classes, and modules are encouraged for public
|
|
|
438
438
|
|
|
439
439
|
## Audit Awareness
|
|
440
440
|
|
|
441
|
-
Code clean-coder writes will be audited later against the A–
|
|
441
|
+
Code clean-coder writes will be audited later against the A–P bug categories from `code-quality-agent`. The hooks listed in this file enforce the Category J slice at write time, but A–I and K–P surface only in audit. For each category's full rubric, sub-bucket decomposition, and concrete checks, see `../audit-rubrics/category_rubrics/` (relative to this agent file). While generating code, anticipate the full A–P surface so the first write clears every audit category.
|
|
442
442
|
|
|
443
443
|
Three audit lanes deserve particular attention while generating new code:
|
|
444
444
|
|