claude-dev-env 1.55.1 → 1.55.2
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 +9 -0
- package/hooks/blocking/md_path_exemptions.py +33 -14
- package/hooks/blocking/md_to_html_blocker.py +3 -2
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +68 -2
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +2 -8
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +8 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -41,6 +41,15 @@ Ask the subagent for a specific answer: "return the file:line where X is defined
|
|
|
41
41
|
|
|
42
42
|
Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compose subagent prompts via the protocol in `agent-spawn-protocol`.
|
|
43
43
|
|
|
44
|
+
## Target Execution Workflow for Code Tasks
|
|
45
|
+
|
|
46
|
+
Run every multi-step code task in two phases:
|
|
47
|
+
|
|
48
|
+
1. **Coders** — one Sonnet agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the tool-less `fable-advisor` agent — which returns a plan, a correction, or a stop signal — and resumes. Source: Anthropic's advisor strategy (https://claude.com/blog/the-advisor-strategy).
|
|
49
|
+
2. **Verification** — when the coders finish, the main session spawns the `fable-verifier` agent in a fresh context. It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
|
|
50
|
+
|
|
51
|
+
Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. The one exemption is mechanical, not discretionary: a diff whose every changed file is non-code or has an unchanged Python AST once docstrings are stripped (docs, docstrings, comments).
|
|
52
|
+
|
|
44
53
|
## Additional Non-overlapping Rules
|
|
45
54
|
|
|
46
55
|
- **task_scope:** Match every action to what was explicitly requested. When intent is ambiguous, research official docs and present options via AskUserQuestion before making any changes. Proceed with edits only on explicit instruction.
|
|
@@ -21,12 +21,10 @@ from hooks_constants.md_to_html_blocker_constants import ( # noqa: E402
|
|
|
21
21
|
ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS,
|
|
22
22
|
ALL_EXEMPT_ROOT_FILENAMES_LOWER,
|
|
23
23
|
CLAUDE_DEV_ENV_REPO_NAME_SEGMENT,
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
CLAUDE_DIRECTORY_NAME,
|
|
25
|
+
CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX,
|
|
26
26
|
MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR,
|
|
27
27
|
PACKAGES_TOP_LEVEL_SEGMENT,
|
|
28
|
-
PLUGIN_DIRECTORY_PATH_PREFIX,
|
|
29
|
-
PLUGIN_DIRECTORY_SEGMENT_MARKER,
|
|
30
28
|
PLUGIN_ROOT_MARKER_DIRECTORY_NAME,
|
|
31
29
|
REPO_ROOT_MARKER_NAME,
|
|
32
30
|
RESOLVED_HOME_DIRECTORY_LOWER,
|
|
@@ -38,7 +36,9 @@ def is_exempt_path(file_path: str) -> bool:
|
|
|
38
36
|
"""Return True when the .md file path is exempt from the blocker policy.
|
|
39
37
|
|
|
40
38
|
Exemption sources, in order of evaluation:
|
|
41
|
-
- Any segment
|
|
39
|
+
- Any directory segment named `.claude` or prefixed `.claude-`
|
|
40
|
+
(case-insensitive): project infrastructure, profile directories like
|
|
41
|
+
`.claude-mel/`, and `.claude-plugin/`
|
|
42
42
|
- Basename in `ALL_EXEMPT_ANYWHERE_FILENAMES` (e.g. SKILL.md)
|
|
43
43
|
- Anchored under `packages/claude-dev-env/<one of
|
|
44
44
|
ALL_CLAUDE_CODE_SOURCE_TOP_DIRECTORIES>/...` (docs, rules,
|
|
@@ -60,15 +60,7 @@ def is_exempt_path(file_path: str) -> bool:
|
|
|
60
60
|
expanded_path = os.path.expanduser(file_path)
|
|
61
61
|
normalized = os.path.normpath(expanded_path).replace("\\", "/")
|
|
62
62
|
lower_normalized = normalized.lower()
|
|
63
|
-
if (
|
|
64
|
-
CLAUDE_DIRECTORY_SEGMENT_MARKER in lower_normalized
|
|
65
|
-
or lower_normalized.startswith(CLAUDE_DIRECTORY_PATH_PREFIX)
|
|
66
|
-
):
|
|
67
|
-
return True
|
|
68
|
-
if (
|
|
69
|
-
PLUGIN_DIRECTORY_SEGMENT_MARKER in lower_normalized
|
|
70
|
-
or lower_normalized.startswith(PLUGIN_DIRECTORY_PATH_PREFIX)
|
|
71
|
-
):
|
|
63
|
+
if _has_claude_infrastructure_segment(lower_normalized):
|
|
72
64
|
return True
|
|
73
65
|
basename_lower = os.path.basename(normalized).lower()
|
|
74
66
|
if basename_lower in ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER:
|
|
@@ -101,6 +93,33 @@ def _resolve_absolute_directory(normalized_path: str) -> str:
|
|
|
101
93
|
return os.path.abspath(directory)
|
|
102
94
|
|
|
103
95
|
|
|
96
|
+
def _has_claude_infrastructure_segment(lower_normalized_path: str) -> bool:
|
|
97
|
+
"""A directory named `.claude` or prefixed `.claude-` (profile and
|
|
98
|
+
plugin directories) holds Claude infrastructure; any path inside one
|
|
99
|
+
is exempt.
|
|
100
|
+
|
|
101
|
+
Only directory segments are matched. The final segment is always the
|
|
102
|
+
file's basename (``normpath`` strips any trailing slash), so a file
|
|
103
|
+
merely named with the ``.claude-`` prefix in an ordinary directory
|
|
104
|
+
stays subject to the policy.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
lower_normalized_path: Lowercased path with separators normalized
|
|
108
|
+
to forward slashes.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
True when any directory segment names a Claude infrastructure
|
|
112
|
+
directory.
|
|
113
|
+
"""
|
|
114
|
+
all_directory_segments = lower_normalized_path.split("/")[:-1]
|
|
115
|
+
for each_segment in all_directory_segments:
|
|
116
|
+
if each_segment == CLAUDE_DIRECTORY_NAME:
|
|
117
|
+
return True
|
|
118
|
+
if each_segment.startswith(CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX):
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
|
|
104
123
|
def _has_plugin_directory_segment(lower_normalized_path: str) -> bool:
|
|
105
124
|
for each_directory_segment in ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS:
|
|
106
125
|
segment_marker = f"/{each_directory_segment}/"
|
|
@@ -25,6 +25,7 @@ from hooks_constants.md_to_html_blocker_constants import ( # noqa: E402
|
|
|
25
25
|
ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS,
|
|
26
26
|
CLAUDE_DEV_ENV_REPO_NAME_SEGMENT,
|
|
27
27
|
CLAUDE_DIRECTORY_NAME,
|
|
28
|
+
CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX,
|
|
28
29
|
PACKAGES_TOP_LEVEL_SEGMENT,
|
|
29
30
|
PLUGIN_ROOT_MARKER_DIRECTORY_NAME,
|
|
30
31
|
)
|
|
@@ -63,7 +64,7 @@ def _block_context() -> str:
|
|
|
63
64
|
"Reference for HTML effectiveness patterns:\n"
|
|
64
65
|
f"{_html_effectiveness_url}\n"
|
|
65
66
|
"Exceptions (.md still allowed):\n"
|
|
66
|
-
f"- Files inside {CLAUDE_DIRECTORY_NAME}/ or {
|
|
67
|
+
f"- Files inside {CLAUDE_DIRECTORY_NAME}/ or {CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX}*/ directories\n"
|
|
67
68
|
f"- {_exempt_anywhere_filenames_summary} anywhere\n"
|
|
68
69
|
f"- Files under {_exempt_plugin_segments_summary} directories\n"
|
|
69
70
|
f"- Files under {_claude_dev_env_source_directories_summary} source directories\n"
|
|
@@ -79,7 +80,7 @@ def _block_system_message() -> str:
|
|
|
79
80
|
".md files are blocked in this project — generate a self-contained .html "
|
|
80
81
|
f"file instead. See {_html_effectiveness_url} for "
|
|
81
82
|
f"design patterns and examples. Exemptions: {CLAUDE_DIRECTORY_NAME}/ and "
|
|
82
|
-
f"{
|
|
83
|
+
f"{CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX}*/ infrastructure, "
|
|
83
84
|
f"{_exempt_anywhere_filenames_summary} anywhere, {_exempt_plugin_segments_summary} trees, "
|
|
84
85
|
f"{_claude_dev_env_source_directories_summary} source trees, "
|
|
85
86
|
f"files under a {PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/ root, "
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"""Tests for md_to_html_blocker directory and filename exemptions.
|
|
2
2
|
|
|
3
|
-
Covers which directory trees (`.claude/`, `.claude
|
|
4
|
-
under `packages/claude-dev-env/`, `agents/`,
|
|
3
|
+
Covers which directory trees (`.claude/`, `.claude-*/` profile and plugin
|
|
4
|
+
directories, source subtrees under `packages/claude-dev-env/`, `agents/`,
|
|
5
|
+
`skills/`, `commands/`) and which
|
|
5
6
|
root-level filenames (`README.md`, `CHANGELOG.md`, `CLAUDE.md`, `AGENTS.md`,
|
|
6
7
|
`SKILL.md`) are exempt from the `.md` block, and the segment-anchored matching
|
|
7
8
|
that prevents nested look-alike paths from bypassing the block.
|
|
@@ -366,3 +367,68 @@ def test_blocks_ordinary_docs_md_file():
|
|
|
366
367
|
assert result.returncode == 0
|
|
367
368
|
output = json.loads(result.stdout)
|
|
368
369
|
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_passes_claude_profile_memory_directory():
|
|
373
|
+
"""A Claude profile directory (`.claude-<name>/`, e.g. `.claude-mel/`)
|
|
374
|
+
carries the same infrastructure as `.claude/`; per-project memory files
|
|
375
|
+
under it accept .md writes."""
|
|
376
|
+
result = _run_hook(
|
|
377
|
+
"Write",
|
|
378
|
+
{
|
|
379
|
+
"file_path": (
|
|
380
|
+
"C:/Users/sample/.claude-mel/projects"
|
|
381
|
+
"/sample-project/memory/fact.md"
|
|
382
|
+
),
|
|
383
|
+
"content": "# Fact",
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
assert result.returncode == 0
|
|
387
|
+
assert result.stdout == ""
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_passes_relative_claude_profile_directory():
|
|
391
|
+
result = _run_hook(
|
|
392
|
+
"Write",
|
|
393
|
+
{
|
|
394
|
+
"file_path": ".claude-mel/projects/sample/memory/fact.md",
|
|
395
|
+
"content": "# Fact",
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
assert result.returncode == 0
|
|
399
|
+
assert result.stdout == ""
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def test_passes_claude_profile_directory_case_insensitive():
|
|
403
|
+
result = _run_hook(
|
|
404
|
+
"Write",
|
|
405
|
+
{"file_path": "C:/Users/sample/.Claude-Mel/MEMORY.md", "content": "# Index"},
|
|
406
|
+
)
|
|
407
|
+
assert result.returncode == 0
|
|
408
|
+
assert result.stdout == ""
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_blocks_dot_directory_that_starts_with_claude_but_lacks_hyphen():
|
|
412
|
+
"""`.claudette/` is not Claude infrastructure: only a directory named
|
|
413
|
+
exactly `.claude` or carrying the `.claude-` prefix is exempt."""
|
|
414
|
+
result = _run_hook(
|
|
415
|
+
"Write",
|
|
416
|
+
{"file_path": "notes/.claudette/intro.md", "content": "# Intro"},
|
|
417
|
+
)
|
|
418
|
+
assert result.returncode == 0
|
|
419
|
+
output = json.loads(result.stdout)
|
|
420
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_blocks_claude_prefixed_filename_in_plain_directory():
|
|
424
|
+
"""A file merely named with the `.claude-` prefix (e.g.
|
|
425
|
+
`docs/.claude-notes.md`) is not Claude infrastructure: the exemption
|
|
426
|
+
matches directory segments only, so a `.claude-*.md` basename inside
|
|
427
|
+
an ordinary directory is blocked."""
|
|
428
|
+
result = _run_hook(
|
|
429
|
+
"Write",
|
|
430
|
+
{"file_path": "docs/.claude-notes.md", "content": "# Notes"},
|
|
431
|
+
)
|
|
432
|
+
assert result.returncode == 0
|
|
433
|
+
output = json.loads(result.stdout)
|
|
434
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
@@ -24,10 +24,7 @@ REPO_ROOT_MARKER_NAME: str = ".git"
|
|
|
24
24
|
CLAUDE_DIRECTORY_NAME: str = ".claude"
|
|
25
25
|
PLUGIN_ROOT_MARKER_DIRECTORY_NAME: str = ".claude-plugin"
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
CLAUDE_DIRECTORY_PATH_PREFIX: str = f"{CLAUDE_DIRECTORY_NAME}/"
|
|
29
|
-
PLUGIN_DIRECTORY_SEGMENT_MARKER: str = f"/{PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/"
|
|
30
|
-
PLUGIN_DIRECTORY_PATH_PREFIX: str = f"{PLUGIN_ROOT_MARKER_DIRECTORY_NAME}/"
|
|
27
|
+
CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX: str = f"{CLAUDE_DIRECTORY_NAME}-"
|
|
31
28
|
|
|
32
29
|
ALL_EXEMPT_ANYWHERE_FILENAMES_LOWER: frozenset[str] = frozenset(
|
|
33
30
|
each_filename.lower() for each_filename in ALL_EXEMPT_ANYWHERE_FILENAMES
|
|
@@ -68,12 +65,9 @@ __all__ = [
|
|
|
68
65
|
"ALL_EXEMPT_ROOT_FILENAMES_LOWER",
|
|
69
66
|
"CLAUDE_DEV_ENV_REPO_NAME_SEGMENT",
|
|
70
67
|
"CLAUDE_DIRECTORY_NAME",
|
|
71
|
-
"
|
|
72
|
-
"CLAUDE_DIRECTORY_SEGMENT_MARKER",
|
|
68
|
+
"CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX",
|
|
73
69
|
"MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR",
|
|
74
70
|
"PACKAGES_TOP_LEVEL_SEGMENT",
|
|
75
|
-
"PLUGIN_DIRECTORY_PATH_PREFIX",
|
|
76
|
-
"PLUGIN_DIRECTORY_SEGMENT_MARKER",
|
|
77
71
|
"PLUGIN_ROOT_MARKER_DIRECTORY_NAME",
|
|
78
72
|
"REPO_ROOT_MARKER_NAME",
|
|
79
73
|
"RESOLVED_HOME_DIRECTORY_LOWER",
|
|
@@ -115,3 +115,11 @@ def test_plugin_root_marker_directory_name_is_dot_claude_plugin() -> None:
|
|
|
115
115
|
a plugin repo root and exempted."""
|
|
116
116
|
assert constants_module.PLUGIN_ROOT_MARKER_DIRECTORY_NAME == ".claude-plugin"
|
|
117
117
|
assert "PLUGIN_ROOT_MARKER_DIRECTORY_NAME" in constants_module.__all__
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_claude_profile_directory_name_prefix_is_dot_claude_hyphen() -> None:
|
|
121
|
+
"""A directory whose name carries the `.claude-` prefix (profile
|
|
122
|
+
directories like `.claude-mel/`, plus `.claude-plugin/`) is Claude
|
|
123
|
+
infrastructure; any path inside one bypasses the .md block."""
|
|
124
|
+
assert constants_module.CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX == ".claude-"
|
|
125
|
+
assert "CLAUDE_PROFILE_DIRECTORY_NAME_PREFIX" in constants_module.__all__
|